📄 一句话概括
死锁是事务世界里的十字路口对峙:两个事务各拿一把锁、都在等对方放手,谁都走不了——它是「并发 + 事务」的必然代价,数据库只能定期发现并牺牲一方(回滚),程序要做的是别让对峙发生。
📝 本节和前面几节的关系
本节是第四章的收尾:约束防线(4-6~4-13)和事务防线(4-14~4-16)都立住了,本节讲它们组合使用时的暗礁——多事务并发互相锁等。ACID 里的 I(隔离性)是死锁的温床,正好混个脸熟后收账。
一、对峙怎么形成
两个事务,两张锁:
时刻 1:事务甲 → 锁住 A 表(我要改 A)
时刻 2:事务乙 → 锁住 B 表(我要改 B)
时刻 3:事务甲 → 想改 B …… 等乙放锁
时刻 4:事务乙 → 想改 A …… 等甲放锁
甲等乙、乙等甲,环形成立——两个都动弹不得,连接数越积越多,整个系统被拖死。
二、锁与隔离:为什么会锁
事务未提交时,数据库对正在改的行/表加锁,防止别的连接读到「草稿」脏数据(隔离性 I)。锁是必要的,但拿锁的顺序一乱,就埋下死锁。
三、数据库的两条救命机制
| 机制 | 做法 |
|---|---|
| 死锁检测 | 定期扫等待环,发现环形等待 → 牺牲一方(回滚代价小的那个,报错「deadlock found」),另一方继续 |
| 锁超时 | 等锁超过时限自动放弃报错,避免无限等 |
两种结局都是「一方报错」——接到这个错,重试整个事务即可(对方已走,路空了)。
四、程序侧防死锁三招
- 统一加锁顺序:所有事务都先动 A 再动 B,环永远成不了
- 事务越短越好:BEGIN 到 COMMIT 别拖,别在事务里等人工确认、调外部接口
- 缩小锁范围:改哪几行动哪几行,别动不动全表 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 后自然继续,环永远成不了。顺序一致 = 死锁免疫。
七、常见误区
- 「死锁是数据库 bug」:不是,是并发拿锁顺序不当的逻辑问题;数据库的职责只是发现并止损
- 「被回滚说明数据坏了」:没有——回滚本身保证无中间残留(4-16 的功劳),重试即可
- 「小系统不会有死锁」:并发一起步就可能遇到;三招预防从小系统就该养成
自测三问
1. 死锁形成的条件?(两事务互持对方要的锁,环形等待) 2. 数据库怎么处理死锁?(检测到环后回滚一方,报错让程序重试) 3. 防死锁第一招?(所有事务统一加锁顺序)- ☑ 第五章 引入数据库-数据库的结构与表的设计 —— 已完结:确定数据类型、约束条件、属性(从需求到 DDL:实体→关系→E-R 图→三范式→配型命名,书评网站四连实战收官)
相关笔记
- 回滚-取消一组已经执行的处理 —— 上一节:牺牲方的善后
- 事务-集中执行无法分割的处理 —— 死锁的温床是隔离性 I
- 步骤的整理-引入系统的流程 —— 下一章起点:设计排在流程哪一环
- 确定数据类型、约束条件、属性-书评网站表格设计的示例4-排列表和列 —— 下一章收尾:本章约束在实战中的落位
- 约束、属性-限制允许保存的数据 —— 本章两路防线的起点
- 🗄️ 数据库基础 · 索引 —— 全课路线图