文章

MySQL 事务

MySQL 事务 MySQL主从复制:https://blog.csdn.net/a18792721831/article/details/146117935 MySQL Binlog:https://blog.csdn.net/a18792721831/article/details/146606305 MySQL General Log:https://blog.csdn.net/a18792

MySQL 事务

文章信息

  • 原文链接:https://jiayq.blog.csdn.net/article/details/156461289
  • 发布时间:2025-12-31 16:58:40
  • 标签:#mysql, #技术分享, #oracle, ##可见性判断算法, ##ACID

MySQL 事务

MySQL主从复制:https://blog.csdn.net/a18792721831/article/details/146117935
MySQL Binlog:https://blog.csdn.net/a18792721831/article/details/146606305
MySQL General Log:https://blog.csdn.net/a18792721831/article/details/146607343
MySQL Slow Log:https://blog.csdn.net/a18792721831/article/details/147166971
MySQL Error Log:https://blog.csdn.net/a18792721831/article/details/147167038
MySQL Redo Log: https://blog.csdn.net/a18792721831/article/details/149862528
MySQL Undo Log: https://blog.csdn.net/a18792721831/article/details/149880355
MySQL 索引算法: https://blog.csdn.net/a18792721831/article/details/149883014
MySQL 索引类型: https://blog.csdn.net/a18792721831/article/details/150275404
MySQL 索引优化: https://blog.csdn.net/a18792721831/article/details/150282163
MySQL 锁: https://blog.csdn.net/a18792721831/article/details/154197322

MySQL 事务
  • 事务的特性
  • 事务的实现
    • 原子性的实现
    • 一致性的实现
    • 隔离性的实现
    • 持久性的实现
  • MVCC 实现
    • 什么是 MVCC
    • 隐藏字段
    • 版本链
    • ReadView
    • 可见性判断
  • 普通读和当前读
    • 普通读
    • 当前读
  • 隔离级别
    • 读未提交
    • 读已提交
    • 可重复读
    • 串行化
  • 总结

事务(Transaction) 是数据库操作的最小工作单元,也是一组操作的集合。事务是一个逻辑概念,事务中的操作要么全部成功,要么全部失败。事务即是区分文件系统和数据库的重要特征之一,也是一个关系型数据库的灵魂所在。

事务的特性

事务一般具有(ACID)特性:

  • A(Atomicity) :原子性,一个事务要么全部成功,要么全部失败。
  • C(Consistency) :一致性,事务的执行不能破坏数据库的完整性和一致性,事务执行前后的数据库必须处于一致的状态。
  • I(Isolation) :隔离性,隔离性一般指的是在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务破坏或干扰。
  • D(Durability) :持久性,事务一旦提交,那么对数据库中的数据的改变就是永久性的,即使发生断点等宕机事故也能恢复数据。

事务的实现

MySQL InnoDB中的事务完全符合 ACID 的特性。

InnoDB中的事务实现需要3个工具,日志文件、锁、MVCC。

  • 日志文件:包含 Redo Log 和 Undo Log,记录了数据修改前后的日志。
  • 锁:锁在事务中主要用来实现隔离和并发。
  • MVCC:中文全称为多版本并发控制,主要通过行数据中两个隐藏的字段(事务ID和回滚指针)来实现。

原子性的实现

原子性是指事务中的所有操作要么全部成功,要么全部失败回滚。

InnoDB通过 Undo Log 来实现原子性。当事务对数据进行修改时,InnoDB会生成对应的 Undo Log。如果事务执行失败或者调用了 ROLLBACK,就可以利用 Undo Log 中的信息将数据回滚到修改之前的状态。

Undo Log 记录了数据修改前的值,回滚时按照 Undo Log 的记录进行逆向操作:

  • 对于 INSERT 操作,Undo Log 记录主键信息,回滚时执行 DELETE
  • 对于 DELETE 操作,Undo Log 记录整行数据,回滚时执行 INSERT
  • 对于 UPDATE 操作,Undo Log 记录修改前的值,回滚时执行反向 UPDATE

举个例子,创建一个账户表:

1
2
3
4
5
6
7
8
9
10
CREATE DATABASE IF NOT EXISTS test_tx;
USE test_tx;
DROP TABLE IF EXISTS account;
CREATE TABLE account (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    balance INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO account(name, balance) VALUES('张三', 1000), ('李四', 1000);

执行一个转账事务,然后回滚:

1
2
3
4
5
6
7
8
9
10
USE test_tx;
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
SELECT * FROM account;
-- 此时张三余额500,李四余额1500
ROLLBACK;
SELECT * FROM account;
-- 回滚后张三余额1000,李四余额1000

执行结果:

1
2
3
4
5
6
7
8
9
10
-- 事务中查询
id	name	balance
1	张三	500
2	李四	1500

-- 回滚后查询
id	name	balance
1	张三	1000
2	李四	1000

可以看到,回滚后数据恢复到了事务开始前的状态。这就是 Undo Log 保证原子性的体现。

Undo Log 的回滚流程:

  1. 事务开始时,记录当前事务ID
  2. 每次修改数据前,先将原数据写入 Undo Log
  3. 如果事务失败或执行 ROLLBACK,根据 Undo Log 逆向恢复数据
  4. 如果事务提交,Undo Log 不会立即删除(因为可能被 MVCC 使用)

一致性的实现

一致性是事务的最终目标,是指事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。

说白了,一致性是一个综合性的保障,它依赖于原子性、隔离性和持久性的共同作用:

  • 原子性 保证事务要么全部成功,要么全部失败,不会出现部分执行的情况
  • 隔离性 保证并发事务之间互不干扰,避免数据异常
  • 持久性 保证已提交的事务数据不会丢失

除了 AID 的保障,一致性还依赖于:

  1. 数据库约束 :主键约束、外键约束、唯一约束、非空约束、CHECK约束等
  2. 触发器 :在数据变更时自动执行业务规则检查
  3. 应用层逻辑 :业务代码保证数据的业务一致性

举个例子,银行转账场景:

1
2
3
4
5
6
-- 张三转账500给李四
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
COMMIT;

一致性要求:转账前后,张三和李四的余额总和不变。

  • 如果事务执行成功,两条 UPDATE 都执行,总和不变
  • 如果事务执行失败,两条 UPDATE 都回滚,总和不变
  • 不会出现只执行一条 UPDATE 的情况

隔离性的实现

隔离性是指在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务干扰。

InnoDB通过 MVCC 两种机制来实现隔离性:

  • 锁机制 :通过共享锁(S锁)、排它锁(X锁)、意向锁、间隙锁、临键锁等实现写-写冲突的隔离
  • MVCC机制 :通过多版本并发控制实现读-写不冲突,提高并发性能

锁机制在之前的文章中已经详细介绍过,这里重点讲 MVCC。

不同的隔离级别,隔离性的实现方式不同:

隔离级别实现方式
读未提交(READ UNCOMMITTED)不加锁,直接读取最新数据
读已提交(READ COMMITTED)MVCC,每次读取生成新的 ReadView
可重复读(REPEATABLE READ)MVCC,事务开始时生成 ReadView,整个事务复用
串行化(SERIALIZABLE)加锁,读加共享锁,写加排它锁

持久性的实现

持久性是指事务一旦提交,对数据库的修改就是永久性的,即使发生宕机等故障也能恢复。

InnoDB通过 Redo Log 来实现持久性。

MySQL采用 WAL(Write-Ahead Logging) 技术,先写日志再写磁盘:

  1. 事务修改数据时,先将修改记录写入 Redo Log Buffer
  2. 事务提交时,将 Redo Log Buffer 刷新到磁盘的 Redo Log 文件
  3. 后台线程异步将脏页刷新到数据文件

这样即使在数据页刷盘之前发生宕机,也可以通过 Redo Log 恢复数据。

Redo Log 的落盘策略由 innodb_flush_log_at_trx_commit 参数控制:

  • 0 :每秒刷新一次,可能丢失1秒数据
  • 1 :每次事务提交都刷新,最安全但性能最低
  • 2 :每次提交写入OS缓存,每秒刷新到磁盘
1
2
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

生产环境建议设置为1,保证数据不丢失。

MVCC实现

什么是MVCC

MVCC(Multi-Version Concurrency Control),多版本并发控制,是一种并发控制的方法。

MVCC的核心思想是:为每一行数据维护多个版本,读操作读取的是数据的快照版本,写操作创建新的版本。这样读写操作就不会相互阻塞,大大提高了并发性能。

MVCC 主要用于实现 读已提交(RC)可重复读(RR) 两种隔离级别。

隐藏字段

InnoDB 在每行数据后面添加了三个隐藏字段:

字段名大小说明
DB_TRX_ID6字节最近修改该行的事务ID
DB_ROLL_PTR7字节回滚指针,指向 Undo Log 中该行的上一个版本
DB_ROW_ID6字节隐藏主键,如果表没有定义主键,则使用该字段

其中 DB_TRX_ID 和 DB_ROLL_PTR 是 MVCC 实现的关键。

版本链

每次对数据进行修改时,InnoDB 会:

  1. 将修改前的数据写入 Undo Log
  2. 更新数据行的 DB_TRX_ID 为当前事务ID
  3. 更新数据行的 DB_ROLL_PTR 指向 Undo Log 中的旧版本

这样,通过 DB_ROLL_PTR 就可以找到该行数据的历史版本,形成一条版本链。

举个例子,假设有一行数据:

1
2
3
id=1, name='张三', balance=1000
DB_TRX_ID=100, DB_ROLL_PTR=null

事务200修改 balance 为 800:

1
2
3
当前行:id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log
Undo Log:id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null

事务300修改 balance 为 500:

1
2
3
4
当前行:id=1, name='张三', balance=500, DB_TRX_ID=300, DB_ROLL_PTR -> Undo Log(200)
Undo Log(200):id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log(100)
Undo Log(100):id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null

这就形成了一条版本链:500(300) -> 800(200) -> 1000(100)

ReadView

ReadView 是 MVCC 实现的核心,用于判断版本链中哪个版本对当前事务可见。

ReadView 包含以下关键信息:

字段说明
m_ids生成 ReadView 时,当前系统中活跃的事务ID列表
min_trx_idm_ids 中的最小值
max_trx_id生成 ReadView 时,系统应该分配给下一个事务的ID
creator_trx_id创建该 ReadView 的事务ID

ReadView 的生成时机:

  • 读已提交(RC) :每次 SELECT 都生成新的 ReadView
  • 可重复读(RR) :事务第一次 SELECT 时生成 ReadView,后续复用

可见性判断

当事务读取某行数据时,需要判断版本链中哪个版本对当前事务可见。判断规则如下:

  1. 如果 DB_TRX_ID == creator_trx_id,说明是当前事务修改的,可见

  2. 如果 DB_TRX_ID < min_trx_id,说明该版本在 ReadView 生成前已提交,可见

  3. 如果 DB_TRX_ID >= max_trx_id,说明该版本在 ReadView 生成后才开始,不可见

  4. 如果 min_trx_id <= DB_TRX_ID < max_trx_id

    • 如果 DB_TRX_ID 在 m_ids 中,说明该事务还未提交,不可见
    • 如果 DB_TRX_ID 不在 m_ids 中,说明该事务已提交,可见

如果当前版本不可见,则通过 DB_ROLL_PTR 找到上一个版本,继续判断,直到找到可见的版本或者版本链结束。

举个例子:

假设当前有三个事务:

  • 事务100:已提交
  • 事务200:活跃中
  • 事务300:活跃中

事务300执行 SELECT,生成 ReadView:

  • m_ids = [200, 300]
  • min_trx_id = 200
  • max_trx_id = 301
  • creator_trx_id = 300

对于版本链 500(300) -> 800(200) -> 1000(100):

  1. 版本500,DB_TRX_ID=300,等于 creator_trx_id,可见 (如果是事务300自己修改的)
  2. 如果是其他事务读取,DB_TRX_ID=300 在 m_ids 中,不可见 ,继续找下一个版本
  3. 版本800,DB_TRX_ID=200 在 m_ids 中,不可见 ,继续找下一个版本
  4. 版本1000,DB_TRX_ID=100 < min_trx_id,可见

所以其他事务读取到的是 balance=1000。

普通读和当前读

普通读

普通读也叫快照读一致性读 ,是指不加锁的 SELECT 语句。

1
2
SELECT * FROM account WHERE id = 1;

普通读通过 MVCC 机制读取数据的快照版本,不会加任何锁,读写不冲突。

在不同隔离级别下的表现:

  • 读已提交(RC) :每次读取都生成新的 ReadView,可能读到其他事务已提交的修改
  • 可重复读(RR) :事务内复用同一个 ReadView,保证多次读取结果一致

当前读

当前读是指读取数据的最新版本,并且会对读取的数据加锁。

以下语句都是当前读:

1
2
3
4
5
6
7
8
9
10
11
12
-- 共享锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM account WHERE id = 1 FOR SHARE;  -- MySQL 8.0

-- 排它锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- DML语句(隐式当前读)
UPDATE account SET balance = 500 WHERE id = 1;
DELETE FROM account WHERE id = 1;
INSERT INTO account(name, balance) VALUES('王五', 1000);

当前读会读取最新的已提交数据,并根据语句类型加相应的锁:

  • LOCK IN SHARE MODE / FOR SHARE:加共享锁(S锁)
  • FOR UPDATE / DML语句:加排它锁(X锁)

当前读和普通读的区别:

特性普通读当前读
读取版本快照版本最新版本
是否加锁不加锁加锁
实现机制MVCC锁机制
并发性能较低

举个例子验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Session 1
USE test_tx;
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交

-- Session 2
USE test_tx;
BEGIN;
-- 普通读,读取快照版本
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(读取的是修改前的版本)

-- 当前读,会阻塞等待 Session 1 释放锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 阻塞中...

隔离级别

MySQL InnoDB 支持四种隔离级别,默认是可重复读(REPEATABLE READ)。

1
2
3
4
5
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 或者
SELECT @@transaction_isolation;

读未提交

读未提交(READ UNCOMMITTED) 是最低的隔离级别。

特点:

  • 事务可以读取其他事务未提交的数据(脏读)
  • 不使用 MVCC,直接读取最新数据
  • 并发性能最高,但数据一致性最差
1
2
3
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

脏读示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Session 1
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交

-- Session 2 (READ UNCOMMITTED)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(读到了未提交的数据,脏读)

-- Session 1
ROLLBACK;

-- Session 2
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(数据又变回去了)

读已提交

读已提交(READ COMMITTED) 是大多数数据库的默认隔离级别(但不是MySQL的默认)。

特点:

  • 只能读取其他事务已提交的数据,解决了脏读
  • 每次 SELECT 都生成新的 ReadView
  • 可能出现不可重复读(同一事务内多次读取结果不同)
1
2
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

不可重复读示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;

-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(同一事务内,两次读取结果不同)
COMMIT;

可重复读

可重复读(REPEATABLE READ) 是 MySQL InnoDB 的默认隔离级别。

特点:

  • 事务内多次读取同一数据结果一致,解决了不可重复读
  • 事务开始时生成 ReadView,整个事务复用
  • InnoDB 通过间隙锁解决了幻读问题
1
2
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

可重复读示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;

-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(仍然是1000,可重复读)
COMMIT;

串行化

串行化(SERIALIZABLE) 是最高的隔离级别。

特点:

  • 所有事务串行执行,完全隔离
  • 读加共享锁,写加排它锁
  • 并发性能最低,但数据一致性最高
1
2
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

在串行化级别下,普通的 SELECT 也会加共享锁:

1
2
3
4
5
6
7
8
9
10
-- Session 1 (SERIALIZABLE)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 自动加共享锁

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 阻塞,等待 Session 1 释放锁

四种隔离级别对比:

隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能InnoDB不可能
串行化不可能不可能不可能

总结

MySQL InnoDB 的事务实现是一个非常精妙的设计,通过 Redo Log、Undo Log、锁和 MVCC 四大机制协同工作,实现了完整的 ACID 特性。

  • 原子性 :通过 Undo Log 实现,事务失败时可以回滚到修改前的状态
  • 一致性 :是事务的最终目标,由 AID 共同保障,加上数据库约束和业务逻辑
  • 隔离性 :通过锁和 MVCC 实现,不同隔离级别有不同的实现策略
  • 持久性 :通过 Redo Log 实现,采用 WAL 技术保证数据不丢失

MVCC 是 InnoDB 实现高并发的关键技术,通过版本链和 ReadView 实现了读写不冲突。理解 MVCC 的工作原理,对于理解 MySQL 的并发控制和排查并发问题非常重要。

在实际开发中,需要根据业务场景选择合适的隔离级别:

  • 对数据一致性要求不高,追求性能:读已提交
  • 大多数场景:可重复读(MySQL默认)
  • 对数据一致性要求极高:串行化

同时也要注意普通读和当前读的区别,在需要读取最新数据或者需要加锁保护的场景,使用当前读(FOR UPDATE / LOCK IN SHARE MODE)。

以后还需要继续努力。加油!

本文由作者按照 CC BY 4.0 进行授权