MVCC工作在读提交和可重复读的事务隔离级别下,是一种不加锁的并发控制机制,维护每行记录的版本链,每个事务可访问的行记录版本不同,可能读取到的不是最新的版本。
版本链
数据库中有一个自增的版本号,每创建一个新事务,都会使得该版本号自增,并赋给当前事务作为其版本号,也可理解为事务id
InnoDB 存储引擎为每行数据添加了3个隐藏字段:
- DB_TRX_ID:最后一次更新(update / insert)该行记录的事务的版本号
- DB_ROLL_PTR:指向版本链中上一条记录的指针
- DB_ROW_ID:如果没有设置主键且该表没有唯一非空索引时,
InnoDB会使用该 id 来生成聚簇索引
快照读和当前读
- 快照读:
MVCC模式下的读操作,读取到的版本链中的可见版本,普通的select语句都是快照读
select * from user where id = 1;
- 当前读:
读取的是最新版本记录,通过加锁实现并发控制
select * from user where id = 1 for update; (排他锁)
select * from user where id = 1 lock in share mode; (共享锁)
update / insert / delete (排他锁)
ReadView
ReadView用于判断版本链中哪条记录是当前快照读能够读取的。
在RC隔离级别下,每个select都会创建最新的ReadView;而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView
ReadView的几个重要属性:
- trx_ids:ReadView创建时其他未提交的活跃事务 ID 列表,不包括当前事务id
- low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
- up_limit_id:活跃事务列表 trx_ids中最小的事务 ID
- creator_trx_id:创建该 ReadView 的事务 ID
可见性判断:
首先读取版本链中最新记录的版本号db_trx_id
db_trx_id<up_limit_id:当前记录(遍历版本链时,当前遍历到的记录)版本号小于活跃事务列表中最小的事务id,表示当前记录已提交,可见db_trx_id==creator_trx_id:当前记录即为当前事务更新的,可见db_trx_id>=low_limit_id:当前记录是在当前事务之后的事务中修改的,可能未提交,不可见db_trx_id在活跃事务列表(trx_ids)中:未提交,不可见db_trx_id不在活跃事务列表(trx_ids)中:已提交,可见
先进行low_limit_id,up_limit_id判断是为了提高判断效率,毕竟数值判断比遍历列表要快
MVCC例子
在RR级别,对于事务10002的第二次select,并没有更新ReadView,仍然用的第一次select时创建的ReadView,对于这个ReadView,事务10003是活跃的,未提交的,所以事务10003最新更新的记录对于事务10002是不可见的,事务10002只能读取到版本链旧记录,两次读取的一样,所以解决了“不可重复读”的问题。
在RC级别,对于事务10002的第二次select,重新生成了ReadView,这个ReadView里事务10003是不活跃,已提交的,所以事务10002的第二次select可以读取到事务10003最新更新的记录,这样依然存在“不可重复读”的问题。