事务隔离级别的核心目标是在数据库并发执行多个事务时,平衡数据的一致性和系统的性能(并发度)。隔离级别越高,数据一致性越好,但并发性能越低(锁竞争更激烈,等待更多);隔离级别越低,并发性能越高,但可能出现各种数据不一致的问题。
MySQL 默认的事务隔离级别是 REPEATABLE READ(可重复读),并且在 REPEATABLE READ 级别下,通过 多版本并发控制 (MVCC) 机制,很大程度上避免了幻读问题(这是 MySQL 与其他一些数据库如 Oracle、PostgreSQL 默认级别为 READ COMMITTED 的一个重要区别)。
关键概念回顾:
- 脏读 (Dirty Read): 事务A读取了事务B尚未提交的修改数据。如果事务B最终回滚,事务A读到的就是无效的"脏"数据。
- 不可重复读 (Non-Repeatable Read): 在同一个事务A中,两次读取同一行数据,得到的结果不同。这是因为在两次读取之间,该行数据被另一个提交了的事务B修改了。
- 幻读 (Phantom Read): 在同一个事务A中,两次执行相同的查询(通常是范围查询
SELECT ... WHERE ...),得到的结果集行数不同(出现了新的"幻影"行或原有行消失了)。这是因为在两次查询之间,另一个提交了的事务B插入了满足查询条件的新行或删除了原有的行。
详细讲解四个隔离级别:
-
读未提交 (READ UNCOMMITTED)
- 特点: 这是最低的隔离级别。
- 允许的问题:
- 脏读: 事务可以读取其他事务尚未提交的修改。
- 不可重复读: 可能发生。
- 幻读: 可能发生。
- 并发性: 最高。因为它几乎不加锁(或者锁持有时间非常短),事务之间等待最少。
- 数据一致性: 最差。读取的数据可能是临时的、无效的(如果其他事务回滚)或中间状态。
- 使用场景: 非常罕见,通常仅在对数据准确性要求极低、需要极高吞吐量且能容忍脏数据的统计类场景(如实时大屏粗略计数)中考虑。强烈不建议在要求数据准确性的业务中使用。
- 实例:
-- 设置会话隔离级别为 READ UNCOMMITTED SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 事务A (查询账户余额,期望余额>=100才能操作) START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- 假设此时读到 balance=150 (未提交) -- 事务B (修改同一账户余额) START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 余额变为50,但尚未提交! -- 事务A 继续执行 (基于之前读到的150做判断) -- ... 做一些逻辑判断,认为余额150>100,允许操作 ... -- 此时事务B ROLLBACK; (可能是出错回滚) -- 实际余额还是150,但事务A之前读到的是50(脏数据)! -- 事务A 继续操作(如再扣款50),可能造成透支(如果它以为余额是150)或逻辑错误。 COMMIT; -- 事务A提交- 问题: 事务A读取了事务B未提交的修改 (
balance=50),而事务B最终回滚了。事务A基于脏数据 (50) 做了逻辑判断,可能导致错误。
- 问题: 事务A读取了事务B未提交的修改 (
-
读已提交 (READ COMMITTED)
- 特点: 这是许多数据库(如 Oracle, PostgreSQL)的默认隔离级别(但不是 MySQL 的默认)。
- 允许的问题:
- 脏读: ❌ 避免。 事务只能读取其他事务已经提交的修改。
- 不可重复读: ✔️ 可能发生。 同一事务内多次读取同一行,结果可能不同(如果其他已提交事务修改了该行)。
- 幻读: ✔️ 可能发生。 同一事务内多次执行相同范围查询,结果集行数可能不同(如果其他已提交事务插入/删除了满足条件的行)。
- 并发性: 较高。避免了脏读带来的最基础问题,锁的持有时间通常比
REPEATABLE READ短(行锁在语句执行后可能更快释放)。 - 数据一致性: 较好。保证了读取的数据是已提交的、有效的。但同一事务内的多次读取结果可能不一致。
- 使用场景: 适用于大多数不需要在同一个事务内保证多次读取数据绝对一致的场景。例如,一个展示数据的列表页,每次查询都是独立的快照。
- 实例:
-- 设置会话隔离级别为 READ COMMITTED SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 事务A (检查商品库存,然后下单) START TRANSACTION; SELECT stock FROM products WHERE id = 10; -- 假设读 stock=5 (已提交值) -- 事务B (购买同一商品) START TRANSACTION; UPDATE products SET stock = stock - 1 WHERE id = 10; -- stock变为4 COMMIT; -- 事务B提交 -- 事务A 再次检查库存 (准备扣减) SELECT stock FROM products WHERE id = 10; -- 现在读到 stock=4 (事务B已提交) -- 事务A 判断 stock=4 > 0,执行下单扣减库存: UPDATE products SET stock = stock - 1 WHERE id = 10; -- 预期变成3 COMMIT; -- 事务A提交,最终库存为3- 避免了脏读: 事务A第一次读到的5是事务B修改前的已提交值。
- 发生了不可重复读: 事务A在同一个事务内两次读取
id=10的stock,第一次是5,第二次是4,结果不同(因为事务B在中间提交了修改)。 - 潜在问题: 如果事务A的业务逻辑要求两次读取的库存必须一致(例如需要基于第一次读取的值做复杂的计算再扣减),那么
READ COMMITTED就不满足要求。这里最终库存是正确的(3),但事务A内部的逻辑可能因为不可重复读而复杂化。
-
可重复读 (REPEATABLE READ) - MySQL 默认级别
- 特点: 这是 MySQL 的默认事务隔离级别。通过 MVCC (多版本并发控制) 实现。
- 允许的问题:
- 脏读: ❌ 避免。
- 不可重复读: ❌ 避免。 在同一事务内,多次读取同一行数据的结果保证是一致的(即使其他事务已提交修改)。MVCC 通过为事务提供一致性视图 (Consistent Read View) 来实现,该视图基于事务开始时的快照。
- 幻读: ⚠️ 理论可能,但 MySQL InnoDB 很大程度上避免。 这是关键点!标准的 SQL 定义中,
REPEATABLE READ允许幻读。但是,MySQL 的 InnoDB 存储引擎通过 MVCC 和 Next-Key Locking(临键锁)的组合,在绝大多数情况下避免了幻读。对于快照读(普通SELECT语句),MVCC 保证看到的是事务开始时的快照,因此不会看到新插入的行。对于当前读(SELECT ... FOR UPDATE,SELECT ... LOCK IN SHARE MODE,UPDATE,DELETE),Next-Key Locking 会锁定扫描到的索引范围,阻止其他事务在该范围内插入,从而避免幻读。
- 并发性: 中等。比
READ COMMITTED稍低,因为锁(特别是 Next-Key Locks)可能持有更长时间,覆盖更大的范围(索引区间)。 - 数据一致性: 好。保证了事务内读取数据的稳定性(同一行可重复读),并通过机制有效防止了幻读,满足大多数应用的需求。
- 使用场景: MySQL 的默认选择,适用于绝大多数需要保证事务内数据读取一致性的场景,如订单处理、账户管理等。是兼顾一致性和并发性的良好平衡点。
- 实例:
-- MySQL 默认是 REPEATABLE READ, 显式设置可选 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 事务A (统计年龄<30的用户数,然后根据结果做其他操作) START TRANSACTION; SELECT COUNT(*) FROM users WHERE age < 30; -- 假设返回 100 (快照) -- 事务B (插入一个新用户并提交) START TRANSACTION; INSERT INTO users (name, age) VALUES ('New User', 25); -- age=25<30 COMMIT; -- 事务B提交 -- 事务A 再次统计 (确保一致性) SELECT COUNT(*) FROM users WHERE age < 30; -- **仍然返回 100** (MVCC 快照读) -- 事务A 基于 100 这个结果继续执行其他逻辑... COMMIT; -- 事务A提交。最终新用户已存在,但事务A内部的两次计数都是100。- 避免了不可重复读和幻读(快照读): 事务A内部的两次
SELECT COUNT(*)都返回 100。即使事务B插入了一个满足条件 (age=25<30) 的新用户并提交,事务A也看不到它(MVCC 读取事务开始时的快照)。这保证了事务A内部逻辑的一致性。 - 注意 UPDATE/DELETE (当前读):
-- 接上面事务A的第一次 SELECT COUNT(*)=100 之后... -- 事务B 插入并提交同上... -- 事务A 尝试更新所有 age<30 的用户 UPDATE users SET status = 'active' WHERE age < 30; -- 这个 UPDATE 是"当前读"。它会看到事务B提交的新插入行 (id=新用户)! -- 受影响的行数可能是 101 (原来100个 + 新插入的1个)! SELECT COUNT(*) FROM users WHERE age < 30; -- 快照读,仍然返回100- 这里发生了 "幻读" 现象:快照读
SELECT看不到新行,但UPDATE(当前读)却能更新到新插入的行,导致事务A内部对受影响行数的认知不一致(UPDATE影响101行,SELECT显示100行)。这是 MySQLREPEATABLE READ下可能遇到的边界情况。 不过,因为UPDATE成功修改了新行,并且这个修改在事务A提交后会持久化,数据本身最终是一致的。关键在于事务A内部的逻辑是否依赖SELECT和UPDATE/DELETE结果的一致性。Next-Key Locking 阻止了在UPDATE执行期间插入新的<30的行,但事务B在UPDATE之前 插入并提交的行会被扫描到。严格来说,这符合 SQL 标准对REPEATABLE READ允许幻读的定义,但 MySQL 通过锁机制避免了在执行过程中新幻影行的产生。
- 这里发生了 "幻读" 现象:快照读
- 避免了不可重复读和幻读(快照读): 事务A内部的两次
-
串行化 (SERIALIZABLE)
- 特点: 最高的隔离级别。它通过强制事务串行执行来实现。
- 允许的问题:
- 脏读: ❌ 避免。
- 不可重复读: ❌ 避免。
- 幻读: ❌ 避免。
- 实现方式: 简单理解,它会在读取的数据上自动加共享锁(
SELECT默认变成SELECT ... LOCK IN SHARE MODE),在写入的数据上加排他锁。这些锁会持有到事务结束。这导致事务之间几乎完全串行化,读写相互阻塞非常严重。 - 并发性: 最低。性能开销巨大,吞吐量急剧下降。
- 数据一致性: 最好。完全保证事务的隔离性,不会出现任何并发问题。
- 使用场景: 仅在对数据一致性要求极高,且完全不能接受任何并发副作用(如金融核心系统的某些极端操作),并且能承受极低并发性能的情况下使用。实践中很少使用。
- 实例:
-- 设置会话隔离级别为 SERIALIZABLE SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 事务A (读取并准备更新) START TRANSACTION; SELECT * FROM accounts WHERE type = 'SAVINGS'; -- 自动加共享锁 -- 事务B (尝试修改任何被事务A查询扫描到的行,或插入可能影响事务A查询的新行) START TRANSACTION; UPDATE accounts SET balance = balance + 100 WHERE id = 5; -- 如果id=5是SAVINGS类型,会被事务A的共享锁阻塞 -- 或者 INSERT INTO accounts (id, type, balance) VALUES (1001, 'SAVINGS', 500); -- 也可能被阻塞(取决于锁范围) -- 事务A 执行更新操作(如基于查询结果计算后更新) UPDATE accounts SET ... WHERE ...; COMMIT; -- 事务A提交后释放锁,事务B才能继续执行 COMMIT; -- 事务B提交- 问题避免: 严格加锁确保了事务A在执行过程中,其他事务无法修改事务A读取过的数据,也无法插入可能影响事务A查询结果的新数据。事务B的
UPDATE或INSERT会被阻塞,直到事务A完成。 - 并发代价: 事务B必须等待事务A完全结束才能执行,即使它们修改的是不同的行(如果事务A的查询扫描范围覆盖了事务B要操作的行或位置)。这导致严重的性能瓶颈。
- 问题避免: 严格加锁确保了事务A在执行过程中,其他事务无法修改事务A读取过的数据,也无法插入可能影响事务A查询结果的新数据。事务B的
总结对比表:
| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | 并发性能 | MySQL 默认 | 实现机制要点 |
|---|---|---|---|---|---|---|
| READ UNCOMMITTED | ✔️ 可能 | ✔️ 可能 | ✔️ 可能 | 最高 | ❌ | 几乎不加锁 |
| READ COMMITTED | ❌ 避免 | ✔️ 可能 | ✔️ 可能 | 较高 | ❌ (Oracle, PG默认) | 语句级快照 (MVCC), 行锁 |
| REPEATABLE READ | ❌ 避免 | ❌ 避免 | ⚠️ 理论可能 (InnoDB 通过 MVCC 快照读 + Next-Key Locking 当前读 很大程度避免) | 中等 | ✔️ | 事务级快照 (MVCC), Next-Key Locks |
| SERIALIZABLE | ❌ 避免 | ❌ 避免 | ❌ 避免 | 最低 | ❌ | 读加共享锁(S锁),写加排他锁(X锁) |
如何选择?
- 优先使用 MySQL 默认的
REPEATABLE READ: 它在保证数据强一致性(避免脏读、不可重复读,有效防止幻读)和并发性能之间取得了很好的平衡,适合绝大多数应用场景。 - 考虑
READ COMMITTED的场景:- 明确需要同一事务内看到其他事务的最新提交(例如,一个事务内先查询后更新,希望更新基于最新的已提交数据)。
- 对不可重复读不敏感的业务逻辑。
- 追求比
REPEATABLE READ稍高的并发吞吐量(尤其是在特定负载下)。 - 注意:切换到
READ COMMITTED需要仔细评估业务逻辑是否能容忍不可重复读和幻读。
- 避免使用
READ UNCOMMITTED和SERIALIZABLE: 除非有非常特殊且明确的、压倒性的理由(如极高吞吐量统计容忍脏数据,或绝对不允许任何并发异常的极端金融操作)。它们带来的问题(数据严重不一致)或代价(性能极差)通常难以承受。
设置隔离级别:
- 全局设置 (影响所有新会话):
SET GLOBAL TRANSACTION ISOLATION LEVEL = level;(需要SUPER权限) - 会话级设置 (仅影响当前会话):
SET SESSION TRANSACTION ISOLATION LEVEL = level; - 单个事务设置:
SET TRANSACTION ISOLATION LEVEL level;(在事务开始前执行)
结论:
理解事务隔离级别及其对并发的影响是设计和优化数据库应用的关键。MySQL 默认的 REPEATABLE READ 配合 InnoDB 的 MVCC 和 Next-Key Locking 提供了一种在保证良好数据一致性同时维持不错并发性能的方案。根据具体业务需求和对一致性/并发性的权衡,有时选择 READ COMMITTED 也是合理的。务必在理解不同级别行为的基础上进行选择和测试。