MVCC(Mutil Version Concurrency Control)多版本并发控制,是一种并发控制的方法(而非具体实现),一般在数据库管理系统中,实现对数据库的并发访问。
上面的解释比较抽象,下面来一点一点分析。
多个事务同时访问数据库中相同的数据,也是一样的,可能存在【读】、【读+写】、【写】这三种情况。
那如何解决上面的问题呢?
到了这里,我们脑子里需要有这么个印象:MySQL实现的MVCC,主要是用于在并发读写的情况下,保证 “读” 数据时无需加锁也可以读取到数据的某一个版本的快照,好处是可以避免加锁,降低开销,解决了读写冲突,增大了数据库的并发性能。
在进一步了解MySQL中实现MVCC的细节之前,还需要了解两个定义:
select …… lock in share mode(共享锁)
、select …… for update | update | insert | delete(排他锁)
select * from t_user where id=1;
在MVCC中的查询都是快照度。
MySQL中MVCC主要是通过行记录中的隐藏字段(隐藏主键 row_id、事务ID trx_id、回滚指针 roll_pointer)、undo log(版本链)、ReadView(一致性读视图)来实现的。
MySQL中,在每一行记录中除了自定义的字段,还有一些隐藏字段:
row_id
:当数据库表没定义主键时,InnoDB会以row_id为主键生成一个聚集索引。
trx_id
:事务ID记录了新增/最近修改这条记录的事务id,事务id是自增的。
roll_pointer
:回滚指针指向当前记录的上一个版本(在 undo log 中)。
简单提下 redo log 和 undo log。在修改数据的时候,会向 redo log 中记录修改的页内容(为了在数据库宕机重启后恢复对数据库的操作),也会向 undo log 记录数据原来的快照(用于回滚事务)。undo log有两个作用,除了用于回滚事务,还用于实现MVCC。
用一个简单的例子来画一下MVCC中用到的undo log版本链的逻辑图:
当事务100(trx_id=100)执行了 insert into t_user values(1,'张三',20);
之后:
当事务102(trx_id=102)执行了 update t_user set name='李四' where id=1;
之后:
当事务103(trx_id=103)执行了 update t_user set name='王五' where id=1;
之后:
在上面的例子中,多个事务对 id=1 的数据修改后,这行记录除了最新的数据,在 undo log 中还有多个版本的快照。那其他事务查询时能查到最新版本的数据吗?如果不能,能读到哪个版本的快照呢?这就要由ReadView来决定了。
ReadView 就是MVCC在对数据进行快照读时,会产生的一个”读视图“(翻译过来就是ReadView~哈哈哈)。
ReadView中有4个比较重要的变量(具体这几个变量名是啥我也不知道,不过不要在意这些细节,这里就随便定义一下……):
m_ids
:活跃事务id列表,当前系统中所有活跃的(也就是没提交的)事务的事务id列表。
min_trx_id
:m_ids 中最小的事务id。
max_trx_id
:生成 ReadView 时,系统应该分配给下一个事务的id(注意不是 m_ids 中最大的事务id),也就是m_ids 中的最大事务id + 1 。
creator_trx_id
:生成该 ReadView 的事务的事务id。
某个事务进行快照读时可以读到哪个版本的数据,ReadView 有一套算法:
(1)当【版本链中记录的 trx_id 等于当前事务id(trx_id = creator_trx_id)】时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见。
(2)当【版本链中记录的 trx_id 小于活跃事务的最小id(trx_id < min_trx_id)】时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见。
(3)当【版本链中记录的 trx_id 大于下一个要分配的事务id(trx_id > max_trx_id)】时,该快照记录对当前事务不可见。
(4)当【版本链中记录的 trx_id 大于等于最小活跃事务id】且【版本链中记录的trx_id小于下一个要分配的事务id】(min_trx_id<= trx_id < max_trx_id)时,如果版本链中记录的 trx_id 在活跃事务id列表 m_ids 中,说明生成 ReadView 时,修改记录的事务还没提交,所以该快照记录对当前事务不可见;否则该快照记录对当前事务可见。
当事务对 id=1 的记录进行快照读时select * from t_user where id=1
,在版本链的快照中,从最新的一条记录开始,依次判断这4个条件,直到某一版本的快照对当前事务可见,否则继续比较上一个版本的记录。
MVCC主要是用来解决RU隔离级别下的脏读和RC隔离级别下的不可重复读的问题,所以MVCC只在RC(解决脏读)和RR(解决不可重复读)隔离级别下生效,也就是MySQL只会在RC和RR隔离级别下的快照读时才会生成ReadView。区别就是,在RC隔离级别下,每一次快照读都会生成一个最新的ReadView;在RR隔离级别下,只有事务中第一次快照读会生成ReadView,之后的快照读都使用第一次生成的ReadView。
事务能否查询到其他事务修改的数据,取决于可见性算法,可见性算法又是取决于 ReadView 中的值,ReadView 在 RC 和 RR 两种隔离级别下生成的时机不同,所以导致两种隔离级别下,某个事务修改数据的可见性对其他事务是不同的(RC隔离级别下一个事务可以查询到其他事务在此期间修改并提交的数据;RR隔离级别下一个事务无法查询到其他事务在此期间修改并提交的数据)。
还是有点抽象?那就手动来亲自验证一下,之后就会清晰很多。(如果想要真正理解上面的算法,建议最好找个例子,亲自验证一下)
还是用上面的例子来说。
前提条件:事务100(trx_id=100)向表中插入了一条id=1的数据: insert into t_user values(1,'张三',20);
并提交了事务。
之后又有3个事务(事务101、事务102、事务103)来对这条数据进行读写操作:
时间顺序 | 事务101 | 事务 102 | 事务 103 |
---|---|---|---|
t1 | begin | ||
t2 | select * from t_user where id=1; | ||
t3 | begin | ||
t4 | select * from t_user where id=1; | ||
t5 | begin | ||
t6 | select * from t_user where id=1; | ||
t7 | update t_user set name=‘李四’ where id=1; | ||
t8 | select * from t_user where id=1; | ||
t9 | select * from t_user where id=1; | ||
t10 | commit | ||
t11 | select * from t_user where id=1; | ||
t12 | update t_user set name=‘王五’ where id=1; | ||
t13 | commit | ||
t14 | select * from t_user where id=1; |
在时间点 t1 ~ t6 时,整个版本链中只有一个快照,trx_id 为 100:
在时间点 t7 ~ t11 时,整个版本链中有两个快照,trx_id 为 102、100:
在时间点 t11 ~ t14 时,整个版本链中有三个快照,trx_id 为 103、102、100:
当前事务隔离级别为RC(读已提交隔)时,每个事务每次查询对应生成的ReadView是这样的,跟着这张图来梳理一下:
当前事务隔离级别为RR(可重复读)时,每个事务每次查询对应生成的ReadView是这样的,跟着这张图来梳理一下:
上面说过,在RC隔离级别下,每一次快照读都会生成一个最新的ReadView;在RR隔离级别下,只有事务中第一次快照读会生成ReadView,之后的快照读都使用第一次生成的ReadView。
所以,事务101在 t8、t14 时刻查询时,使用的 ReadView 跟 t2 时刻一样;事务102在t9时刻查询时使用的ReadView 跟 t4 时刻一样;事务103在 t11 时刻查询时使用的ReadView 跟 t6 时刻一样。
文章到这里就结束了,有心的同学可以跟着上面【事务隔离级别为RC】时的步骤,来推演验证一下在每个时间点、每个事务查询都能查到哪个版本的快照数据,也能加深一下理解(为了有些同学推演后想对比答案,我就把答案也写在下面了)。
转载请注明出处——胡玉洋 《深入理解MySQL的MVCC原理》