数据库事务(Transaction)的 ACID 特性中,最难处理的就是 I(Isolation,隔离性)。
在理想状态下,我们希望所有的事务都像“排队”一样一个接一个执行。但在高并发的生产环境中,数千个事务同时读写同一张表。如果隔离得太死,系统性能会崩塌;如果隔离得太松,数据就会乱套。本文将带你理清并发场景下的数据冲突逻辑,以及如何应对令人生畏的“死锁”。
一、 并发下的三大“灵异事件”
在没有任何隔离措施的情况下,多个事务并发操作会产生三种经典问题:
- 脏读(Dirty Read): 事务 A 改了数据还没提交,事务 B 就读到了。万一事务 A 随后回滚了,事务 B 读到的就是彻底不存在的假数据。
- 不可重复读(Non-repeatable Read): 事务 A 在一个事务内读了两次同一条数据。结果在两次读取中间,事务 B 把它改了并提交了,导致 A 两次读到的结果不一样。
- 幻读(Phantom Read): 事务 A 按范围查询(如 WHERE id > 10),读到了 5 条。事务 B 此时插入了一条新数据并提交,事务 A 再读,发现莫名其妙多出了 1 条,“幻觉”产生了。
二、 四种隔离级别:安全与性能的权衡
为了解决上述问题,SQL 标准定义了四个隔离级别。级别越高,数据越安全,但并发性能越低。
隔离级别
解决脏读
解决不可重复读
解决幻读
性能
读未提交 (Read Uncommitted)
❌
❌
❌
极高
读已提交 (Read Committed)
✅
❌
❌
高
可重复读 (Repeatable Read)
✅
✅
✅ (MySQL)
中
可串行化 (Serializable)
✅
✅
✅
极低
注意: MySQL InnoDB 的默认级别是 Repeatable Read(可重复读)。与标准不同的是,MySQL 通过 MVCC(多版本并发控制) 和 Next-Key Locks 机制,在可重复读级别下就基本解决了幻读问题。
三、 底层黑科技:MVCC 到底是怎么回事?
为什么 MySQL 在可重复读下,别人改了数据我却看不见?它是怎么做到“不加锁也能高性能查询”的?
核心原理就是 MVCC。你可以把它想象成给每一行数据都存了多个“快照(Snapshot)”:
- 每行数据其实隐含了两个字段:创建该版本事务 ID 和 删除该版本事务 ID。
- 当事务开始时,系统会生成一个“一致性视图”。
- 事务查询时,只能看到“早于自己启动”的事务提交的数据。
结论: 这种“快照读”让查询不再需要加锁,读写不冲突,这是 MySQL 支撑高并发的基石。
四、 避坑指南:为什么会产生死锁?
死锁不是数据库的 Bug,而是并发控制的必然结果。最常见的死锁场景是:两个事务都在等待对方释放锁。
典型死锁案例:
- 事务 A: UPDATE t SET name = 'x' WHERE id = 1;(拿到了 ID=1 的行锁)
- 事务 B: UPDATE t SET name = 'y' WHERE id = 2;(拿到了 ID=2 的行锁)
- 事务 A: UPDATE t SET name = 'z' WHERE id = 2;(等待事务 B 释放 ID=2 的锁)
- 事务 B: UPDATE t SET name = 'w' WHERE id = 1;(等待事务 A 释放 ID=1 的锁)
死锁形成! 数据库检测到这种闭环后,会强制回滚其中一个事务。
五、 如何排查与预防死锁?
作为开发人员,遇到死锁不要慌,按以下步骤处理:
1. 如何排查?
当程序报错时,直接在数据库执行:
SHOW ENGINE INNODB STATUS;
在输出的 LATEST DETECTED DEADLOCK 部分,你会清楚地看到是哪两条 SQL 在互相伤害,以及它们各自持有了什么锁。
2. 如何预防?
- 固定操作顺序: 这是解决死锁最有效的办法。如果所有业务逻辑都是先更新 ID=1 再更新 ID=2,就不会产生闭环。
- 缩短事务逻辑: 事务越长,持锁时间越久,碰撞概率越大。尽量不在事务中进行远程 API 调用或复杂的耗时计算。
- 尽量使用索引: 如果 UPDATE 语句没有走索引,MySQL 会锁住整个表(甚至是锁住所有的间隙),大幅增加死锁概率。
- 降低隔离级别: 如果业务允许,可以将级别降为 Read Committed,这会减少大量的间隙锁(Gap Lock)。
六、 总结
- MySQL 默认是 Repeatable Read,靠 MVCC 实现高性能。
- 事务不是越长越好,长事务是系统崩溃的诱因。
- 死锁不可怕,关键是要有固定的操作顺序和高效的索引。