一、认识事务
1.引入
若MySQL的CURD不加控制会出现的问题:

对于以上的问题,CURD 满足以下条件
- 买票的过程是原子的
- 买票互相不能影响
- 买完票要永久有效
- 买前,和买后都要是确定的状态
而事务就是来解决这种问题的
2.事务的概念
事务的定义
-
事务是由一组逻辑上相关的DML语句(如INSERT、UPDATE、DELETE)组成的操作集合。
-
事务中的SQL语句要么全部成功执行 ,要么全部失败回滚,不存在部分执行的情况。
-
MySQL提供事务机制,确保这一整体性,并规定不同客户端看到的数据状态可以不同。
事务的四大属性(ACID)
1. 原子性(Atomicity)
-
一个事务中的所有操作,要么全部完成,要么全部不完成。
-
如果在执行过程中发生错误,系统会将事务**回滚(Rollback)**到开始前的状态,就像该事务从未执行过一样。
2. 一致性(Consistency)
-
事务开始前和结束后,数据库的完整性没有被破坏。
-
所有写入的数据必须符合预设的规则(如数据类型、约束、触发器等),确保数据的精确性、串联性和业务逻辑的正确性。
3. 隔离性(Isolation)
-
允许多个事务并发访问同一份数据时,彼此之间不会互相干扰。
-
通过事务隔离级别来避免因交叉执行导致的数据不一致问题。
-
隔离级别包括:
-
读未提交(Read Uncommitted)
-
读提交(Read Committed)
-
可重复读(Repeatable Read)
-
串行化(Serializable)
-
4. 持久性(Durability)
-
事务一旦提交,其对数据库的修改就是永久性的。
-
即使系统发生故障(如断电、崩溃),已提交的数据也不会丢失。
ACID 之间的关系
原子性、隔离性、持久性是因,一致性是果。正是由于事务具备了原子、隔离、持久三大特性,才能最终保证数据库在事务执行前后始终处于一致状态。
事务的本质
事务不仅仅是SQL语句的集合,更是一种从业务逻辑出发、在数据库层面保证数据正确性与完整性的机制。它的存在使得开发者可以将复杂的多步操作作为一个不可分割的单元来执行,从而简化并发控制和错误恢复的处理。
3.事务的必要性
- 当业务操作涉及操作量大、复杂度高的数据时,通常需要多条SQL共同完成,例如银行转账(先查询、再扣款、再加款)。这些SQL在逻辑上相互依赖,只有组合在一起才有业务意义,因此必须作为一个整体进行管理。
- 在高并发场景下,多个事务可能同时访问同一份数据,若不加以控制,会引发数据不一致、执行失败等问题。
4.事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
sql
mysql> show engines \G -- 行显示
*************************** 1. row ***************************
Engine: InnoDB -- 引擎名称
Support: DEFAULT -- 默认引擎
Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES -- 支持事务
XA: YES
Savepoints: YES -- 支持事务保存点
*************************** 5. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO -- MyISAM不支持事务
XA: NO
Savepoints: NO
5.事务的提交方式
事务的提交方式常见的有两种:
- 自动提交
- 手动提交
查看事务提交方式
sql
show variables like 'autocommit';

修改事务提交方式
sql
set autocommit=0; #禁止自动提交
set autocommit=1; #开启自动提交

6.事务操作演示
准备工作
将mysql的默认隔离级别设置成读未提交,便于演示
具体操作后面专门会讲,现在以使用为主:
设置隔离级别成功后,需要quit退出MySQL再重新登入

创建演示表
sql
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

①事务的常规操作
查看连接mysqld(服务器)的mysql(客户端)数量
sql
show processlist;

我们启动两个终端(两个客户端),一个执行事务操作,一个查看用户表信息,左边使用 begin 或 start transaction启动一个事务:

在左终端插入一条数据,由于我们将隔离等级设置成读未提交,所以在左终端使用commit提交之前,右终点就能查看到事务插入的信息:

然后我们在事务中使用 savepoint命令创建一个保存点,然后继续插入:

然后我们可以在事务中使用rollback加上保存点名就可以滚回到保存点,这时在右终端查看数据时就看不到我们插入的第二条信息了:

如果直接使用rollback不加保存点名,则直接回滚到事务开始:

小结:
- begin 或 start transaction 命令可以启动一个事务。
- savepoint 保存点命令,可以在事务中创建指定名称的保存点。
- rollback to 保存点命令,可以让事务回滚到指定保存点。
- rollback命令,可以直接让事务回滚到最开始。
- commit命令,可以提交事务,提交事务后就不能回滚了。
②演示原子性
如果左终端的事务在提交之前因为某些原因与MySQL断开连接,那么MySQL会自动让事务回滚到开始并且提交:

③演示持久性
当事务commit提交之后,即使左终端因为某些原因断开了与MySQL的连接,已经提交的数据不会回滚:

注:使用启动事务都必须要手动使用commit命令提交,数据才会被持久化,与autocommit无关
④单条SQL与事务的关系
全局变量autocommit影响的是单条SQL语句,InnoDB中的每一条SQL都会默认被封装成事务
autocommit为ON,则单条SQL语句执行后会自动被提交,如果为OFF,则SQL语句执行后需要使用commit进行手动提交:
- 当autocommit设置为自动提交时,不需要commit,只要执行完单条SQL,那么数据就已经具备了持久化,我们前面的博客的所有数据操作SQL都可以看作是自动提交的单条事务SQL
- 当autocommit设置为手动提交时,不论是否使用begin启动事务,所有的单条SQL都看作是事务,只有手动执行commit后,数据才会持久化
举例说明:下面两种操作效果相同

二、事务的隔离级别
1.认识隔离级别
隔离性的由来
MySQL服务可能同时被多个客户端进程(线程)访问,每个访问都以事务方式进行。一个事务由多条SQL构成,存在执行前、执行中、执行后三个阶段。原子性保证用户层面看到的是事务要么尚未执行、要么已经完成的状态,执行中出现问题可以随时回滚。
然而,所有事务都有执行过程,当多个事务各自执行多条SQL时,仍可能出现互相影响的情况,例如多个事务同时访问同一张表甚至同一行记录。为了保证事务执行过程中尽量不受干扰,数据库引入了隔离性这一重要特征。
隔离级别的概念
由于实际应用中允许事务受到不同程度的干扰,数据库进一步提出了隔离级别的概念,以便在不同场景下平衡数据一致性与并发性能。
隔离级别主要解决的是读写并发 带来的数据不一致问题------即当一个事务在读取数据时,另一个事务可能正在修改同一份数据,需要根据隔离级别来决定读操作能看到什么程度的数据。对于写写并发 ,由于两个事务同时对同一数据进行修改必然产生冲突,无论隔离级别如何,都必须通过锁机制强制串行执行,以保证数据正确性;而对于读读并发,多个事务同时读取数据不会相互影响,因此无需任何隔离措施。
四种隔离级别
-
读未提交(Read Uncommitted)
在该隔离级别下,所有事务都可以看到其他事务尚未提交的执行结果。这相当于没有任何隔离性,会引发脏读、幻读、不可重复读等多种并发问题,实际生产中几乎不使用,仅在极少数极端场景下才有应用。
-
读提交(Read Committed)
该隔离级别满足隔离的简单定义:一个事务只能看到其他已提交事务所做的改变。它是大多数数据库的默认隔离级别,但不是MySQL的默认级别。这种级别会引起不可重复读问题,即同一事务中多次执行SELECT可能得到不同的结果。
-
可重复读(Repeatable Read)
这是MySQL的默认隔离级别。它确保同一个事务在执行过程中多次读取操作数据时,会看到同样的数据行,从而解决了不可重复读的问题。但该级别下仍然存在幻读问题。
-
串行化(Serializable)
这是事务的最高隔离级别。它通过强制事务排序,使事务之间不可能相互冲突,从而解决了幻读问题。实现方式是在每个读取的数据行上加共享锁,但会导致超时和锁竞争问题,该级别过于极端,实际生产中基本不使用。
隔离级别的实现方式
隔离性基本上都是通过锁机制实现的,不同的隔离级别对锁的使用方式不同。常见的锁类型包括:
-
表锁
-
行锁
-
读锁(共享锁)
-
写锁(排他锁)
-
间隙锁(GAP)
-
Next-Key锁(GAP锁与行锁的组合)
2.隔离级别的查看与设置
虽然一个稳定的数据库通常会选择一种默认隔离级别,但默认级别可能无法满足上层业务需求,因此数据库提供了上述四种隔离级别供用户根据实际场景自行选择和设置。
①查看全局隔离级别
sql
select @@global.transaction_isolation;

②查看会话隔离级别
sql
select @@session.transaction_isolation;
#或者
select @@transaction_isolation;

③设置会话隔离级别
sql
set session transaction isolation level {read uncommitted/read committed/repeatable read/serializable};
设置会话的隔离级别只影响到当前会话,新建的对话仍然采用全局隔离级别

④设置全局隔离级别
sql
set global transaction isolation level {read uncommitted/read committed/repeatable read/serializable};
设置全局隔离级别时,修改后的值不会影响当前已经存在的会话 ,只有新启动的会话才会生效。
也就是说,每个会话在启动时,会将当前的全局隔离级别作为其初始值,之后该会话的隔离级别便与全局设置无关了。

3.四种隔离级别演示
①读未提交
首先启动两个终端,隔离级别都设置成读未提交:

接着双方都启动一个事务,左边的事务插入数据后,没有提交,右边的事务就已经能看到插入结果了:

②读已提交
首先将两个终端的隔离级别都设置成读已提交

两个终端都启动一个事务,左边事务添加的数据在没有提交之前,右边事务无法看到:

当左端事务commit提交之后,右端事务才能看到添加后的数据:

③可重复读
首先将两个终端的隔离级别都设置为可重复读:

两个终端都启动事务,左端事务添加的数据在commit提交之前,右端事务看不到数据,这和上面是一样的:

由于隔离级别是可重复读,即使左端事务提交了修改,右端事务仍然看不到数据:

只有右端事务也提交(右端事务结束)后,才能看到左端事务添加的数据:

④串行化
首先把两个终端的隔离级别都设置为串行化:

如果这两个事务中有一个事务要对表进行写操作,那么这个事务立即被阻塞:

只有访问这张表的其它事务都提交后,这个被阻塞的事务才会被唤醒:

一旦一个事务对表进行CURD操作时,此事务会被放入等待队列被阻塞,直到另一个事务提交,但是如果此事务阻塞时间过长,将会由于锁等待超时退出当前事务:(等待时间是可以修改的)

⑤隔离级别总结
1. 读未提交(Read Uncommitted)
特点:一个事务在执行过程中,另一个事务能够立即看到该事务未提交的数据。
实现方式:几乎没有加锁,并发效率高但问题严重。
存在的问题:
-
脏读:一个事务读到另一个未提交事务的更新数据
-
不可重复读、幻读
适用场景:实际生产中几乎不使用,仅在极少数极端场景下有应用。
2. 读提交(Read Committed)
特点:一个事务只能看到其他已提交事务所做的改变。
实现方式:读操作不加锁(使用MVCC),写操作加锁。
存在的问题:
- 不可重复读:同一事务内多次执行SELECT可能得到不同结果(因为其他事务提交了修改)
说明:这是大多数数据库的默认隔离级别,但不是MySQL的默认级别。
3. 可重复读(Repeatable Read)
特点:确保同一事务在执行过程中多次读取操作数据时,会看到同样的数据行。
实现方式:读操作不加锁(使用MVCC),写操作加锁。
解决的问题:解决了不可重复读问题。
存在的问题:
-
在一般数据库中会存在幻读问题
-
但MySQL在该级别下通过Next-Key锁(GAP锁+行锁)解决了幻读问题
说明:这是MySQL的默认隔离级别,一般情况下不建议修改。
4. 串行化(Serializable)
特点:通过强制事务排序,使事务之间不可能相互冲突。
实现方式:
-
读操作加共享锁(多个事务可并发读)
-
写操作加排他锁(写操作会阻塞其他事务)
解决的问题:解决了脏读、不可重复读、幻读所有问题。
缺点:会导致超时和锁竞争,并发性能极低,实际生产中基本不使用。
总结
核心问题:
| 问题 | 定义 | 特点 |
|---|---|---|
| 脏读 | 一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但未commit的数据 | 读到未提交的数据 |
| 不可重复读 | 同一事务内,同样的查询条件,在不同时间段读取到了不同的值(由于其他事务修改或删除了数据) | 重点在修改和删除,读出的值不一样 |
| 幻读 | 同一事务内,同样的查询条件,多次读取时记录数不一致(由于其他事务插入了新数据) | 重点在新增,读出的记录数不一样 |
幻读说明:
总述:
幻读是指在同一个事务中,使用相同的查询条件前后两次执行时,由于其他事务插入了新数据,导致读出的记录数不一致,仿佛出现了幻觉;它本质上属于不可重复读的一种特殊情形。
详细说明:
在可重复读隔离级别下,一个事务无论何时执行相同的查询,看到的数据都是一致的,这就是"可重复读"的含义。
但是,一般的数据库在可重复读级别下,并无法阻止其他事务插入新数据 。原因在于:隔离性主要是通过给已有数据加锁来实现的,而新插入的数据在插入前并不存在,所以普通的锁无法提前锁定一个"还不存在的记录",这就导致了一个问题------一个事务在查询时,可能会读到另一个事务刚刚插入的新数据,从而使前后两次查询的结果记录数不一致,就好像凭空多出了几条记录,这种现象就叫做幻读。
MySQL 在可重复读级别下,通过使用 Next-Key 锁(即间隙锁 GAP 与行锁的组合)解决了幻读问题。这种锁机制不仅能锁定已有的数据行,还能锁定数据行之间的"间隙",从而防止其他事务在这个间隙中插入新数据,保证了查询结果的一致性。
四种隔离级别对比:

隔离级别选择原则
-
隔离级别越严格,安全性越高,但数据库并发性能越低,因为锁竞争越激烈
-
需要在安全性与并发性能之间寻找平衡点
-
MySQL默认使用可重复读级别,一般情况下不建议修改
-
数据库提供四种隔离级别供用户根据业务场景自行选择和设置
4.一致性
当数据库只包含事务成功提交的结果时,数据库就处于一种一致性状态,而事务执行完的结果,就是使数据库从一个一致性状态变到另一个一致性状态:
- 一致性需要原子性来保证:事务执行过程中如果发生错误,则已经做出的操作需要回滚到最开始状态,就像该事务没有执行过一样
- 一致性需要持久性来保证:事务提交后,对数据的修改必须是永久的,即使服务器异常退出也不会丢失
- 一致性需要隔离性来保证:多个事务并发执行时,不会有数据不一致的问题
- 一致性也与用户的业务逻辑强相关,如果用户的业务逻辑不合理或存在问题,那么可能也会让数据库处于不一致状态
三、隔离性的底层实现
1.数据库的并发场景
数据库并发场景主要分为以下三类:
-
读-读并发:不存在任何问题,不需要并发控制。
-
读-写并发:这是数据库中最常见的并发场景,存在线程安全问题,可能引发事务隔离性问题,如脏读、不可重复读、幻读等异常。在解决读-写并发时,既要考虑线程安全,也要兼顾并发性能。
-
写-写并发:存在线程安全问题,可能导致更新丢失问题,具体包括两类:
-
第一类更新丢失(回滚丢失):一个事务的回滚覆盖了另一个已提交事务的更新数据;
-
第二类更新丢失(覆盖丢失):一个事务的提交覆盖了另一个已提交事务的更新数据。
-
2. 多版本并发控制(MVCC)概述
多版本并发控制(Multi-Version Concurrency Control,简称MVCC) 是一种用于解决读-写冲突 的无锁并发控制机制。它通过为每个修改保存多个版本,使读操作无需等待写操作,写操作也无需等待读操作,从而在保证事务隔离性的同时提升数据库的并发性能。
3.MVCC的核心实现依赖
理解MVCC需要掌握以下三个前提知识:
- 三个记录隐藏字段
- undo日志
- Read View(读视图)
3.1 三个记录隐藏字段
在 InnoDB 存储引擎中,数据库表中的每条记录除了用户自定义的字段外,还包含若干隐藏字段,用于支持 MVCC 和事务回滚等机制。主要包括:
-
DB_TRX_ID(6 字节):记录最近一次修改(插入或更新)该记录的事务 ID。
-
DB_ROLL_PTR(7 字节):回滚指针,指向该记录的上一个版本(通常存储在 undo log 中),用于构建版本链。
-
DB_ROW_ID(6 字节):隐含的自增 ID。如果表没有定义主键,InnoDB 会自动以该字段生成聚簇索引;若表有主键,则 DB_ROW_ID 不会作为索引使用。
此外,还有一个删除标志(delete flag) 隐藏字段,用于标记记录是否被删除,当执行 DELETE 或更新操作时并不立即物理删除,而是修改该标志,便于事务回滚和 MVCC 读取历史版本。
举例说明:
假设我们向一张只有姓名和年龄字段的表中插入一条记录,该插入操作的事务ID为9,那么系统会自动为该记录添加三个隐藏字段:

DB_TRX_ID填入9,表示创建该记录的事务IDDB_ROW_ID填入1,作为隐式自增主键(因为这是插入的第一条记录)DB_ROLL_PTR设置为 null,表示该记录没有历史版本
这三个隐藏字段是 MVCC 实现的核心基础,此外还存在其他隐藏字段(如删除标志),但图中未画出。
3.2 undo日志
undo 日志的基本概念
undo log(回滚日志)是 MySQL 三大日志之一,主要用于:
-
保证事务的原子性:支持对已执行操作进行回滚
-
支持 MVCC:记录数据的历史版本,构建版本链
undo log 可以理解为 MySQL 内存中的一段缓冲区(日志缓冲区),用于临时存储日志数据,后续在适当时机刷新到磁盘。
MVCC 模拟:版本链的形成
以更新操作为例,演示版本链的构建过程:
1. 初始插入
事务 ID 为 9 的事务插入一条记录(张三,28),记录中:
-
DB_TRX_ID = 9 -
DB_ROW_ID = 1(隐式主键) -
DB_ROLL_PTR = null(无历史版本)

2. 第一次更新(事务 10)
事务 10 将 name 从"张三"改为"李四":
-
先给记录加行锁
-
修改前,将原记录拷贝到 undo log(写时拷贝)
-
修改原记录的
name为"李四" -
修改原记录的
DB_TRX_ID = 10 -
修改原记录的
DB_ROLL_PTR指向 undo log 中副本的地址 -
事务 10 提交,释放锁
此时最新记录为"李四",版本链形成:最新记录 → undo log 副本(张三)

3. 第二次更新(事务 11)
事务 11 将 age 从 28 改为 38:
-
先给最新记录加行锁
-
修改前,将当前最新记录拷贝到 undo log(采用头插方式)
-
修改原记录的
age为 38 -
修改原记录的
DB_TRX_ID = 11 -
修改原记录的
DB_ROLL_PTR指向新副本的地址 -
事务 11 提交,释放锁
此时版本链为:最新记录(李四,38)→ 上一版本(李四,28)→ 最初版本(张三,28)

核心机制
-
每次修改都采用写时拷贝,将当前版本拷贝到 undo log 后再修改
-
多个版本通过回滚指针形成历史版本链
-
回滚就是用 undo log 中的历史版本覆盖当前数据
insert 与 delete 的版本处理
1. delete 操作
-
记录被删除时,并非物理删除
-
先将记录拷贝到 undo log
-
将隐藏字段中的删除标志(delete flag) 设置为 1
-
回滚时,将标志改回 0,数据恢复
2. insert 操作
-
插入前没有数据,因此没有历史版本
-
为支持回滚,插入的数据也会被放入 undo log
-
事务提交后,undo log 中的 insert 记录可以被清空
3. select 操作
-
select 本身不修改数据,因此无需为 select 维护版本
-
但 select 读取时可以选择读取最新版本(当前读)或历史版本(快照读)
当前读 vs 快照读
| 类型 | 定义 | 操作示例 |
|---|---|---|
| 当前读 | 读取最新的记录 | 增(INSERT)、删(DELETE)、改(UPDATE)、以及 SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE |
| 快照读 | 读取历史版本 | 普通的 SELECT 语句(在特定隔离级别下) |
-
当前读需要加锁,以保证数据一致性
-
快照读通过 MVCC 读取历史版本,无需加锁,可实现读写并行,提高并发性能
隔离级别与读取方式的关系
-
读未提交(RU) 和 串行化(Serializable) 下,
SELECT为当前读 -
读提交(RC) 和 可重复读(RR) 下,
SELECT可能为当前读 或快照读,具体取决于隔离级别的实现机制
隔离性存在的根本原因
事务从 BEGIN 到 CURD 到 COMMIT,存在执行前、执行中、执行后三个阶段。多个事务并发执行时,CURD 操作会交织在一起。为了在并发场景下保证每个事务"有先有后"并看到自己该看到的内容,就需要引入隔离性 与隔离级别。
MVCC 通过无锁的快照读,使得读操作无需等待写操作,写操作也无需等待读操作,在保证隔离性的同时大大提升了数据库的并发性能。
3.3 Read View(读视图)
Read View 的定义与作用
Read View(读视图) 是 MySQL 在事务执行快照读 时生成的一个数据结构,用于判断当前事务能够看到数据版本链中的哪一个版本。它本质上是事务可见性判断的依据,生成时机是事务首次进行快照读的时刻,而非事务启动时。
Read View 在 MySQL 源码中是一个类,其核心成员包括:
| 字段 | 含义 |
|---|---|
m_ids |
生成 Read View 时系统中活跃事务 ID 的列表(即尚未提交的事务) |
m_up_limit_id |
m_ids 列表中的最小事务 ID,也称为"低水位" |
m_low_limit_id |
生成 Read View 时系统尚未分配的下一个事务 ID(即当前已出现的最大事务 ID + 1),也称为"高水位" |
m_creator_trx_id |
创建该 Read View 的当前事务的 ID |
下面是ReadView中的部分源码字段:
cpp
class ReadView {
private:
/** 高水位:大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
};
可见性判断规则
当事务进行快照读时,会遍历版本链中的各个版本,每个版本记录都有 DB_TRX_ID(创建或最后修改该版本的事务 ID)。通过将 DB_TRX_ID(当前事务的 ID ) 与 Read View 的字段进行比较,判断该版本是否可见:
-
若
DB_TRX_ID < m_up_limit_id(m_ids列表中的最小事务 ID)说明该版本由生成 Read View 时已经提交的事务 修改,因此可见。
-
若
DB_TRX_ID >= m_low_limit_id(当前已出现的最大事务 ID + 1**)**说明该版本由生成 Read View 时尚未启动的事务 修改,因此不可见。
-
若
m_up_limit_id <= DB_TRX_ID < m_low_limit_id说明该版本由生成 Read View 时正处于活跃状态或刚好提交的事务修改。此时:
-
如果
DB_TRX_ID在m_ids列表中 ,表示该事务仍然活跃(未提交),则不可见; -
如果
DB_TRX_ID不在m_ids列表中 ,表示该事务在生成 Read View 前已经提交,则可见。
-
-
特殊规则
若
DB_TRX_ID == m_creator_trx_id,即该版本由当前事务自己修改,则可见。

判断流程(源码逻辑)

id=当前事务的 ID
在 changes_visible 函数中,判断流程如下:
-
若
id < m_up_limit_id或id == m_creator_trx_id,返回true(可见)。 -
若
id >= m_low_limit_id,返回false(不可见)。 -
若
m_ids为空,返回true(可见)。 -
否则,在
m_ids列表中二分查找id:-
若找到,表示该事务仍活跃,返回
false(不可见); -
若未找到,返回
true(可见)。
-
若当前版本不可见,则通过回滚指针 DB_ROLL_PTR 沿版本链向下遍历,继续判断上一个版本,直到找到可见版本为止。
示例说明
假设初始记录为(张三,28)
|----------|---------|------------------------|------------------|--------------------|
| name | age | DB_TRX_ID(创建该记录的事 务ID) | DB_ROW_ID(隐式 主键) | DB_ROLL_PTR(回滚 指针) |
| 张三 | 28 | null | 1 | null |
依次启动事务操作:时间自顶向下
|-----------------------|-----------------------|-------------------|-----------------------|
| 事务 1 [id=1] | 事务 2 [id=2] | 事务 3 [id=3] | 事务 4 [id=4] |
| 事务开始 | 事务开始 | 事务开始 | 事务开始 |
| ... | ... | ... | 修改且已提交 |
| 进行中 | 快照读 | 进行中 | |
| ... | ... | ... | |
其中事务 4 在事务 2 快照读之前已提交,事务 1 和事务 3 仍在活跃。
事务 2 进行快照读时生成的 Read View:
-
m_ids = [1, 3](活跃事务) -
m_up_limit_id = 1(m_ids列表中的最小事务 ID) -
m_low_limit_id = 5(最大事务 ID 为 4,下一个为 5) -
m_creator_trx_id = 2(当前事务的 ID)
此时版本链是:

判断是否能看到事务 4 提交后的版本:
-
4 < 1?否。 -
4 >= 5?否。 -
4在m_ids中吗?否(因为事务 4 已提交)。 -
结论:可见。
因此事务 2 的快照读能够看到事务 4 提交的最新版本。
总结
-
Read View 是快照读的可见性依据,在事务首次快照读时生成,之后不再变化。
-
通过
m_up_limit_id(低水位)和m_low_limit_id(高水位)将事务 ID 划分为三个区间:-
小于低水位:已提交,可见;
-
大于等于高水位:未启动,不可见;
-
位于中间 :通过
m_ids判断是否活跃,活跃则不可见,否则可见。
-
-
版本链遍历保证了事务只看到自己或已提交事务的修改,从而实现 MVCC 的无锁快照读,提升并发性能。
4.RR 与 RC 的本质区别
4.1 核心区别
| 隔离级别 | Read View 生成时机 | 快照读可见性 |
|---|---|---|
| 可重复读(RR) | 事务中第一次快照读 时生成一个 Read View,之后整个事务内的所有快照读都复用这个 Read View | 只能看到 Read View 生成前已提交的事务所做的修改,之后其他事务提交的修改不可见 |
| 读提交(RC) | 事务中每次快照读 都会重新生成一个新的 Read View | 每次快照读都能看到当前已提交的最新数据,因此可能看到其他事务在不同时间提交的修改 |
4.2 RR 级别下快照读的特点
-
首次快照读决定后续
事务中第一次执行快照读(如普通
SELECT)时,MySQL 会生成一个 Read View,记录此时系统中所有活跃事务的 ID。后续所有快照读都使用同一个 Read View。 -
对后续提交的修改不可见
如果其他事务在第一次快照读之后提交了修改,由于 Read View 未更新,这些修改对当前事务的快照读仍然不可见。这保证了同一事务内多次快照读结果一致,即可重复读。
-
当前读与快照读的分离
在 RR 级别下,使用
SELECT ... LOCK IN SHARE MODE或SELECT ... FOR UPDATE执行的是当前读(读取最新数据,会加锁),不受快照读 Read View 的限制,可以立即看到其他事务已提交的修改。
4.3 RC 级别下快照读的特点
-
每次快照读独立生成 Read View
事务中每次执行快照读(普通
SELECT)都会重新生成一个 Read View,其中包含当前系统最新的活跃事务信息。 -
可见其他事务的提交
由于每次快照读都基于最新的 Read View,因此如果其他事务在两次快照读之间提交了修改,第二次快照读就能看到这些修改,导致同一事务内多次读取结果不同,即不可重复读。
4.4 RR 与 RC 对不可重复读的影响
-
RR 级别:通过复用同一个 Read View,避免了不可重复读问题。
-
RC 级别:每次快照读都生成新的 Read View,所以无法避免不可重复读。
4.5 快照读与当前读的补充说明
-
快照读 :普通
SELECT,通过 MVCC 读取历史版本,无锁。 -
当前读 :
SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE等,总是读取最新版本,需要加锁。
在 RR 级别下,如果事务在第一次快照读之后其他事务提交了修改,快照读仍然看不到,但当前读可以看到。
示例
首先将两个终端的隔离级别都设置为可重复读:

两个终端都启动事务,左端事务添加的数据在commit提交之前,右端事务看不到数据

由于隔离级别是可重复读,即使左端事务提交了修改,右端事务使用**普通 SELECT**仍然看不到数据:(快照读)

但是左端事务提交了修改,右端事务使用**SELECT ... LOCK IN SHARE MODE**可以看到数据:(当前读)

4.6 总结
RR 与 RC 的本质区别在于 Read View 的生成策略:
-
RR:事务中第一个快照读创建 Read View,之后复用,保证可重复读。
-
RC:每次快照读都创建新的 Read View,因此可能产生不可重复读。
这一差异直接决定了两种隔离级别在并发读写时对数据一致性的保障程度,以及性能上的权衡。