跳到主要内容

事务与隔离级别

问题

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 的特殊之处

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 UPDATESELECT ... LOCK IN SHARE MODEINSERTUPDATEDELETE
幻读的特殊情况

在 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;
保存点(Savepoint)
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;
长事务的危害

长事务会导致:

  1. undo log 膨胀:事务期间的所有旧版本数据都不能被清理
  2. 锁持有时间过长:阻塞其他并发事务
  3. 占用大量数据库连接

生产环境应避免长事务,设置合理的 innodb_rollback_on_timeoutwait_timeout


常见面试问题

Q1: ACID 分别靠什么机制来保证?

答案

特性实现机制
原子性 Aundo log:记录事务的反向操作,回滚时执行 undo log 撤销修改
持久性 Dredo log:事务提交时先写 redo log(WAL),即使崩溃也能通过 redo log 恢复数据
隔离性 IMVCC(快照读隔离)+ (当前读隔离)
一致性 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,原因:

  1. RC 的锁范围更小(没有间隙锁),并发性能更好
  2. RC 配合 Row 格式 binlog 不会有主从不一致问题
  3. RC 更符合业务直觉(每次读都是最新已提交数据)

Q4: RR 级别下 InnoDB 如何解决幻读?

答案

InnoDB 在 RR 级别下通过两种机制配合解决幻读:

  1. 快照读(普通 SELECT):通过 MVCC 解决。事务开始时创建 Read View,整个事务都读取这个时间点的快照,看不到其他事务新插入的行。
  2. 当前读(SELECT FOR UPDATE / INSERT / UPDATE / DELETE):通过 Next-Key Lock(记录锁 + 间隙锁)解决。锁住索引记录和记录之间的间隙,阻止其他事务在锁定范围内插入新行。

但如前所述,快照读和当前读混用时仍可能出现逻辑上的"幻读"。

Q5: 事务的传播行为是什么?(Spring 相关)

答案

事务传播行为是 Spring 框架的概念,不是 MySQL 本身的特性。它定义了当一个事务方法调用另一个事务方法时,事务如何传播。

最常用的几种(MySQL 层面就是嵌套事务和保存点的使用):

传播行为说明
REQUIRED(默认)有事务就加入,没有就新建
REQUIRES_NEW总是新建事务,挂起当前事务
NESTED有事务就创建嵌套事务(保存点)
SUPPORTS有事务就加入,没有就非事务执行
NOT_SUPPORTED非事务执行,挂起当前事务

详细内容参见 Spring 事务管理

相关链接