性能文章>MySQL多版本并发控制MVCC实现原理>

MySQL多版本并发控制MVCC实现原理原创

338724

承接上文MySQL事务特性ACID实现原理

MVCC(多版本并发控制)

数据库中的并发大概分几种情况:

  • 读读:不需要并发控制,因为没有改变任何值。

  • 读写:有并发安全问题,比如幻读、脏读、不可重复读。

  • 写写:有并发安全问题,可能存在数据丢失的情况。

解决并发安全最普通的方式,可以使用加锁实现,但是效率低。

  • 当前读

在数据进行读取的时候,读取的都是最新版本的数据,并且还要保证其他并发事务不能修改当前数据,需要对读取的记录进行加锁操作。

  • 快照读

读取历史版本的数据。

什么样的操作会触发当前读或快照读?

select lock in share mode加读锁、select for update加写锁、update、delete、insert这些都会触发当前读。

select有可能触发快照读,读到的不是最新数据,而是旧的数据。

结合案例说明

对这张表进行操作,

先关闭自动提交;同时开启2个事务;2边查询到的数据是一致的。

(纠错:上图heheda修改为lian)事务2做了更新操作并提交,事务1读取到的结果是更新之后的,即可以读到最新的数据。

事务2再进行一次更新操作,

此时事务1查询到的结果却不是事务2更新之后的,即没有读到最新的数据。

这里涉及到的就是当前读和快照读的问题,跟缓存没有关系,这也是MVCC存在的重要原理。

MVCC原理介绍

MySQL默认的隔离级别是可重复读。

MVCC在底层实现的时候,包含了三部分操作:

第一部分叫隐藏字段

MVCC在实际操作的时候,除了我们看到的资源之外,还会包含一些看不到的字段,重点说3个:

  • DB_TRX_ID 创建这条记录或最后一次修改该记录的事务id,在事务操作的时候事务id的值是递增的。

  • DB_ROLL_PTR 回滚指针,指向上一个数据的版本。

  • DB_ROW_ID 隐藏主键,如果没有显式主键的话,就会多一个隐藏主键。

插入一条数据,

这是第一条数据,没有历史版本,所以DB_ROLL_PTR为NULL。

这个表没有主键,所以会给一个默认的值,有主键的话就会显示对应的主键。

这就是刚开始的数据状态。

第二部分是undolog

回滚日志,记录的是数据的历史版本。

DB_ROLL_PTR指向的历史版本就在undolog中。

事务2将name更新为lisi。

最后一次修改是事务2,回滚指针指向上一个版本。

再将age修改为21,

undolog会形成一个链表,链首是最新的旧记录,链尾是旧的旧记录。

那undolog不是会一直变大吗?

一个数据不可能一直无限膨胀,有一个后台的purge线程,会清除没用的数据。

第三部分才是最主要:readview(读视图)

即事务进行快照读操作的时候产生的读视图,在当前的读视图中包含3个关键字段:

  • trx_list表示readview生成时刻当前系统活跃的事务id列表;

  • up_limit_id表示活跃列表中最小的事务id值;

  • low_limit_id系统尚未分配的下一个事务的id值。

当生成readview的时候,会把这些字段值进行填充。

当填充完成之后,再根据可见性算法来判断是否可以读取到对应的数据结果。

t3时刻事务1进行select操作的时候,能否读取事务2 t2时刻修改之后的结果?

在进行快照读操作的时候,会产生读视图,所谓的快照读就是select操作。

快照读的时候,填充好对应的字段信息,

当前系统活跃的事务是1和3,事务2已经提交了。

当前活跃列表中最小的事务id是1,系统尚未分配的下一个事务id是4。

DB_TRX_ID 创建这条记录或者最后一次修改该记录的事务id是2,因为事务2修改的,所以DB_TRX_ID为2。

可见性算法的判断过程

根据可见性算法进行判断,首先比较DB_TRX_ID(2)大于up_limit_id(活跃列表中最小的事务id值1),进入下一个判断,如果小于则当前事务能看到所在的记录。

如果大于等于当前出现过的最大的事务id(4)则表示DB_TRX_ID=2所在的记录在read view生成后出现的,那么对于当前事务肯定不可见,如果小于则进入下一个判断。

DB_TRX_ID=2是否在活跃事务(1,3)中,如果不在,说明在readview生成之前就已经commit了,那么修改的结果是能够看到的。

所以t3时刻进行select操作的时候,能读取t2时刻修改之后的结果。

每次在进行快照读的时候,会生成readview。

若t2、t4时刻共有2次快照读,来把对应的readview数据写完整,

t2时刻当前系统活跃列表中的id是1、2、3,同一个列表中最小的id是1,尚未分配的下一个事务id是4。

新增或最近修改这条记录的事务id是多少?

因为没有新增操作,但因为事务id的值是递增的,一定是小于1的,假设为0,反正小于1就行了。

t4时刻对应的read view:事务2提交了,最小事务id值为1,尚未分配的下一个事务id是4。

t3时刻事务2提交了事务,所以t4时刻最后修改的事务id是2。

绿色部分的值跟

这里的readview是一样的。

readview数据值是一样的,可见性算法是一样的,但是结果却是不同的(即事务1在t3时刻可以读取到最新的数据,在t4时刻却读不到最新数据),

所以要考虑下,在整个过程里面,哪里可能会发生变化?

读取时刻是不同的。

根据现象进行大胆猜测:第二次readview并没有重新生成,而是用的之前的readview。

接下来验证猜测是否正确:

最终状态的readview根据可见性算法判断,DB_TRX_ID=2 大于 up_limit_id=1,进入下一个判断,DB_TRX_ID=2 小于 low_limit_id=4,进入下一个判断,验证DB_TRX_ID=2是否在活跃事务中(此时在的),如果在,则代表在readview生成的时刻,这个事物还是活跃状态,还没有commit,当前事务中是看不到修改的数据。

验证完可见性算法之后,跟上述的结论是可以对上的,所以在第二次快照读的时候,确实是用的第一次生成的readview,没有重新生成新的。

小结

在RC隔离级别里,每次进行快照读操作的时候 ,都会重新生成新的readview,所以每次可以查询的最新的结果集的记录;

在RR隔离级别里,只有当前事务在第一次进行快照读的时候才会生成readview,之后进行的快照读操作都会沿用之前的readview;

所以这也是为什么在不同的隔离级别里面看到的效果是不一样的原因。

承接上文

幻读现象

因为在RR(可重复读)隔离级别里,事务1的第二次查询没有生成新的readview,而是用的第一次查询时生成的readview,所以第二次查询返回2条数据,而不是3条数据。

事务1第二次查询返回2条数据,但更新的时候,却有3条数据被更新,这就是幻读现象。

select 执行的是快照读(某个版本数据的Read View),而update 执行的是当前读(最新的数据,即最新的Read View,因此更新了三条数据)。

幻读问题产生的本质原因是:如果事物中操作的都是快照读,那么是不会产生幻读问题的。

但是当快照读和当前读一起使用的时候才会产生幻读问题,因为执行了一个update操作,即用了当前读,此时读取的数据是不一致的,就产生了幻读的问题。

唤读问题的解决

在查询的时候通过加锁来解决幻读问题,

select for update,事务1不提交,事务2的插入操作会一直阻塞。

加锁是为了解决幻读问题,并不是说实际操作中一定要加锁,加不加锁取决于实际需求。

一般情况下select * from ....where ...是快照读,不会加锁。

而 for update,lock in share mode,update,delete都属于当前读。

当前读和快照读跟隔离级别没有关系。

锁是加在索引上的。

for update是排他锁,lock share mode叫共享锁,那什么叫间隙锁?

1是记录锁或行锁,(1,3)是间隙锁,(1,3]是临键锁(左开右闭)。

因此可以发现,MVCC + 锁共同实现了隔离级别。

 

点赞收藏
分类:标签:
平凡人笔记

公众号:平凡人笔记

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

日常Bug排查-偶发性读数据不一致

日常Bug排查-偶发性读数据不一致

4
2