行锁会出现死锁,根本原因是:行锁的粒度细、锁数量多,导致多个事务可以“交叉”持有不同行的锁,从而形成循环等待。下面分步骤拆解:
1️⃣ 行锁的粒度细,锁数量多
- 表锁:整个表只有1把锁,所有事务按顺序排队,无法形成循环等待。
- 行锁:每行记录都可独立加锁(如
SELECT ... FOR UPDATE锁定单行)。- 事务A可以锁住行1,事务B可以锁住行2,此时两把锁独立存在。
- 如果后续事务A想再锁行2,事务B想再锁行1,就会互相阻塞。
2️⃣ 死锁的四个必要条件(行锁如何满足)
| 死锁条件 | 行锁场景下的体现 |
|---|---|
| 互斥 | 行锁是独占的(如X锁),同一行只能被一个事务持有。 |
| 占有且等待 | 事务A已锁住行1,等待行2;事务B已锁住行2,等待行1。 |
| 不可抢占 | 行锁不会强制剥夺,必须等待事务主动释放。 |
| 循环等待 | 形成闭环:A等B → B等A(如下图)。 |
3️⃣ 经典死锁场景示例
场景:两个事务更新同一表的不同行
sql
-- 事务A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁住行1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待行2的锁(被事务B持有)
-- 事务B(几乎同时执行)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 锁住行2
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待行1的锁(被事务A持有)结果:事务A和B互相等待对方的锁,形成死锁(循环等待)。
数据库处理:InnoDB会检测到死锁,强制回滚其中一个事务(通常选择代价更小的事务)。
4️⃣ 为什么表锁不会死锁?
- 表锁只有一把锁,所有事务按顺序排队,无法形成“交叉持有”的循环等待(如事务A和B不可能同时持有表锁的一部分)。
5️⃣ 如何减少行锁死锁?
- 固定顺序加锁:所有事务按相同顺序访问行(如始终按
id升序更新)。 - 减少锁范围:用
WHERE精准命中索引,避免间隙锁扩大锁范围。 - 降低隔离级别:如从
REPEATABLE READ改为READ COMMITTED(减少间隙锁)。 - 缩短事务:尽快提交/回滚,减少锁持有时间。
总结一句话
行锁因“多把锁+交叉等待”而可能死锁,表锁因“只有一把锁+顺序排队”天然免疫死锁。