🗄️ 第四章 管理数据-避免非法数据的功能
📄 一句话概括

死锁是事务世界里的十字路口对峙:两个事务各拿一把锁、都在等对方放手,谁都走不了——它是「并发 + 事务」的必然代价,数据库只能定期发现并牺牲一方(回滚),程序要做的是别让对峙发生。

📝 本节和前面几节的关系

本节是第四章的收尾:约束防线(4-6~4-13)和事务防线(4-14~4-16)都立住了,本节讲它们组合使用时的暗礁——多事务并发互相锁等。ACID 里的 I(隔离性)是死锁的温床,正好混个脸熟后收账。

一、对峙怎么形成

两个事务,两张锁:

时刻 1:事务甲 → 锁住 A 表(我要改 A)
时刻 2:事务乙 → 锁住 B 表(我要改 B)
时刻 3:事务甲 → 想改 B …… 等乙放锁
时刻 4:事务乙 → 想改 A …… 等甲放锁

甲等乙、乙等甲,环形成立——两个都动弹不得,连接数越积越多,整个系统被拖死。

二、锁与隔离:为什么会锁

事务未提交时,数据库对正在改的行/表加锁,防止别的连接读到「草稿」脏数据(隔离性 I)。锁是必要的,但拿锁的顺序一乱,就埋下死锁。

三、数据库的两条救命机制

机制 做法
死锁检测 定期扫等待环,发现环形等待 → 牺牲一方(回滚代价小的那个,报错「deadlock found」),另一方继续
锁超时 等锁超过时限自动放弃报错,避免无限等

两种结局都是「一方报错」——接到这个错,重试整个事务即可(对方已走,路空了)。

四、程序侧防死锁三招

  1. 统一加锁顺序:所有事务都先动 A 再动 B,环永远成不了
  2. 事务越短越好:BEGIN 到 COMMIT 别拖,别在事务里等人工确认、调外部接口
  3. 缩小锁范围:改哪几行动哪几行,别动不动全表 UPDATE

五、动手 Demo:亲眼造一次对峙(思维版)

在线单连接环境造不出真死锁(它需要两个连接同时开事务),用推演表代替:

连接 1                          连接 2
BEGIN;                          BEGIN;
UPDATE t SET n=1 WHERE id=1;    UPDATE t SET n=2 WHERE id=2;
UPDATE t SET n=1 WHERE id=2;    UPDATE t SET n=2 WHERE id=1;
  → 等(行 2 被连接 2 锁着)      → 等(行 1 被连接 1 锁着)
      └──────── 环形成立,死锁 ────────┘

稍后:数据库检测到环 → 回滚其中一个 → 另一个完成
被回滚方收到错误 → 程序重试整个事务 → 成功

本机装 MySQL 后双开两个客户端可真实复现,报错形如 Deadlock found when trying to get lock。

六、30 秒实验

把推演里两个事务的 UPDATE 顺序改成一致(都先动 1 再动 2)再推一遍——第二个事务在「动 1」时就开始等,等第一个 COMMIT 后自然继续,环永远成不了。顺序一致 = 死锁免疫。

七、常见误区

自测三问 1. 死锁形成的条件?(两事务互持对方要的锁,环形等待) 2. 数据库怎么处理死锁?(检测到环后回滚一方,报错让程序重试) 3. 防死锁第一招?(所有事务统一加锁顺序)

相关笔记