大白话解析 MySQL 中的 MCCC 机制
你好,我是悟空呀。
说下 MySQL 中的 MVCC 机制?
MySQL 中有四种隔离级别,Read Repeatable (RR)级别可以防止脏读、不可重复读、幻读问题。Read Committed (RC)级别解决了脏读问题。
那它是怎么做到的呢?就是利用了 MVCC 多版本控制机制。而且可以实现 读-写,写-读不冲突。
本解答尽量通俗易懂:
多版本
就是有多个某行记录更新后的版本,然后将这些版本从上到下串起来。有点像串糖葫芦,这个就是版本链。
比如说银行转账记录,将多次对账户的修改都串起来了。记录里面有是哪个事务做的转账记录,最后值等于多少。
(1)账户 A = 初始值 200 元,事务 id = 40 ->
(2)账户 A = 初始值 200 元 + 100 元 = 300 元,事务 id = 51 ->
(3)账户 A = 300 元 + 50 元 = 350 元,事务 id = 59 ->
(4)账户 A = 350 元 - 30 元 = 320 元,事务 id = 72
在 MySQL 就是利用 undo log 日志将这些串起来的。
如下图所示,undolog 的版本串起来长这样:
\
控制
用自身的事务 id 和其他地方存的事务 id 进行比较,看是否符合读取版本链上的条件,如果符合,读取后就返回了。怎么控制的呢?利用 ReadView。ReadView 其实也不难理解,就是对当前活跃事务的一个统计。然后 MySQL 利用这个数据统计 + 版本链上的事务 id 来进行比较,获得某个可读到的版本。
ReadView
保证你只能读到事务开启之前,别的事务提交的值,或者自己提交的值。其他情况下无法读取到其他事务提交的值,避免了脏读。
ReadView 生成时机?
每个事务执行查询时都会生成自己事务的 ReadView。RC 级别是每次查询都会重新生成一份,RR 级别是事务中的 ReadView 都不变。
ReadView 里面有四个重要的属性:
m_ids 事务列表:有哪些事务在 MySQL 里执行还没提交的;min_trx_id 最小事务 id:m_ids 列表中最小的值 ;max_trx_id 最大事务 id:下一个要生成的事务 id,就是最大事务 id;creator_trx_id:当前事务的 id。
这四个属性怎么用的呢?
比如说事务 A 用来查询,事务 B 用来更新,它俩都开启了事务,也都还没有提交,对应的事务 id 分别为 51 和 59,那么 ReadView 就长这样:
\
\
活跃事务列表就是 [51,59]。最小事务 id = 51。最大事务 id = 59+1 = 60。当前事务 id = 51。
事务 A 首先拿着这几个属性值,到版本链上一个一个比较版本上的事务 id,符合条件就返回。比较又分三种情况:
1、如果版本上的事务 id < 最小值 51,说明这个行记录在这些活跃的事务创建前就已经提交了,这个行记录的版本对于当前事务 A 是可见的,就返回了。
2、如果版本上的事务 id >= 最大值 60,则说明提交的事务是 ReadView 生成之后创建的,这个版本也是不可读的,就接着往下找。
3、如果版本上的事务 id 在在最小和最大值之间,就进行下一步判断:
3.1、如果在这个列表 [51,59] 里面,说明提交的事务是和 A 事务差不多时间开启的事务,被 ReadView 记录在列表里面了。这种事务提交的版本也是不可读的,就接着往下找。(避免了脏读)
3.2、如果不在这个列表 [51,59] 里面,说明事务已提交了,是可以读取的,读到了就返回。
RC 的读已提交怎么做到的?
我们说 RC 隔离级别下,事务 A 下次查询时,就可以读到其他事务提交的数据了(读已提交),但是根据上面的 3.1 的情况来看,事务 A 是读取不到事务 B 提交的呀?
这就需要在 A 查询时重新生成一个 ReadView 了,来看下重新生成的长啥样:
活跃事务列表就是 [51]。
最小事务 id = 51。
最大事务 id = 60。注意:这是 MySQL 下一个要生成的事务 id,不是指活跃事务中的最大事务 id。
当前事务 id = 51。
看到了吗?
事务 B 的 事务 id 59 不在活跃事务列表啦!但是又是小于最大事务 id 60 的。这就符合 3.2 的情况啦,可以读到这个版本了。
那下次 事务 A 再次查询时,又会生成一个 ReadView,可以读到其他事务提交的数据,这个数据和上次的数据很有可能不一样,也就是说不能保证每次读到的数据一样的,这就是不可重复读。RR 的可重复读怎么做到的?
它和 RC 不同的地方在于,事务 A 查询时,是不会重新生成 ReadView 的,也就是说 B 提交的事务读取不到的,那就顺着版本链继续找呗。找着找着就只能读事务 A 自己提交的,或者事务开启之前,其他事务提交的,那么事务 A 每次查询都是读到一样的数据啦,但是读取的都不是最新的数据,这就是可重复读,避免了读取数据不一致的情况。
注意:不管其他事务怎么修改数据,事务 A 生成的 ReadView 是不会改变的,基于这个 ReadView 看到的值都是一样的!
RR 的幻读是怎么避免的?
比如 A 执行范围查询:select * from table where age > 10,查到了一条数据 X。然后事务 C 72 插入了一条数据,事务 A 再次查询时,可以查到两条数据 X 和 Y。但是 Y 的版本链上事务 id 等于 72,大于最大事务 id 60,说明是事务 A 发起查询后,当然是不可读到的了,所以事务 A 还是只能读到数据 X。
小结
通过版本链 + ReadView 做到了这些事情:避免了 RR 隔离级别下的脏读、不可重复度、幻读问题。避免了 RC 隔离级别下的脏读问题,实现了读取已提交数据的功能。
作者简介:悟空,8 年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM 性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构
。本文已收录至 www.passjava.cn
版权声明: 本文为 InfoQ 作者【悟空聊架构】的原创文章。
原文链接:【http://xie.infoq.cn/article/80198f1804fe9017dcafbaa82】。未经作者许可,禁止转载。
评论