转载自:https://blog.csdn.net/huyuyang6688/article/details/123028254
概述
MVCC 全称 Mutil-Version Concurrency Control,多版本并发控制,是一种并发控制方法,旨在减少读写操作的冲突
我们知道,当有多个事务同时操作数据库的相同数据时,会出现并发问题,例如,读 + 写事务并发可能会导致脏读、幻读和不可重复读等问题,写+写事务并发可能会导致数据覆写等问题
为了解决读 + 写事务并发可能导致的问题,MysqL 的 innodb 引擎实现了 MVCC,做到不用加锁也可以实现安全的非阻塞的并发读 + 写,而对于写 + 写事务并发则只能通过加锁解决
当前读 + 快照读
当前读:当前读会对读取的记录加锁,保证读取数据是最新版本,比如:select …… lock in share mode(共享锁)
,select …… for update | update | insert | delete(排他锁)
快照读:每次修改数据都会在 undo log 记录原来的数据(保留快照),快照读就是读取 undo log 的某一版本的快照,读取数据可能不是最新版本,比如:select * from t_user where id=1
MVCC 实现原理
1. 隐藏字段
- row_id:当表没定义主键时,InnoDB 会以 row_id 为主键生成一个聚集索引
- trx_id:记录了新增/最近修改这条记录的事务 id,事务 id 是自增的
- roll_pointer:回滚指针指向当前记录的上一个版本(在 undo log 中)
2. 版本链
在修改数据时,会向 undo log 记录数据原来的快照,除了用于回滚事务,还用于实现 MVCC
用一个简单的例子来画一下MVCC 用到的 undo log 版本链的逻辑图:
当事务(trx_id = 100)执行了 insert into t_user values(1,'张三',20)
当事务(trx_id=102)执行了 update t_user set name='李四' where id=1
当事务(trx_id=103)执行了 update t_user set name='王五' where id=1
3. ReadView
在上面的例子中,多个事务对 id=1 的数据修改后,这行记录除了最新的数据,在 undo log 中还有多个版本的快照。那其他事务查询时能查到最新版本的数据吗?如果不能,能读到哪个版本的快照呢?这就要由 ReadView 来决定了
在对数据进行快照读时,会产生的一个 ReadView,ReadView 有四个比较重要的变量:
- 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 有一套算法:
- 当【版本链中记录的 trx_id 等于当前事务 id(trx_id = creator_trx_id)】时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见
- 当【版本链中记录的 trx_id 小于活跃事务的最小 id(trx_id < min_trx_id)】时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见
- 当【版本链中记录的 trx_id 大于下一个要分配的事务 id(trx_id > max_trx_id)】时,该快照记录对当前事务不可见
- 当【版本链中记录的 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
,在版本链的快照中,从最新的一条记录开始,依次判断这四个条件,直到某一版本的快照对当前事务可见,否则继续比较上一个版本的记录
MVCC 主要是用来解决 RC 隔离级别下的脏读和 RR 隔离级别下的不可重复读的问题,所以 MVCC 只在 RC(解决脏读)和 RR(解决不可重复读)隔离级别下生效,也就是 MysqL 只会在 RC 和 RR 隔离级别下的快照读时才会生成 ReadView。区别就是,在 RC 隔离级别下,每一次快照读都会生成一个最新的 ReadView,在 RR 隔离级别下,只有事务中第一次快照读会生成 ReadView,之后的快照读都使用第一次生成的 ReadView
手动验证 MVCC 原理
前提条件:事务(trx_id=100)向表中插入一条的数据并提交了事务:insert into t_user values(1,20)
之后又有三个事务(事务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:
1. 事务隔离级别为 RC(读已提交)
当前事务隔离级别为 RC(读已提交)时,每个事务每次查询对应生成的 ReadView 是这样的,跟着这张图来梳理一下:
在时间点 t2,事务 101 查询时生成的 ReadView 内容为:
trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101
当前时间点,版本链中只有一个快照(trx_id = 100),因为 trx_id(100) < min_trx_id(101),符合算法的第(2)条规则,所以 trx_id = 100 的这个快照对当前事务可见
在时间点 t4,事务 102 查询时生成的 ReadView 内容为:
trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102
当前时间点,版本链中只有一个快照(trx_id = 100),因为 trx_id(100) < min_trx_id(101),符合算法的第(2)条规则,所以 trx_id=100 的这个快照对当前事务可见
在时间点 t6,事务 103 查询时生成的 ReadView 内容为:
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103
当前时间点,版本链中只有一个快照(trx_id = 100),因为 trx_id(100) < min_trx_id(101),符合算法的第(2)条规则,所以 trx_id=100 的这个快照对当前事务可见
在时间点 t8,事务 101 查询时生成的 ReadView 内容为:
trx_list: 101,103
min_trx_id:101
max_trx_id:104
creator_trx_id:101
当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:
对于 trx_id=102 的快照,因为 trx_id(102) = creator_trx_id(102),符合算法的第(1)条规则,所以 trx_id=102 的这个快照对当前事务可见
在时间点 t11,事务 103 查询时生成的 ReadView 内容为:
trx_list: 101,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103
当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:
对于 trx_id=102 的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且 trx_id(102) 不在 trx_list(101,103) 中,说明当前事务生成 ReadView 时,修改该记录的事务不是活跃事务(已经提交),根据算法的第(4)条规则,trx_id = 102 的快照对当前事务可见。这也就验证了在 RC 隔离级别下,事务 102 修改且提交的数据对于事务 103 是可见的
在时间点 t14,事务 101 查询时生成的 ReadView 内容为:
trx_list: 101
min_trx_id:101
max_trx_id:104
creator_trx_id:101
当前时间点,版本链中有三个快照(trx_id=103 -> trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:
对于 trx_id = 103 的快照,min_trx_id(101) <= trx_id(103) < max_trx_id(104) ,且 trx_id(103) 不在 trx_list(101) 中,说明当前事务生成 ReadView 时,修改该记录的事务不是活跃事务(已经提交),根据算法的第(4)条规则,trx_id = 103 的快照对当前事务可见。这也就验证了在 RC 隔离级别下,事务 103 修改且提交的数据对于事务 101 是可见的
2. 事务隔离级别为 RR(可重复读)
当前事务隔离级别为 RR(可重复读)时,每个事务每次查询对应生成的 ReadView 是这样的,跟着这张图来梳理一下:
上面说过,在 RC 隔离级别下,每一次快照读都会生成一个最新的 ReadView;在 RR 隔离级别下,只有事务中第一次快照读会生成 ReadView,之后的快照读都使用第一次生成的 ReadView
所以,事务 101 在 t8、t14 时刻查询时,使用的 ReadView 跟 t2 时刻一样;事务 102 在 t9 时刻查询时使用的ReadView 跟 t4 时刻一样;事务103 在 t11 时刻查询时使用的 ReadView 跟 t6 时刻一样
原文链接:https://www.cnblogs.com/Yee-Q/p/17989786