事务与隔离级别
问题
MySQL 事务的 ACID 特性是什么?四种隔离级别分别解决了什么问题?InnoDB 默认的隔离级别为什么选择可重复读?
答案
ACID 特性
| 特性 | 含义 | 实现方式 |
|---|---|---|
| A(Atomicity)原子性 | 事务中的操作要么全部成功,要么全部回滚 | undo log(回滚日志) |
| C(Consistency)一致性 | 事务前后数据库从一个一致状态到另一个一致状态 | 由 A + I + D 共同保证 |
| I(Isolation)隔离性 | 并发事务之间互不干扰 | MVCC + 锁 |
| D(Durability)持久性 | 事务提交后数据永久存储,即使崩溃也不丢失 | redo log(重做日志) |
ACID 中一致性是最终目标,原子性、隔离性、持久性是实现手段。一致性还包括业务层面的约束(如转账前后总金额不变),不仅靠数据库保证。
并发事务问题
当多个事务并发操作同一数据时,可能出现以下问题:
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到了其他事务未提交的数据 | 事务 A 修改了一行数据但未提交,事务 B 读到了修改后的值,随后 A 回滚 |
| 不可重复读 | 同一事务中两次读取同一行,结果不同 | 事务 A 两次读取同一行中间,事务 B 修改并提交了该行 |
| 幻读 | 同一事务中两次查询,结果集的行数不同 | 事务 A 两次范围查询中间,事务 B 插入了新行并提交 |
四种隔离级别
SQL 标准定义了 4 种隔离级别,由低到高:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最低级别,几乎不用 |
| READ COMMITTED(读已提交) | ❌ 解决 | ✅ 可能 | ✅ 可能 | Oracle 默认级别 |
| REPEATABLE READ(可重复读) | ❌ 解决 | ❌ 解决 | ✅ 可能 | InnoDB 默认级别 |
| SERIALIZABLE(串行化) | ❌ 解决 | ❌ 解决 | ❌ 解决 | 最高级别,性能最差 |
InnoDB 在 REPEATABLE READ 级别下通过 MVCC + Next-Key Lock 在很大程度上解决了幻读问题(但并非完全消除,快照读和当前读混用时仍可能出现)。这是 InnoDB 选择 RR 作为默认级别的重要原因。
隔离级别的实现原理
| 隔离级别 | MVCC 读视图策略 | 加锁策略 |
|---|---|---|
| READ UNCOMMITTED | 不使用 MVCC,直接读最新值 | 不加读锁 |
| READ COMMITTED | 每次 SELECT 创建新的 Read View | 记录锁 |
| REPEATABLE READ | 事务开始时创建 Read View,整个事务复用 | 记录锁 + 间隙锁 (Next-Key Lock) |
| SERIALIZABLE | 不使用 MVCC | 所有 SELECT 自动加共享锁 |
关于 MVCC 的详细原理,参见 MVCC 多版本并发控制。
快照读 vs 当前读
InnoDB 中有两种读取方式:
| 读类型 | 说明 | SQL 示例 |
|---|---|---|
| 快照读(Snapshot Read) | 读取 MVCC 版本链中的快照数据 | 普通 SELECT |
| 当前读(Current Read) | 读取最新的已提交数据,并加锁 | SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE |
在 RR 级别下,如果事务中先快照读再当前读,可能出现"幻读"现象:
-- 事务 A
BEGIN;
SELECT * FROM user WHERE id = 5; -- 快照读,不存在 id=5
-- 事务 B 插入 id=5 并提交
-- INSERT INTO user(id, name) VALUES(5, 'test'); COMMIT;
-- 事务 A
UPDATE user SET name = 'new' WHERE id = 5; -- 当前读,能更新成功!
SELECT * FROM user WHERE id = 5; -- 这次能看到 id=5(因为自己修改过)
COMMIT;
严格说这不算标准定义的幻读,但确实是 RR 级别下的一个"异常"现象。
设置和查看隔离级别
SELECT @@transaction_isolation; -- MySQL 8.0+
SELECT @@tx_isolation; -- MySQL 5.7
-- 设置全局隔离级别(新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 仅对下一个事务生效
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
事务的基本操作
-- 开启事务
BEGIN; -- 或 START TRANSACTION;
-- 执行操作
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 提交事务
COMMIT;
-- 回滚事务(出错时)
ROLLBACK;
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
SAVEPOINT sp1; -- 设置保存点
UPDATE account SET balance = balance + 100 WHERE id = 2;
ROLLBACK TO sp1; -- 回滚到保存点,只撤销第二条
COMMIT; -- 第一条 UPDATE 仍然提交
自动提交
MySQL 默认开启自动提交(autocommit = 1),每条 SQL 自动作为一个事务执行并提交。
-- 查看自动提交状态
SHOW VARIABLES LIKE 'autocommit';
-- 关闭自动提交(需手动 COMMIT)
SET autocommit = 0;
长事务会导致:
- undo log 膨胀:事务期间的所有旧版本数据都不能被清理
- 锁持有时间过长:阻塞其他并发事务
- 占用大量数据库连接
生产环境应避免长事务,设置合理的 innodb_rollback_on_timeout 和 wait_timeout。
常见面试问题
Q1: ACID 分别靠什么机制来保证?
答案:
| 特性 | 实现机制 |
|---|---|
| 原子性 A | undo log:记录事务的反向操作,回滚时执行 undo log 撤销修改 |
| 持久性 D | redo log:事务提交时先写 redo log(WAL),即使崩溃也能通过 redo log 恢复数据 |
| 隔离性 I | MVCC(快照读隔离)+ 锁(当前读隔离) |
| 一致性 C | 上述三者共同保证,加上数据库约束(主键、外键、唯一索引、NOT NULL 等) |
关于 redo log 和 undo log 的详细原理,参见 MySQL 日志系统。
Q2: 脏读、不可重复读、幻读的区别?
答案:
- 脏读:读到了未提交的数据,如果对方回滚了,读到的就是无效数据。是最严重的一致性问题。
- 不可重复读:同一事务中两次读取同一行数据值不同(被别人修改了)。针对的是 UPDATE/DELETE。
- 幻读:同一事务中两次范围查询行数不同(别人插入了新行)。针对的是 INSERT。
从严重程度看:脏读 > 不可重复读 > 幻读。
Q3: MySQL 默认隔离级别是什么?为什么不用 RC?
答案:
MySQL InnoDB 默认使用 REPEATABLE READ(可重复读)。
选择 RR 而非 RC 的历史原因:MySQL 早期的 Statement 格式 binlog 在 RC 级别下可能导致主从数据不一致。在 RR 级别下配合 gap lock 可以避免这个问题。
虽然现在 Row 格式 binlog 已成为主流(不再有此问题),但默认级别保持了兼容。实际生产中很多公司(如阿里)会将隔离级别改为 RC,原因:
- RC 的锁范围更小(没有间隙锁),并发性能更好
- RC 配合 Row 格式 binlog 不会有主从不一致问题
- RC 更符合业务直觉(每次读都是最新已提交数据)
Q4: RR 级别下 InnoDB 如何解决幻读?
答案:
InnoDB 在 RR 级别下通过两种机制配合解决幻读:
- 快照读(普通 SELECT):通过 MVCC 解决。事务开始时创建 Read View,整个事务都读取这个时间点的快照,看不到其他事务新插入的行。
- 当前读(SELECT FOR UPDATE / INSERT / UPDATE / DELETE):通过 Next-Key Lock(记录锁 + 间隙锁)解决。锁住索引记录和记录之间的间隙,阻止其他事务在锁定范围内插入新行。
但如前所述,快照读和当前读混用时仍可能出现逻辑上的"幻读"。
Q5: 事务的传播行为是什么?(Spring 相关)
答案:
事务传播行为是 Spring 框架的概念,不是 MySQL 本身的特性。它定义了当一个事务方法调用另一个事务方法时,事务如何传播。
最常用的几种(MySQL 层面就是嵌套事务和保存点的使用):
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) | 有事务就加入,没有就新建 |
REQUIRES_NEW | 总是新建事务,挂起当前事务 |
NESTED | 有事务就创建嵌套事务(保存点) |
SUPPORTS | 有事务就加入,没有就非事务执行 |
NOT_SUPPORTED | 非事务执行,挂起当前事务 |
详细内容参见 Spring 事务管理。