Skip to content

事务问题:并发场景下的四大异常

想象一个场景:

你和你的女朋友同时查看同一张银行卡余额,都是 10000 块。

然后你刷卡消费了 5000 块,她也刷卡消费了 5000 块。

理论上,应该只剩 0 块。但如果数据库没有正确的隔离机制,可能出现各种奇怪的结果...

这就是事务的并发问题。


并发事务的四大异常

脏读(Dirty Read)

定义:事务 A 读取了事务 B 未提交的数据。

时间线:

T1: BEGIN;
T1: UPDATE accounts SET balance = 5000 WHERE user_id = 1;  -- 把余额改成 5000
T2: BEGIN;
T2: SELECT balance FROM accounts WHERE user_id = 1;  -- 脏读!读到了 T1 未提交的数据:5000
T1: ROLLBACK;  -- T1 回滚了,余额恢复成 10000
T2: UPDATE accounts SET balance = balance - 5000;     -- T2 基于错误的数据做操作
T2: COMMIT;

后果:T2 基于错误的余额做决策,导致数据不一致。

隔离级别:读未提交(Read Uncommitted)会产生脏读。


不可重复读(Non-Repeatable Read)

定义:同一个事务中,两次读取同一行数据,结果不一样(因为其他事务修改并提交了)。

时间线:

T1: BEGIN;
T1: SELECT balance FROM accounts WHERE user_id = 1;  -- 第一次读:10000
T2: BEGIN;
T2: UPDATE accounts SET balance = 8000 WHERE user_id = 1;  -- T2 修改了余额
T2: COMMIT;  -- T2 提交了
T1: SELECT balance FROM accounts WHERE user_id = 1;  -- 第二次读:8000(不一样了!)
T1: COMMIT;

特点:其他事务修改了数据并提交,导致当前事务两次读取结果不同。

隔离级别:读已提交(Read Committed)会产生不可重复读。


幻读(Phantom Read)

定义:同一个事务中,两次执行相同的查询,返回的记录数不一样(因为其他事务插入删除了记录)。

时间线:

T1: BEGIN;
T1: SELECT * FROM orders WHERE user_id = 1;  -- 第一次查:2 条订单
T2: BEGIN;
T2: INSERT INTO orders (user_id, amount) VALUES (1, 100);  -- T2 插入了一条新订单
T2: COMMIT;  -- T2 提交了
T1: SELECT * FROM orders WHERE user_id = 1;  -- 第二次查:3 条订单(多了!)
T1: COMMIT;

特点:其他事务插入或删除了记录,导致当前事务两次查询结果集不同。好像遇到了「幻影」。

隔离级别:可重复读(Read Committed)及以上可能产生幻读。


脏写(Dirty Write)

定义:事务 A 修改了一行数据,事务 B 也修改了同一行数据,但事务 B 先提交了。事务 A 后来回滚时,覆盖了事务 B 的修改。

时间线:

T1: BEGIN;
T2: BEGIN;
T1: UPDATE accounts SET balance = 5000 WHERE user_id = 1;  -- T1 余额改 5000
T2: UPDATE accounts SET balance = 6000 WHERE user_id = 1;  -- T2 余额改 6000(T2 在 T1 之前提交)
T2: COMMIT;
T1: ROLLBACK;  -- T1 回滚,恢复原值 10000,覆盖了 T2 的 6000

后果:T2 的提交被 T1 的回滚覆盖了,T2 的修改丢失了。

隔离级别:几乎所有隔离级别都会产生脏写,这是最严重的并发问题。


四种异常对比

异常原因读取了什么隔离级别
脏读读取了未提交的数据其他事务未提交的修改Read Uncommitted
不可重复读其他事务修改并提交了数据其他事务提交的修改Read Committed
幻读其他事务插入了新记录其他事务插入的记录Repeatable Read
脏写两个事务同时修改,后提交的覆盖了先提交的-几乎所有

图解:脏读 vs 不可重复读 vs 幻读

┌─────────────────────────────────────────────────────────────────┐
│                        并发事务异常                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  脏读(Dirty Read):                                           │
│  T1: UPDATE x = 1 (未提交)                                      │
│  T2: SELECT x FROM ... -- 读到了 T1 未提交的值                  │
│  T1: ROLLBACK                                                        │
│                                                                 │
│  不可重复读(Non-Repeatable Read):                             │
│  T1: SELECT x = 1 (第一次读)                                    │
│  T2: UPDATE x = 2 (并提交)                                      │
│  T1: SELECT x = 2 (第二次读,和第一次不一样)                    │
│                                                                 │
│  幻读(Phantom Read):                                          │
│  T1: SELECT COUNT(*) = 10 (第一次查,10 条记录)                 │
│  T2: INSERT INTO ... (并提交)                                   │
│  T1: SELECT COUNT(*) = 11 (第二次查,多了 1 条)                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

如何解决这些异常?

不同的隔离级别可以防止不同的异常:

隔离级别防止脏读防止不可重复读防止幻读
Read Uncommitted
Read Committed
Repeatable Read✅(InnoDB)
Serializable

面试场景

面试官: 什么是脏读?

你: 脏读是指一个事务读取了另一个事务未提交的数据。比如事务 A 修改了一条记录但还没提交,事务 B 读取了这条记录,然后事务 A 回滚了——事务 B 读到的就是脏数据。

面试官: 幻读和不可重复读有什么区别?

你: 不可重复读是同一个事务中,两次读取同一行数据结果不同(因为被其他事务修改并提交了)。幻读是两次执行相同查询,返回的记录数不同(因为其他事务插入或删除了记录)。

面试官: MySQL 的 InnoDB 是怎么解决幻读的?

你: InnoDB 的默认隔离级别是可重复读(RR),通过 MVCC(多版本并发控制)解决了大部分幻读问题。对于当前读(如 SELECT ... FOR UPDATE),InnoDB 还会使用间隙锁来防止幻读。


一句话总结

并发事务的四大异常:脏读是读到了别人未提交的数据,不可重复读是别人修改并提交后导致数据变了,幻读是别人插入或删除了记录导致记录数变了,脏写是回滚覆盖了别人的提交。

基于 VitePress 构建