MVCC实现原理

MVCC多版本并发控制机制

在上面博客中介绍了可重复的事务隔离级别下,无法读到其他事务已提交的数据,这个功能就是MVCC(Multi-Version Concurrency Control)来实现的,

对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。

Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。

MVCC 的原理

MVCC主要靠两个技术来实现的 undo log 和 readview

undo log

undo日志版本链是指一行数据被多个事务多次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链(见下图)

事务号的生成

trx_id 就是事务号,每个事务开始后,遇到第一个更新,删除,新增的sql语句,会生成一个全局唯一的事务编码。这个编码是递增的。要注意的是,如果一个事务中只有查询语句,是不会生成事务号的。

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

roll_pointer 是指向本次行数据改动之前的版本标识。

现在来描述一下这个版本链是如何生成的:

account 表。字段 Id 和 name

  1. 事务80 新增一条记录 id = 1 , name = ’lilei’, undo log 中记录一条这样的行数据,同时维护了两个隐藏的列,trx_id 和 roll_pointer , trx_id 就是 80, 由于这一行是新增的,没有历史记录,所以 roll_pointer 是空的, 事务80提交。
  2. 事务300更新这行数据, 将name 更新为 ’lilei300’, undo log 同样记录这行数据,为了便于说明,假设版本记录为2 trx_id = 300, roll_pointer 的值就是 1 版本的位置。注意此时事务 300 没有提交
  3. 事务100也会更新这条数据,将 name 更新为 ’lilei1’ , undo log 添加新记录,版本记录3,trx_id= 100 , roll_pointer 指向版本2
  4. 事务100 再次更新这行记录,name = ’lilei2’ , undo log 添加新纪录,版本记录是4, trx_id =100, roll_pointer 指向版本3 , 注意,此时事务100仍然没有提交
  5. 事务200更新这条记录,name = ’lilei3’, undo log 添加新纪录,版本记录是5, trx_id = 200, roll_pointer 指向版本4
  6. 事务200更新这条记录,name = ’lilei4’, undo log 添加新纪录,版本记录是6, trx_id = 200, roll_pointer 指向版本5, 注意此时事务200 没有提交
  7. 在上图的基础上,再有一个事务300更新这条记录,name = ’lilei5’, 版本记录是7 ,trx_id = 300, roll_pointer 指向版本6, 事务300 未提交

经过上述几个步骤,此行数据的版本链已经形成了, 根据最新记录,可以找到所有历史记录

readview

假设系统中没有其他事务在运行,在步骤7这一时刻, 正在运行的事务有 100 200 300

现在有步骤8开始执行

  1. 在一个新的事务A中先执行一条查询语句 select name from account where id = 1;

执行8这个查询语句时,会先找到这行数据的最新版本,然后开始遍历历史版本,那么根据什么规则来判断应该取哪一个版本的数据呢?如下:

因为事务号在没有遇到写数据的操作时,是不会生成事务编码的。所以此时系统中正在运行的事务还是只有 100 200 300 , 以事务为单位,第一条查询开始时,会生成一个 readview 标识, 记录此时此刻所有正在运行的事务id,并生成一个有序的数组,即:100,200,300 也就是说当前系统中,最小事务是100, 最大事务是300

对于当前事务A来说,undo log 版本中, 事务x小于100的,都是已经提交的,应该可以被当前事务A看到直接取事务X数据即可。 而事务号x大于 300 的,都是将来要运行的事务,本事务A不应该看到,继续遍历

如果undo log 版本链中有一条数据的版本号x,处于 100 <= x <= 300。 这时还要分为两种情况,
如果 x 在100,200, 300 中可以找到相同的,就是说明还没有提交, 事务A 看不见事务x修改的数据继续遍历。 如果找不到相同的,说明事务x已经提交,可以看到事务X修改的数据,取 x版本。

这里可能会有一个疑问,readview 标识是在第一条查询语句开始的时候生成的。保存可所有未提交的事务,为什么会有 x 不在 其中的情况呢?因为readview 生成的时机是和事务的隔离级别有关的。

现在增加如下操作:
9. 事务200提交
10. 事务A中再次执行查询语句 select name from account where id =1;

可重复读的的级别下 ,整个事务中,只要第一条查询语句开始的时候,就生成了一个readview 标识。以后不会再改变,即使 200 这个事务已经提交,在步骤10中, readview 仍然是 100,200,300 。根据上面的规则,200 在(100,200,300) 中,版本6 ,5 属于不可见数据。这样就实现了可重复读的机制。同样版本 4, 3 , 2 也属于不可见数据,所以取版本1 , name = ’lilei'

读提交级别下,整个事务中,每条查询语句都会生成一个新的 readview 标识。在 步骤10, readview 是 (100,300) . 200 不在其中,所以版本6的数据可见, name = ’lilei4'

总结一下版本号与readview的对比规则:

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据

以上就MVCC的原理了。

回滚

知道了undo log 日志是如何生成的,那么回滚的原理也就显而易见了,删除本事务的undo log,并按顺序维护 roll_pointer 的指向k。