在谈及mvcc之前 我们需要了解以下知识
- 锁
- 共享锁【S锁】 又称读锁,若事务T对数据对象A加上S锁,则其它事务可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。(表级锁)
- 排他锁【X锁】又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。(行级锁)
- 快照读(Read View)、当前读
- 普通的select语句就是快照读 (不加任何锁)
- 使用了以下语句,则称为当前读
select lock in share mode(共享锁)
select for update(排他锁)
update(排他锁)
insert(排他锁)
delete(排他锁)
- undo log 日志
- 数据库事务开始之前,会将要修改的记录放到undo日志里,当事务回滚时或者数据库崩溃时,可以利用UndoLog撤销未提交事务对数据库产生的影响。
- 事务未提交之前,undolog保存的是未提交之前的版本数据,undolog中的数据可作为数据旧版本快照供其他并发事务进行快照读。
mvcc的实现原理
- 是基于undo log、版本链、readview实现,如下图所示
- trx_id:事务id,每进行一次事务操作,就会自增1
- roll_pointer:回滚指针,用于找到上一个版本的数据
readview结构
- 当我们select读取数据的时候,会生成一个 readview (RC隔离级别下,每一次生成一个, RR隔离级别下,事务开始的时候生成一个)
- 字段说明
- m_ids:当前系统中活跃事务的id列表,也就是未提交的事务
- min_trx_id:活跃事务中事务id最小的事务
- max_trx_id:生成此快照时预分配给下一事务的id,即trx_id+1
- creator_trx_id:生成当前快照时的事务id
select时如何选取数据版本
- 总共分为以下4步,如果符合,则在版本链中读取,否则继续查找 图示说明
-
举个栗子
- 并发情况下、并且设置为RC隔离级别
- 版本链如下
- 此时我们根据版本链的数据访问规则可以看出在第一个readview时我们select时姓名等于张三这条记录是我们能够读取的,同理第二个readview通过读取规则可以得出张小三是我们能够读取的
- 并发情况下,设置隔离级别为RR,此时其它的步骤都是相似的,不同的是在RR隔离级别下,仅在第一次执行快照读的时候生成readview,后续的快照读将复用此readview
- 此时如图示所示 只不过第一次和第二次生成的readview相同,因此读到的数据也是相同的
-
总结
- 我们可以发现mvcc通过类似于版本控制而不加锁的方式避免了脏读、不可重复读、幻读,提高并发性能,但是我们发现它并不能完全阻止幻读的问题发生
- 这是因为 在同一个事务中如果多次select(快照读)可以复用readview,但是在当前事务中如果出现了当前读,那么readview会重新生成,因此此时mvcc就无能为力了,因此想要完全解决幻读,mysql就引出了 Next-Key Locks 的概念,将在下一节来说明 Next-Key Locks