MySQL之MVCC的实现

209 阅读8分钟

事务

特性

一致性可以理解为我们使用事务的目的,而隔离性、原子性、持久性均是为了保障一致性的手段,保证一致性需要由应用程序代码来保证。

原子性

原子性指的是:当前事务的操作要么同时成功,要么同时失败。原子性由redo log日志(执行成功)、undo log日志(失败回滚)来保证。

隔离性

在事务并发执行时,他们内部的操作不能互相干扰。如果多个事务可以同时操作一个数据,那么就会产生脏读、重复读、幻读的问题,可以设置数据库隔离级别来解决,分别有read uncommit(读未提交),read commit (读已提交),repeatable read(可重复复读),serializable(串行)。MySQL通过锁和MVCC机制来保证隔离性。

一致性

事务的一致性是指事务执行之前和执行之后,数据始终处于一致的状态。

持久性

一旦提交了事务,它对数据库的改变就应该是永久性的。说白了就是,会将数据持久化在硬盘上。持久性由redo log 日志来保证。

并发情况下一致性问题

数据库一般会并发执行多个事务,而多个事务可能会并发对相同的数据进行增加、删除、修改和查询操作,进而导致并发事务问题,影响数据的一致性。并发事务带来的问题包含更新丢失(脏写)、脏读、不可重复读、幻读。

更新丢失(脏写)

当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖。本质上是写操作的冲突,解决办法是让没个事务按照串行方式执行,按照一定的顺序执行写操作。

脏读

如果一个事务读取到了另一个未提交事务修改过的数据,我们就称发生了脏读现象。

不可重复读

在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并已提交的数据。

幻读

在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,数据条目发生了变化。

事务隔离级别

事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,不同的事务之间不该互相影响,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。

隔离级别名称会引发的问题数据库默认隔离级别
read uncommitted读未提交脏读、不可重复读、幻读
read committed读已提交不可重复读、幻读Oracle / SQL Server
repeatable read可重复读幻读MySQL
serializable串行化无(因为写会加写锁,读会加读锁)

InnoDB 对 MVCC 的实现

MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

隐藏字段

行格式会默认添加以下三个字段

DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。

DB_ROLL_PTR(7字节) 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空

DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

undo-log

undo log 主要有两个作用:

当事务回滚时用于将数据恢复到修改前的样子。

另一个作用是 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读。

ReadView

主要包含4个比较重要的属性

m_ids: 在生成ReadView时,当前系统活跃的事务id列表。

min_trx_id: 在生成ReadView时,当前系统活跃的事务中最小的事务id,也就是m_ids中最小值。

max_trx_id: 在生成ReadView时,系统应该分配给下一个事务的id值。

creator_trx_id: 生成该ReadView的事务id。

可见性算法

**工作流程:**将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录。

1、db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据可以被当前事务访问。

2、db_trx_id < min_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,该版本可以被当前事务访问。

3、db_trx_id >= max_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,该版本不能被当前事务访问。

4、min_limit_id <= db_trx_id < max_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中

​ 在列表中,说明该版本对应的事务正在运行,数据不能显示(不能读到未提交的数据

​ 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(可以读到已经提交的数据

案例

MVCC例子.png

在 RC 下 ReadView 生成情况

假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为

img

由于 RC 级别下每次查询都会生成Read View ,并且事务 101、102 并未提交,此时 103 事务生成的 Read View 中活跃的事务 m_ids 为:[101,102]m_low_limit_id为:104,m_up_limit_id为:101,m_creator_trx_id 为:103

  • 此时最新记录的 DB_TRX_ID 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是 101,不可见
  • 继续找上一条 DB_TRX_ID为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 name = 菜花

时间线来到 T6 ,数据的版本链为

markdown

因为在 RC 级别下,重新生成 Read View,这时事务 101 已经提交,102 并未提交,所以此时 Read View 中活跃的事务 m_ids:[102]m_low_limit_id为:104,m_up_limit_id为:102,m_creator_trx_id为:103

  • 此时最新记录的 DB_TRX_ID 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 为 101,满足 101 < m_up_limit_id,记录可见,所以在 T6 时间点查询到数据为 name = 李四,与时间 T4 查询到的结果不一致,不可重复读!

时间线来到 T9 ,数据的版本链为

markdown

重新生成 Read View, 这时事务 101 和 102 都已经提交,所以 m_ids 为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务 ID 为 102,满足 102 < m_low_limit_id,可见,查询结果为 name = 赵六

在 RR 下 ReadView 生成情况

在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表)

在 T4 情况下的版本链为

markdown

在当前执行 select 语句时生成一个 Read View,此时 m_ids:[101,102]m_low_limit_id为:104,m_up_limit_id为:101,m_creator_trx_id 为:103

此时和 RC 级别下一样:

  • 最新记录的 DB_TRX_ID 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是 101,不可见
  • 继续找上一条 DB_TRX_ID为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 name = 菜花

时间点 T6 情况下

markdown

在 RR 级别下只会生成一次Read View,所以此时依然沿用 m_ids :[101,102]m_low_limit_id为:104,m_up_limit_id为:101,m_creator_trx_id 为:103

  • 最新记录的 DB_TRX_ID 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 为 101,不可见
  • 继续根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是 101,不可见
  • 继续找上一条 DB_TRX_ID为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 name = 菜花

时间点 T9 情况下:

markdown

此时情况跟 T6 完全一样,由于已经生成了 Read View,此时依然沿用 m_ids :[101,102] ,所以查询结果依然是 name = 菜花

参考资料

book.douban.com/subject/352… MySQL 是怎样运行的:从根儿上理解 MySQL

book.douban.com/subject/356… 深入理解分布式事务

juejin.cn/post/701648… 3y对线面试官系类

github.com/Snailclimb/… InnoDB存储引擎对MVCC的实现