事务隔离级别是数据库面试和日常开发中的高频话题,但很多人只是背下了“读未提交、读已提交、可重复读、串行化”这四个名词,对脏读、不可重复读、幻读的具体表现却一知半解。本文用两个典型例子,把抽象概念变成看得见的现象。
准备工作:一张账户表
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
);
INSERT INTO account VALUES (1, 1000);
以下例子基于 MySQL InnoDB 的默认设置。
脏读:读到了别人还没提交的数据
脏读指一个事务读到了另一个事务尚未提交的修改。
| 时刻 | 事务A | 事务B |
|---|---|---|
| T1 | BEGIN; | |
| T2 | UPDATE account SET balance = 2000 WHERE id = 1; | |
| T3 | BEGIN; | |
| T4 | SELECT balance FROM account WHERE id = 1; → 读到 2000 | |
| T5 | ROLLBACK; | |
| T6 | 再查一次,余额变回 1000 |
事务B在T4读到的2000,是事务A最终回滚掉的“脏数据”。如果业务根据这个值做了扣款或发货,就会出错。
如何避免:把隔离级别设为**读已提交(Read Committed)**或更高。MySQL 中通过 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; 设置。
幻读:同一范围查询,行数变了
幻读指同一事务内,两次执行相同的范围查询,第二次却多出(或少了)一些行。
| 时刻 | 事务A | 事务B |
|---|---|---|
| T1 | BEGIN; | |
| T2 | SELECT * FROM account WHERE balance > 500; → 1 行 | |
| T3 | BEGIN; | |
| T4 | INSERT INTO account VALUES (2, 800); | |
| T5 | COMMIT; | |
| T6 | 再次同样查询 → 变成 2 行 |
事务A两次查询条件完全相同,结果集却“凭空”多了一行,这就是幻读。它和不可重复读的区别在于:不可重复读针对同一行的值被修改,幻读针对结果集的行数发生变化。
MySQL InnoDB 的实际情况:在可重复读(Repeatable Read)级别下,InnoDB 通过 MVCC(多版本并发控制) 让普通 SELECT 始终读取事务开始时的快照,因此上面的例子在 RR 下不会出现幻读——第二次查询仍返回 1 行。
但注意:如果事务A执行的是 SELECT ... FOR UPDATE 这类当前读,InnoDB 会使用 Next-Key Lock(间隙锁) 阻止事务B插入,同样能避免幻读。所以“RR 完全解决幻读”这个说法并不严谨,准确说法是:InnoDB 在 RR 级别下通过 MVCC 和间隙锁,在大多数场景中避免了幻读。
四种隔离级别速查
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | InnoDB 下基本避免 |
| 串行化 | 不可能 | 不可能 | 不可能 |
实践建议
- 互联网业务常用读已提交(RC):减少间隙锁带来的死锁,提升并发。
- 金融类对一致性要求高的场景可用可重复读(RR),但要理解间隙锁可能增加锁冲突。
- 不要依赖默认值:不同数据库默认级别不同(Oracle、PostgreSQL 默认 RC,MySQL 默认 RR),跨库迁移时要显式确认。
- 业务层加锁(如
SELECT ... FOR UPDATE)比单纯调隔离级别更可控,但要注意锁的范围和顺序。
理解这些现象,比背诵名词更重要——下次遇到“数据对不上”的 bug,你就能快速定位是不是隔离级别在捣鬼。