MySQL中的MVCC
MVCC的概念
MVCC: Multi-Version Concurrency Control,即多版本并发控制.
是乐观锁的一种实现方式.
并发事务存在的问题:
更新丢失(Lost Update):多个事务同时更新同一行时,最后的更新会覆盖之前的更新。
脏读(Dirty Reads):一个事务对记录的未提交修改被其他事务读取到。
不可重复读(Non-Repeatable Reads):一个事务内多次查询相同记录结果不一致。
幻读(Phantom Reads):一个事务重新查询之前检索过的数据,发现出现新的数据。
解决:
加读写锁。
一致性快照读(MVCC)。
特点
用来提高数据库高并发场景下的吞吐性能。
MySQL中InnoDB引擎支持MVCC。
比加行锁效率高,开销低。
在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用。
可以基于乐观锁和悲观锁实现。
使用行级锁(row_level_lock),而非行锁(innodb_row_lock).
同一个事务能够看到数据一致的视图.
事务开始的时间不同,看到相同表的数据可能不同.
基本原理
通过保留某个时间点的快照实现的.
基本特征
每行数据都存在一个版本,每次数据更新时都更新该版本.
修改数据时复制当前版本的数据进行修改,各个事务之间互不影响.
保存时比较版本号,成功(commit)则覆盖原记录,失败则放弃(rollback).
InnoDB存储引擎MVCC实现策略
细节:
每一行保存两个隐藏列:当前行创建时版本号和删除时版本号.
版本号是系统版本号,每开始一个新事务,系统版本号自增.而事务的版本号为事务开始时的系统版本号.
每个事务有自己的版本号.
MVCC下的InnoDB的增删改查
插入数据
设记录的版本号为当前事务的版本号。
向表中插入数据。
将create version设置为当前事务的版本号,delete version为空。
更新操作
将旧的记录标记为已删除,delete version为当前事务版本号。
插入一行新的记录,create version为当前事务版本号,delete version为当前版本号。
删除操作
将待删除的行的delete version设置为当前事务版本号。
查询操作
记录需满足两个条件:
delete version为空或者设置的版本号大于当前事务的版本号(即:删除操作发生在当前事务之后)
create version小于等于当前事务版本号(即:记录创建在当前事务之前)
注:
MVCC只适用于MySQL中的读已提交(Read Committed)和可重复读(Repeatable Read)。
Read uncommitted存在脏读,即:读到未提交事务的数据行。
串行化是对表加锁。
InnoDB MVCC 实现原理
实现方式:
每一行记录都有两个隐藏列:DATA_TRX_ID和DATA_ROLL_PTR。(若没有主键,则还有一个隐藏主键)
DATA_TRX_ID:记录最近更新这条记录的事务ID(6字节)
DATA_ROLL_PTR:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)
DB_ROW_ID:行标识(隐藏单增ID),没有主键时主动生成(6字节)
多事务并发操作数据
特征:
不同事务对同一行的更新操作产生多个版本。
通过回滚指针将这些版本链接成一条Undo Log链。
更新操作流程:
将待操作的行加排他锁。
将该行原本的值拷贝到Undo Log中,DB_TRX_ID和DB_ROLL_PTR保持不变。(形成历史版本)
修改该行的值,更新该行的DATA_TRX_ID为当前操作事务的事务ID,将DATA_ROLL_PTR指向第二步拷贝到Undo Log链中的旧版本记录。(通过DB_ROLL_PTR可以找到历史记录)
记录Redo Log,包括Undo Log中的修改。
INSERT操作:产生新的记录,其DATA_TRX_ID为当前插入记录的事务ID。
DELETE操作:软删除,将DATA_TRX_ID记录下删除该记录的事务ID,真正删除操作在事务提交时完成。
一致性读的实现
RU隔离级别下 ==> 直接读取版本的最新记录。
SERIALIZABLE隔离级别 ==> 通过加锁互斥访问数据实现。
RC和RR隔离级别 ==> 使用版本链(ReadView,可读视图)
RR下的ReadView生成
特点:
每个事务首次执行SELECT语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView。
每个事务后续的SELECT操作复用其之前生成的ReadView。
UPDATE,DELETE,INSERT对一致性读snapshot无影响。
示例:事务A,B同时操作同一行数据
若事务A的第一个SELECT在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT操作也无法读到事务B修改后的数据。
若事务A的第一个SELECT在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。
RC下的ReadView生成
特点:
每次SELECT执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView。
ReadView的组成:(当前活跃事务ID列表,称为m_ids)
最小值为up_limit_id:最先开始的事务。
最大值为low_limit_id:最后开始的事务。
ID越小,事务开始的越早;ID越大,事务开始的越迟。
若被访问版本的trx_id小于up_limit_id == > 生成该版本的事务在ReadView生成前就已提交 == > 该版本可以被当前事务访问。
若被访问版本的trx_id大于low_limit_id == > 生成该版本的事务在ReadView生成之后才提交 == > 该版本不可被当前事务访问 == > 通过Undo Log找到之前的版本重新判断。
若被访问的版本在up_limit_id和low_limit_id之间 == > 需要判断trix_id是否在m_ids中存在 == > 若存在,则生成该版本的事务还在活跃,则该版本不可访问,可由Undo Log找到之前的版本进行重新判断;若不存在,则创建ReadView时该版本对应的事务已提交,可以访问该版本。
找到记录后,还要判断delete_flag是否为true,若为true,则该记录已被删除,不返回;若为false,则记录可以返回。
注:对于ID较大的事务较ID较小的事务先提交的情况,即事务发生晚但提交的早
RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。
参考: