MySQL MVCC 详解

转载自: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. 隐藏字段

MySQL 每一行记录除了自定义字段,还有一些隐藏字段:

  • 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 有一套算法:

  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,在版本链的快照中,从最新的一条记录开始,依次判断这四个条件,直到某一版本的快照对当前事务可见,否则继续比较上一个版本的记录

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,102,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 时刻一样

posted @ 2024-01-31 16:35  低吟不作语  阅读(18)  评论(0编辑  收藏  举报