MySQL 死锁总复发?用螺旋事务相位互逆法定位加锁顺序(附 Python 诊断脚本)

你一定见过这种报警:

复制代码
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 辅助生成。


相关推荐
m0_646429971 小时前
MySQL 初始化 SQL 中文乱码问题总结
数据库·sql·mysql
实战派K8S&DB1 小时前
TDSQL 核心模块与进程体系
运维·数据库·分布式·sql·mysql
Lost_the_wind1 小时前
MySQL 学习笔记三
笔记·学习·mysql
腾科IT教育2 小时前
Oracle OCP和MySQL OCP怎么选?方向对比指南
mysql·oracle·ocp·oracle ocp·ocp怎么选·mysql ocp
用户460381291944 小时前
别人都 commit 了,为什么我的 select 还是旧值?图解 MySQL MVCC
mysql
毕业设计7036 小时前
(免费领源码)django空气净化器销售系统09218- java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
python·scrapy·mysql·pycharm·django·flask·pandas
CJi0NG7 小时前
【自用】MySQL-事务
数据库·mysql
VX_bysjlw9857 小时前
数码设备销售网站设计与实现39138-计算机毕设原创(免费领源码+带部署教程)
java·vue.js·spring boot·mysql·tomcat·mybatis·idea
遇码8 小时前
system prompt 里拼了时间和知识大纲,每轮缓存都被击穿:静态/动态分离的提示词装配方法
人工智能·ai·大模型
PHP实战开发录8 小时前
MySQL数字排序为什么乱
数据库·mysql·php