MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)

专栏持续更新中:MySQL详解

一、MVCC概念

MVCC是多版本并发控制(Multi-Version Concurrency Control),是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现已提交读和可重复读隔离级别,也经常称为多版本数据库。MVCC机制会生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本(系统版本号和事务版本号)

  • 快照读(非锁定读):读的是记录的可见版本,不用加锁。如 select做的都是快照读,会把已经commit的数据(即整表数据)生成一个快照(这就可以防止不可重复读)
  • 当前读:读取的是记录的最新版本,返回当前读的记录,并且对数据加锁。如 insert,delete,update,select...lock in share mode/for update这些操作,都是读的是最新的数据

MVCC:每一行记录实际上有多个版本,每个版本的记录除了数据本身之外,增加了其它字段(DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR)

已提交读隔离级别:每个语句执行前都会重新生成一个 Read View,快照中只包含已commit的数据 可重复读隔离级别:启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的

什么叫事务启动呢?

  • 执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机
  • 执行了 start transaction with consistent snapshot 命令,就会马上启动事务

快照内容读取原则:

  1. 版本未commit,无法读取生成快照
  2. 版本已commit,但是在快照创建后提交的,无法读取
  3. 版本已commit,但是在快照创建前提交的,可以读取
  4. 当前事务做的修改,是需要重新生成快照的。读取的是最新版本,并且对数据加锁,阻塞其他操作事务修改记录。核心逻辑就是判断版本链中的哪个版本是当前事务可见可处理的

"数据快照"中并不是数据,存储的是一些事务id

Read View 有四个重要的字段:

  • creator_trx_id :指的是创建该 Read View 的事务的事务 id
  • m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,"活跃事务"指的就是,启动了但还没提交的事务。重新生成数据快照m_ids可能会有更新,不重新生成数据快照m_ids就不会更新
  • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值
  • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值 ,也就是所有已提交的和未提交的事务中最大的事务 id 值 + 1

Innodb如何判断某条记录是否对当前事务可见呢?一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值 ,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值 ,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_id 在 m_ids 列表中 ,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
    • 如果记录的 trx_id 不在 m_ids列表中 ,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)

在已提交读隔离级别下,每次查询都会重新生成数据快照,若其他事务已经提交了,当前事务再次查询时重新生成的数据快照中的m_ids、min_trx_id、max_trx_id可能会发生改变,这样对比每条记录的trx_id后,可见性就会发生改变

在可重复读隔离级别下,每次查询都使用第一次生成的数据快照

二、MVCC应用于已提交读隔离级别

1. 解决脏读

先设置隔离级别为已提交读并开启事务,已提交读解决了脏读,未解决可重复读和幻读

这样通过快照读,MVCC就解决了脏读

不管是已提交读还是可重复读,只要我们select的时候,就会产生一个数据快照,相当于给当前的数据拍个照片,以后去查询,都是查询快照上的数据(除非有新的数据被commit)。已提交读隔离级别采用非锁定读,非锁定读是在快照上的读取。

在已提交读隔离级别,每一次select都会产生一个新的数据快照 ,当事务1进行更改的时候,事务2又去select,重新产生数据快照(有可能和前面的快照相同),然而产生新的数据快照的前提是新的数据已经被事务正确commit,prepare状态的数据不会出现在快照中

数据有2种状态:prepare(未提交时)和commit(已提交)

事务2第二次select的时候,由于事务1并没有commit新的数据(数据处于prepare状态),当又一次产生数据快照时,产生的数据快照还是undo log回滚日志的链表指向的旧数据,这就解决了脏读问题

然而,在已提交读隔离级别依然会发生不可重复读的现象(两次查询,得到的数据内容不一样,属于正确读取的范围)

2. 无法解决不可重复读

因为每一次select都会重新产生1次数据快照,其他事务update后commit,新的数据已经符合生成快照的要求了,于是再次select的时候新commit的数据也会出现在新生成的快照中,发生了不可重复读

3. 无法解决幻读

和出现不可重复读现象的原因相同,由于新commit的数据符合生成快照的要求,再次select的时候新commit的数据也会出现在新生成的快照中,自然就出现了幻读

三、MVCC应用于可重复读隔离级别

1. 解决脏读

事务第一次select就产生数据快照,而且只产生这一次快照,select时都是直接用老的数据快照,所以可以解决脏读

2. 解决不可重复读

因为事务第一次select就产生数据快照,而且只产生这一次快照

设置可重复读隔离级别,并2个开启事务

事务2 select,生成数据快照,在可重复读隔离级别下,以后再select都不会再生成快照

生成的快照如下:

事务1进行update,然后commit

我们update以后,表格就变成了这样:

我们事务2再次select id=12的数据,这时候就是在事务2第一次select生成的快照上查数据了

这就解决了不可重复读!!!

3. 理解 可重复读隔离级别,只生成一次数据快照

再举一个例子理解:在可重复读隔离级别,只生成一次数据快照

由于事务1已经commit了,新的数据不再是prepare状态,已经符合了生成快照的条件。当事务2再select(快照读)的时候,这条age=22的数据自然就被查到了

4. 理解 可重复读隔离级别,只能部分解决幻读

先查看表数据

回滚并重启事务

事务2生成的快照如下:

事务2第一次select是两条数据,事务1 insert之后,事务2再次select依然是两条,看似解决了幻读,其实只是部分解决(并不能完全解决幻读)

那我们看一下为什么是部分解决幻读

事务1 insert然后commit后,表格的数据应该是这样的

此时事务2 update

可以看见,update找到了id=24的数据,这就证明update做的是当前读(读最新的commit状态的数据),而不是快照读,因为快照上根本就没有id=24的数据

其中1000是事务1的ID,2000的事务2的ID

由于每个事务可以看见自己修改、更新的数据,当事务2再次select的时候,就可以看见id=24的数据了,这就发生了幻读(主要因为insert,delete,update,select...lock in share mode/for update这些操作,是当前读)

未提交读 已提交读 可重复读 串行化
/ MVCC MVCC + 临键锁 临键锁
脏读、不可重复读、幻读 不可重复读、幻读 幻读 /
相关推荐
ruleslol19 小时前
MySQL的段、区、页、行 详解
数据库·mysql
独自归家的兔19 小时前
Spring Cloud核心架构组件深度解析(原理+实战+面试高频)
spring cloud·面试·架构
while(1){yan}19 小时前
MyBatis Generator
数据库·spring boot·java-ee·mybatis
奋进的芋圆19 小时前
DataSyncManager 详解与 Spring Boot 迁移指南
java·spring boot·后端
それども19 小时前
MySQL affectedRows 计算逻辑
数据库·mysql
是小章啊20 小时前
MySQL 之SQL 执行规则及索引详解
数据库·sql·mysql
计算机程序设计小李同学20 小时前
个人数据管理系统
java·vue.js·spring boot·后端·web安全
富士康质检员张全蛋20 小时前
JDBC 连接池
数据库
yangminlei20 小时前
集成Camunda到Spring Boot项目
数据库·oracle
Echo娴20 小时前
Spring的开发步骤
java·后端·spring