MVCC 多版本并发控制

103 阅读3分钟

数据库事务2.png

MVCC工作在读提交和可重复读的事务隔离级别下,是一种不加锁的并发控制机制,维护每行记录的版本链,每个事务可访问的行记录版本不同,可能读取到的不是最新的版本。

版本链

数据库中有一个自增的版本号,每创建一个新事务,都会使得该版本号自增,并赋给当前事务作为其版本号,也可理解为事务id

InnoDB 存储引擎为每行数据添加了3个隐藏字段:

  • DB_TRX_ID:最后一次更新(update / insert)该行记录的事务的版本号
  • DB_ROLL_PTR:指向版本链中上一条记录的指针
  • DB_ROW_ID:如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

image.png

快照读和当前读

  • 快照读:
    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判断是为了提高判断效率,毕竟数值判断比遍历列表要快

image.png

MVCC例子

image.png

在RR级别,对于事务10002的第二次select,并没有更新ReadView,仍然用的第一次select时创建的ReadView,对于这个ReadView,事务10003是活跃的,未提交的,所以事务10003最新更新的记录对于事务10002是不可见的,事务10002只能读取到版本链旧记录,两次读取的一样,所以解决了“不可重复读”的问题。

在RC级别,对于事务10002的第二次select,重新生成了ReadView,这个ReadView里事务10003是不活跃,已提交的,所以事务10002的第二次select可以读取到事务10003最新更新的记录,这样依然存在“不可重复读”的问题。