基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析

在这里插入图片描述

  • 前言
  • 事务特性
  • 事务并发引发的问题
    • 脏读
    • 不可重复读
    • 幻读
  • 隔离级别
    • 如何更改事务隔离级别
  • 事务基本操作
  • MVCC
    • 版本链
    • ReadView
    • READ COMMITTED
      • 脏读问题
      • 不可重复读问题
    • REPEATABLE READ
      • 不可重复读问题
      • 幻读问题
    • 小结
  • 总结

前言

MySQL 事务是比较重要且核心的一部分,在操作数据库 DML 语句时,以及开源框架基于 MySQL 进行事务操作时,保持事务的 ACID 特性是数据可靠的一大保障

事务特性

原子性(Atomicity)

一个事务必须被视为不可分割的最小单元,事务的所有操作要么全部提交成功、要么全部提交失败,对于一整个事务来说,不能只执行其中的一部分操作,例如:A 转账给 B,A 余额必须减少,B 余额必须增加

一致性(Consistency)

事务从一种状态扭转到另外一种状态,在事务开始前、结束后,数据的完整性没有被破坏
例如:A、B 事务操作前的总额是多少,转账后,事务操作后的总额也应该是一样的

隔离性(Isolation)

事务的执行不能被其他事务执行所干扰,即一个事务的执行应该与其他并发执行的事务是相互隔离的

持久性(Durability)

一旦事务被提交,对其所做的任何信息变更,都应该永久持久化到数据库中,即使系统瘫痪,已提交的数据也不会丢失

事务并发引发的问题

MySQL 基于客户端 / 服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接之后,就可以称之为一个会话 Session;每个客户端都可以在开启的会话中向 MySQL 服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说,可以同时处理多个事务

MySQL 四大事务特性中 > 隔离性,理论上在某个事务在对数据进行访问或 DML 操作时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问此数据,这样的话,由并发事务执行就转变为了串行化执行;串行化执行方式对性能影响太大,既想保持事务的隔离性,又想让服务器在处理同一数据 > 多事务时性能尽量高些,从而会为我们带来以下数据问题:脏读、不可重复读、幻读.

脏读

当一个事务读取到了另外一个事务修改但未提交的数据,称之为脏读

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第1张图片

如上图,在事务 A 执行过程中,事务 A 对数据资源进行了修改,事务 B 读取到了事务 A 修改后的数据;可能由于某些原因,导致事务 A 没完成提交,发生了 Rollback 操作,则事务 B 读取到的数据就是脏数据

这种读取到另外一个事务未提交的数据的现象称为脏读(DD)

不可重复读

当在一个事务内的记录被检索过两次,若两次得到的结果不同,此现象称为不可重复读

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第2张图片

事务 B 读取了两次数据,在第一次读取完准备读第二次期间,事务 A 修改了数据,导致事务 B 在第二次读出来的数据与第一次是不一致的.

幻读

在事务执行过程中,另外一个事务新增或删除了记录,正在读取记录的事务,会发生幻读.

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第3张图片
事务 B 在前后两次读取同一个范围内的数据,在第一次读取完准备读第二次期间,事务 A 新增或删除了数据,导致事务 B 后一次读取到前一次未统计到的行数

幻读、不可重复读有些类似,但幻读重点强调了读取到了之前没有获取到的记录

隔离级别

SQL 标准中规定了四种隔离级别:未提交读、已提交读、可重复读、可串行化读,但不同数据库厂商对 SQL 标准规定的四种隔离级别支持不一样,比如:Oracle 只支持已提交读、可串行化读两种隔离级别,MySQL 同 SQL 标准一样支持四种隔离级别,但与其不同的是,MySQL 在可重复读隔离级别下,是一大程度下是可以避免幻读问题发生的.

隔离级别 脏读 不可重复读 幻读
未提交读
READ UNCOMMITTED
可能 可能 可能
已提交读
READ COMMITTED
可能 可能
可重复读
REPEATABLE READ
SQL 标准可能
MySQL 少数场景会发生
可串行化
SERIALIZABLE

MySQL 默认隔离级别:REPEATABLE READ,可以手动修改事务的隔离级别

如何更改事务隔离级别

通过下面的语句可以更改事务的隔离级别:

SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL level;

level 可选值有四个:REPEATABLE READ、READ COMMITTED、READ UNCOMMITTED、SERIALIZABLE

设置事务的隔离级别语句中,在 SET 关键字后面可以放置 GLOBAL 关键字、SESSION 关键字,这样会对不同范围的事务产生不同的影响,具体如下:

  1. 使用 GLOBAL 关键字,在全局范围内生效
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

对执行完该语句之后产生的会话起作用,当前已经存在的会话无效

  1. 使用 SESSION 关键字,在当前会话范围内生效
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

对当前会话的所有后续事务生效,该语句可以在已经开启的事务中执行,但不会影响当前正在执行的事务

  1. GLOBAL、SESSION 两者都不使用,那么只会执行语句的下一个事务产生影响
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

对当前会话中下一个即将开启的事务有效,下一个事务执行完以后,将会恢复到默认的隔离级别,该语句不能在已经开启事务中间执行,会报错 Transaction characteristics can't be changed while a transaction is in progress

  1. 若想在服务器启动时想改变事务的默认隔离级别,可以修改启动参数 transaction-isolation 值,比如:在启动服务器时指定了 --transaction-isolation=SERIABLIZABLE,那么事务的默认隔离级别就会从原来的 REPEATABLE READ -> SERIABLIZABLE

查看当前会话默认隔离级别可以通过查看系统变量 transaction_isolation 值或 SELECT @@transaction_isolation 来确定

mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

注意:transaction_isolation 系统变量是在MySQL 5.7.20 版本中引入来替换 tx_isolation 的,若你使用的还是之前版本,请将上述用到系统变量 transaction_isolation 地方替换为 tx_isolation

事务基本操作

事务开始:begin、start transaction(推荐)、begin work

事务回滚:rollback

事务提交:commit

事务保存点:savepoint

回滚保存点:ROLLBACK TO [SAVEPOINT] 保存点名称;

在我们进行事务操作时,操作了不同的 DML 语句,可以基于不同的语句作 savepoint
比如:插入 A 数据,执行 savepoint a、更新 B 数据,执行 savepoint b、删除 C 数据,执行 savepoint c
此时,若想取消删除 C 数据这个步骤,可以执行:ROLLBACK TO c; RELEASE SAVEPOINT c;

隐式提交:是否开启隐式提交 > 取决于 autocommit ON 开或 OFF 关

当使用 START TRANSACTION 或 BEGIN 语句开启了一个事务,或者把系统变量 autocommit 值设置为 OFF 时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了 COMMIT 语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交

会导致事务隐式提交的语句包括,如下:

  1. 执行 DDL 语句 > create、alter、drop,当执行这些语句时,就会隐式提交前面 SQL 语句所属的事务.
    基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第4张图片

  2. 更新 MySQL 数据库表信息:使用 ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD 等语句时也会隐式的提交前边语句所属于的事务

  3. 事务控制或关于锁定的语句:在一个会话中,一个事务还未提交或回滚,又使用 START TRANSACTION 或 BEGIN 语句开启了一个事务,会隐式提交上一个事务;或者使用了 LOCK TABLES、UNLOCK TABLES 等关于锁定的语句也会隐式提交前面语句所属的事务

  4. 加载数据语句:使用 LOAD DATA 语句来批量往数据库导入数据时,也会隐式提交前面语句所属的事务

  5. 其他语句:START SLAVE、STOP SLAVE、RESET SLAVE、ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH 等语句也会隐式提交前面语句所属的事务

MVCC

MVCC 全称 Multi-Version Concurrency Control,多版本并发控制,主要是为了提高数据库的并发性能

平时,在同一行数据上同时发生读写请求时,会上锁阻塞住,但 MVCC 提供更好的方式去处理读-写请求,可以做到在发生读-写请求冲突时不用加锁

这个读是快照读,不是当前读,当前读是一种加锁的操作,是悲观锁 > FOR UPDATE

前面在介绍 隔离级别 时,说到了 MySQL 在 REPEATABLE READ 隔离级别下,可以很大程度上避免幻读问题的发生,从以下几个概念及实操来说明是如何去避免的

版本链

当对一条数据并发执行多次操作时,对该条数据会形成版本链

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中包含了两个必要的隐藏列,不包括 row_id

row_id 并不是必要的,在创建的表中有主键或非 NULL 的 UNIQUE 键时,row_id 就是主键或唯一键,若没有主键或唯一键,会默认生成 row_id

必要的两个隐藏列,如下:

  1. trx_id:一个事务对某条聚簇索引记录进行改动时,都会把该事务的 id 赋值给 trx_id 隐藏列
  2. roll_pointer:在对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后 roll_pointer 这个隐藏列就相当于一个指针,可以通过它来溯源该记录修改前的信息

undo 日志:为了实现事务的原子性操作,InnoDB 存储引擎在实际进行 DML 操作记录时,都先要把对应的 undo 日志记下来。一般对一条记录进行一次改动,就对应一条 undo 日志,但在某些更新操作中,也可能会对应两条 undo 日志;一个事务在执行过程中可能会新增、删除、更新若干条记录,也就是说会记录很多条 undo 日志,这些 undo 日志会从 0 开始编号,依此按顺序生成:1、2、…、N,此编号被称之为 undo no

undo 日志是 MySQL 三大日志其中之一,还包含了 redo log、bin log,日志这方面的内容后续文章再详细分析

为了说明【MySQL 在 REPEATABLE READ 隔离级别下,可以很大程度上避免幻读问题的发生】此问题,创建一张演示表,如下:

create table technology_column(
	id BIGINT(10) not null primary key auto_increment COMMENT '主键',
	category_name varchar(30) not null COMMENT '专栏名称'
) Engine=InnoDB CHARSET=utf8 COMMENT '技术专栏表';

往这张表中插入一条数据,如下:

INSERT INTO technology_column VALUES(1, 'Spring');

假设插入该条数据的事务 id > trx_id = 80,那么此条记录的示意图如下:

在这里插入图片描述
假设之后有两个事务:trx_id 分别为 100、120,对这条记录进行了 UPDATE 操作,操作流程如下:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第5张图片

对记录每次进行改动,都会记录一条 undo 日志,每条 undo 日志都有一个 roll_pointer 属性
(INSERT INTO 操作对应的 undo 日志没有该属性,因为该记录没有更早的版本)可以将这些 undo 日志连起来,串成一个链表,如上图右侧所示~

对 INSERT INTO 操作后的记录,每次更新后,都会将旧值放到一条 undo 日志中,就当是该记录的一个旧版本,便于作事务回滚、数据溯源,随着更新的次数增多,所有的版本都会被 roll_pointer 属性连接成一个链表,将这个链表称为版本链,版本链的头节点就是当前记录最新的值;另外,每个版本中还包含了生成该版本对应的事务 id
基于此,利用该记录的版本链来控制并发事务同时访问该记录的行为,那么这种机制就称之为多版本并发控制 MVCC

ReadView

读取视图 > 作用于 SQL 查询语句

对于使用 READ UNCOMMITTED 读未提交隔离级别的事务来说,由于可以读取到未提交的事务修改过的信息,所以直接读取记录的最新版本即可,由此读未提交就会出现脏读、不可重复读、幻读

对于使用 SERIALIZABLE 可串行化读隔离级别的事务来说,InnoDB 采用加锁的方式来访问记录,当事务正在执行时,其他事务就会阻塞住直到前面的事务提交或回滚后才会执行,所以不会出现脏读、不可重复读、幻读

引入版本链的机制主要是为了解决:已提交读、可重复读的事务隔离级别

对于使用 READ COMMITTED、REPEATABLE READ 隔离级别事务来说,都必须保证读取到的数据是已经事务已提交修改过的记录,也就是说:假如事务已经修改了记录但尚未提交,是不能直接读取到最新版本记录的

核心问题:READ COMMITTED、REPEATABLE READ 隔离级别在不可重复读、幻读上的区别是从何而来,基于前面所介绍的版本链,主要关键是需要判断这两种级别在版本链中哪个版本是当前事务可见的,为此 InnoDB 提出了 ReadView 概念

ReadView 主要包含了四个比较重要的内容,如下:

  1. m_ids:表示在生成 ReadView 时当前系统正在活跃的读写事务的事务 id 集合
  2. min_trx_id:表示在生成 ReadView 时当前系统中获取的读写事务中的最小事务 id,也就是 m_ids 中的最小值
  3. max_trx_id:表示在生成 ReadView 时当前系统中应该分配给下一个事务的 id 值

max_trx_id 并不是 m_ids 集合中的最大值,事务 id 是递增分配的;比如:现在有 id > 1、2、3 三个活跃事务,之后 id=3 的事务提交了,那么新的读事务在生成 ReadView 时, m_ids 集合中还有 1、2,min_trx_id 值就为 1,max_trx_id 值就为 4

  1. creator_trx_id:表示生成该 ReadView 读取视图的事务 id

下面来具体介绍,READ COMMITTED、REPEATABLE READ 隔离级别是如何分别处理脏读、不可重复读、幻读问题的.

在 MySQL 中,READ COMMITTED、REPEATABLE READ 隔离级别非常大的一个区别就是它们生成 ReadView 的时机不同

READ COMMITTED

READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个 ReadView

以上面的 technology_column 表为例,现在只有一条事务 id 为 80 插入的一条记录

比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:

Trx_id-100120 Begin;
Trx_id-100120select * from technology_column where id = 1;
Trx_id-100update technology_column set category_name ='MySQL' where id = 1;   
Trx_id-100update technology_column set category_name ='Redis' where id = 1;   
Trx_id-100Commit;
Trx_id-120update technology_column set category_name ='分布式' where id = 1;   
Trx_id-120update technology_column set category_name ='Linux' where id = 1;   
Trx_id-120Commit;

脏读问题

Trx_id-100:Commit; 语句执行之前,technology_column.id =1 记录得到的版本链表,如下所示:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第6张图片

# 查询语句
select * from technology_column where id = 1;

以上使用 READ COMMITTED 隔离级别的事务,Trx_id 100、120 的事务均未提交,所以此时查询的数据仍然为 Spring!在 Trx_id-100:Commit; 语句执行之前,整个的执行过程如下:

  1. 在执行语句时,会先生成一个 ReadView
  2. ReadView 活跃集合 m_ids 内容为 100,120,min_trx_id 为 100,max_trx_id 为 121,creator_id 为 0
  3. 从版本链中挑选可见的记录,从上图中可以看出,最新版本的 category_name 值内容为 Redis;该版本的 trx_id 为 100,在 m_ids 集合内,所以不符合可见性要求

trx_id 属性值在 ReadView 中 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;若不在 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务已经被提交,该版本才可以被访问

  1. 通过 Roll_ptr 指针跳到下一版本继续访问,下一个版本 category_name 值内容为 MySQL,该版本的 trx_id 为 100,仍然在 m_ids 集合内,也不符合可见性要求
  2. 通过 Roll_ptr 指针跳到下一版本继续访问,下一个版本 category_name 值内容为 Spring,该版本的 trx_id 为 80,小于 ReadView 中 min_trx_id 值,所以这个版本是符合要求的,最后返回给客户端的版本就是这条 category_name 值内容为 Spring 的记录

所以有这种机制存在,就不会发生脏读问题!因为会去判断活跃的事务版本,必须是不在活跃中的事务版本才能使用,也就不可能读到没有 commit 提交的记录

不可重复读问题

基于上面的操作,先将 trx_id = 100 事务提交,然后再到 trx_id =120 事务更新表中 technology_column.id 为 1 的记录,也就是执行如下 SQL 语句:

Trx_id-100Commit;
Trx_id-120update technology_column set category_name ='分布式' where id = 1;   
Trx_id-120update technology_column set category_name ='Linux' where id = 1; 

此时,technology_column 表中 id=1 记录的版本链如下图所示:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第7张图片
接着使用 READ COMMITTED 隔离级别事务,在 Trx_id-120 begin; 语句后查询的数据为 Spring,而在 Trx_id-100:Commit; 语句执行之后,Trx_id-120:Commit; 语句执行之前,使用 Trx_id 为 120 的事务,查询 id=1 记录,此时查询到的数据为 Redis,整个的执行过程如下:

  1. 在执行 SELECT 语句时又会单独生成一个 ReadView
  2. ReadView 活跃集合 m_ids 内容为 120,min_trx_id 为 120,max_trx_id 为 121,creator_id 为 0

Trx_id 为 100 的事务已经提交了,所以再次生成 ReadView 快照时就没有它了

  1. 接着从版本链中挑选可见的记录,从图中可以看出,最新版本的 category_name 内容为 Linux,该版本的 Trx_id 值为 120,在 m_ids 集合内,所以不符合可见性要求
  2. 通过 Roll_ptr 指针跳到下一个版本中,下一个版本 category_name 内容为 分布式,该版本的 Trx_id 值为 120,在 m_ids 集合内,也不符合可见性要求
  3. 通过 Roll_ptr 指针跳到下一个版本中,下一个版本 category_name 内容为 Redis,该版本的 Trx_id 为 100,小于 ReadView 中 min_trx_id 值 120,所以这个版本的内容是符合可见性要求的,最后返回给用户的版本就是这条 category_name 内容为 Redis 的数据
  4. 此时,在 Trx_id 为 120 的事务执行期间,就发生了两次查询数据不一致的问题,这就是不可重复读问题,由此说明,READ COMMITTED 读已提交隔离级别事务避免不了此问题的发生

依此类推,若之后 Trx_id 为 120 的事务也提交了,再次使用 READ COMMITTED 隔离级别事务,查询 technology_column.id 为 1 的记录时,得到的结果就是 Linux,具体流程类似于上面

通过 m_ids(活跃事务集合)、min_trx_id(最小事务 id) 结合隔离级别的特性,来比对版本链的记录是否符合可见性要求,而读已提交是在每次执行 SELECT 语句时都会生成 ReadView 视图快照.

REPEATABLE READ

REPEATABLE READ 隔离级别的事务只有在第一次读取数据时才会生成一个 ReadView,之后的查询就不会重复生成了

比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:

Trx_id-100120 Begin;
Trx_id-100120select * from technology_column where id = 1;
Trx_id-100update technology_column set category_name ='MySQL' where id = 1;   
Trx_id-100update technology_column set category_name ='Redis' where id = 1;   
Trx_id-100select * from technology_column where id = 1;
Trx_id-100Commit;
Trx_id-120update technology_column set category_name ='分布式' where id = 1;   
Trx_id-120update technology_column set category_name ='Linux' where id = 1;   
Trx_id-120select * from technology_column where id = 1;
Trx_id-120Commit;

不可重复读问题

在事务 100、120 执行前都会先生成 ReadView 读取视图,也就是它们读取到的 category_name 内容都是 Spring,在前面介绍过,READ COMMITTED 读已提交隔离级别会在 Trx_id 为 120 的事务执行期间发生不可重复读问题,所以在这里主要分析 REPEATABLE READ 可重复读隔离级别是如何解决此问题的, 它在执行事务时对应的版本链表如下:

Trx_id-120
基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第8张图片
在 Trx_id 为 120 第一次 SELECT 语句执行以后,生成了 ReadView 读取视图快照

ReadView 内容:m_ids 活跃事务集合为 100、120,min_trx_id 为 121,creator_trx_id 为 0

因为当前事务隔离级别为 REPEATABLE READ,所以在 Trx_id 为 120 的事务执行期间会直接复用上面所生成的 ReadView 快照信息,也就是前后两次 SELECT 查询语句执行后的结果都是一致的,读取到的 category_name 内容都是 Spring,这就是可重复读解决不可重复读问题的核心所在

因为它在事务执行期间,一直都是使用的第一次生成的 ReadView,自然而然也不会发生脏读问题,不会读取到其他事务已提交的数据信息

总结一下 ReadView 比较规则,如下:

  1. 若被访问版本的 Trx_id 属性值与 ReadView 中 creator_trx_id 值相同,那么就意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务所访问
  2. 若被访问版本的 Trx_id 属性值小于 ReadView 中 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 之前已经提交,所以该版本可以被当前事务所访问
  3. 若被访问的版本 Trx_id 属性值大于或等于 ReadView 中 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 之后才开启,所以该版本不可以被当前事务所访问
  4. 若被访问版本的 Trx_id 属性值在 ReadView 中 min_trx_id、max_trx_id 之间(min_trx_id < trx_id < max_trx_id)那就需要判断一下 trx_id 属性值是否在 m_ids 活跃事务集合中;如果在的话,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被其他事务所访问;如果不在的话,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被其他事务所访问

幻读问题

REPEATABLE READ 隔离级别在 MVCC 机制下可以解决不可重复读问题,也可以在一定程度下解决幻读问题

幻读问题:一个事务在某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自于另外一个事务添加的新记录

比如:在 REPEATABLE READ 隔离级别下,事务 T1 先通过某个搜索条件读取到多条记录,然后事务 T2 插入一条符合这个搜索条件的记录并提交完成,此时 T1 再次通过这个搜索条件执行查询,结果如下:无论事务 T2 比 事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交信息的,因为在 REPEATABLE READ 隔离级别下,只会在第一次查询时才生成 ReadView 读取视图快照,后续的查询会延续使用第一次生成的 ReadView 信息,由此可见,REPEATABLE READ 可以在一定程序下解决幻读问题的发生

但在某些情况下,REPEATABLE READ 隔离级别,InnoDB MVCC 会发生幻读问题,结合案例如下:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第9张图片

首先在事务 T1 中执行查询语句,如下:

select * from technology_column where id between 2 and 3;

此时 technology_column 表中只有一条数据,id 为 2、3 的数据并未存在;接着我们在事务 T2 中执行 INSERT 语句,如下图:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第10张图片

通过以上语句,在 technology_column 表中插入了一条 id 为 2 的数据,此时,再回到事务 T1 执行如下语句:

update technology_column set category_name = 'RocketMQ' where id = 2;
select * from technology_column where id between 2 and 3;

执行结果如下:

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第11张图片

很明显事务 T1 出现了幻读现象,在 REPEATABLE READ 隔离级别下,事务 T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView(但此时版本链表中没有生成的对应的条目)之后 T2 事务向表中新插入一条记录并提交,然后 T1 事务执行了 Update 语句;ReadView 并不能阻止事务 T1 执行 UPDATE 或 DELETE 语句来改动这个新插入的记录,但这样一来,这条新记录的 Trx_id 隐藏列的值就变成了事务 T1 的事务 id

基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析_第12张图片
之后事务 T1 再次使用普通 SELECT 语句去查询这条记录时就可以看到了,也可以把这条记录返回给客户端,因为这种特殊现象的存在,所以 REPEATABLE READ 可重复隔离级别不能完全避免幻读问题的发生

事务 T1 第一次读是空的情况,事务 T2 新增了这条数据,事务 T1 在自己事务中对这条数据进行了修改

小结

所谓的 MVCC(Multi-Version Concurrency Control)多版本并发控制,指的就是在使用 READ COMMITTED、REPEATABLE READ 这两种隔离级别事务时,执行普通 SELECT 操作时访问数据的版本链过程,这样子可以使不同的事务读写、写读操作并发执行,从而提高系统的性能

READ COMMITTED、REPEATABLE READ 这两种隔离级别事务一个最大不同:生成 ReadView 时机不同,READ COMMITTED 在每次进行普通 SELECT 操作时都会生成一个 ReadView,而 REPEATABLE READ 只有在第一次进行普通 SELECT 操作时生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 即可,从而基本上可以避免幻读问题的发生,但如果第一次读 ReadView 是空数据的情况下 > 某些场景则无法避免幻读的发生

最后,所谓的 MVCC 只是在我们进行普通 SELECT 查询时才生效,对于锁定读就是不普通的查询,所以它就无法让我们的 MVCC 机制发挥作用了.

总结

该篇博文,简而言之说明了 MySQL 中事务四大特性,详细阐述了由于四种隔离级别下各自会产生什么样的问题,从实战方面进行演示了 MySQL 基于事务上的一些基本操作,最重要的是讲述了 InnoDB 引擎下的 MVCC 机制,有提及它的版本链、ReadView 以及其下一些比较重要的概念,最后,重点说明了 READ COMMITTED 读已提交、REPEATABLE READ 可重复读,这两种隔离级别会是如何解决一些并发问题以及会在什么样的场景下发生一些异常问题。希望您能够喜欢,能帮助到你是我最大的快乐!

另外,博主有专门讲解 Spring 事务是如何结合 MySQL 一起应用的,博文链接如下:

Spring 事务传播机制、隔离级别以及事务执行流程源码结合案例分析

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

你可能感兴趣的:(MySQL,mysql,数据库,java)