你一定见过这种报警:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
复盘的时候大家常说:
- "并发太高了"
- "加个重试就行"
- "把事务写小一点"
但下周死锁又来了。
用螺旋生成论的话,结论很直接:
死锁不是"并发问题",而是事务加锁相位互逆,两个事务各自拿着对方想要的锁,在相位空间里转成闭环。
1. 先造一个最小死锁场景
两张表:
CREATE TABLE account (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
balance INT NOT NULL,
INDEX idx_user (user_id)
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 101, 1000), (2, 102, 1000);
事务 A:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 稍等
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
事务 B:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 2;
-- 稍等
UPDATE account SET balance = balance + 100 WHERE id = 1;
COMMIT;
A 持有 id=1 的行锁,等 id=2;
B 持有 id=2 的行锁,等 id=1。
这就是最经典的相位互逆。
2. 什么是"事务相位"
给每个事务定义一个加锁相位序列:
T_A: lock(1) -> wait(2)
T_B: lock(2) -> wait(1)
如果把锁对象看成圆周上的点:
- id=1 在相位 0°
- id=2 在相位 180°
那么:
- T_A 的加锁方向是
0° -> 180° - T_B 的加锁方向是
180° -> 0°
两条路径反向,迟早撞成环。
所以死锁的根因不是"锁多",而是:
多个事务对相同资源集合的加锁顺序不一致。
3. 用 Python 把锁等待画成相位图
不需要真的去扒 information_schema.INNODB_LOCKS(新版 MySQL 已迁移到 performance_schema),我们可以先把应用层事务抽象出来:
from collections import defaultdict
# txn: 事务号 -> 加锁顺序
txns = {
"T_A": [1, 2],
"T_B": [2, 1],
}
# 构造等待边:当前事务在等它下一个还没拿到的锁
wait_graph = defaultdict(list)
for txn, locks in txns.items():
for i in range(len(locks) - 1):
held = locks[i]
want = locks[i + 1]
wait_graph[txn].append((held, want))
# 检测互逆:如果存在 T1: a->b,T2: b->a
edges = set()
for txn, lst in wait_graph.items():
for held, want in lst:
edges.add((held, want))
def has_deadlock_phase(edges):
for a, b in edges:
if (b, a) in edges:
return True, (a, b)
return False, None
flag, pair = has_deadlock_phase(edges)
print("死锁相位互逆:", flag)
print("互逆锁对:", pair)
输出:
死锁相位互逆: True
互逆锁对: (1, 2)
只要出现 (a,b) 和 (b,a) 同时存在,就可以判定:加锁相位互逆,死锁闭环已形成。
4. 工程上怎么治
方案 1:统一加锁顺序(最稳)
所有事务都按 id 升序加锁:
-- 事务 A 和事务 B 都先锁 1,再锁 2
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
相位变成:
T_A: 0° -> 180°
T_B: 0° -> 180°
方向一致,环打不开。
方案 2:一次拿到所有锁
SELECT * FROM account WHERE id IN (1,2) FOR UPDATE;
先占住资源,再算业务,再更新。
方案 3:缩短锁窗口
- 事务里别调外部接口
- 别在事务里 sleep
- 别在事务里写日志/发消息
5. 螺旋视角的一句话总结
MySQL 死锁 = 事务加锁相位互逆。
排查死锁,不要只看"报错那一行",要看多个事务的加锁顺序是否形成反向相位。
统一加锁顺序,就是把所有事务的相位箭头掰到同一个方向。
下一篇写:K8s Pod 总被调度到同一批节点?用螺旋调度相位饱和法看 kube-scheduler 的打分偏置。
参考文献
- 张智明. 螺旋生成元:跨学科统一数学框架. Zenodo. DOI: 10.5281/zenodo.21555082
- 张智明. 螺旋计算. Zenodo. DOI: 10.5281/zenodo.21356615
- 张智明. 螺旋数原理. Zenodo. DOI: 10.5281/zenodo.20602099
本文由作者原创,部分代码由 AI 辅助生成。