结论先行:
innodb_deadlock_detect默认是 ON,它让 InnoDB 在每次锁等待时都主动去「找环」,代价是检测本身的 CPU;在高并发、锁等待者多的场景里,这个「找环」会反过来成为最大的 CPU 消耗者。此时把检测关掉、改靠innodb_lock_wait_timeout超时回滚,是把「O(N²) 的主动检测」换成「O(1) 的超时兜底」的取舍——代价是死锁不再被即时发现,而是拖到超时才以1205报出来。
一、现象:死锁检测本身成了 CPU 杀手
去年给一套库存扣减服务做性能压测。核心表 t_stock(8000 万行、单表 19GB、InnoDB、MySQL 8.0.32、16C64G),业务路径是「同一 SKU 的高并发扣减」:
UPDATE t_stock SET qty = qty - ?, version = version + 1
WHERE sku_id = ? AND qty >= ?;
压测脚本 sysbench 64 线程,单 SKU 热点集中在 200 个 sku_id 上。现象:
- 32 线程:CPU 45%,P99 = 120ms,无
1213(死锁)告警; - 64 线程:CPU 从 45% 直接顶到 97%,P99 从 120ms 飙到 3.2s;
SHOW ENGINE INNODB STATUS里TRANSACTIONS段稳定出现 50+ 个LOCK WAIT,但LATEST DETECTED DEADLOCK段几乎不更新——也就是说,CPU 烧在「检测」上,而不是真发生了多少死锁。
用 perf top 看 CPU 热点,lock_wait_suspend_thread → trx_check_deadlock 这条链占了 38%——死锁检测本身就是最大的 CPU 消费者。
二、原理:ON 时每把锁都要扫一遍等待图
innodb_deadlock_detect 控制 InnoDB 是否在锁等待时主动找环:
| 配置 | 行为 | 复杂度 |
|------|------|--------|
| ON(默认) | 每个拿不到锁的事务进入等待,InnoDB 遍历当前 lock wait 图找环 | 等待者 N 个时,新增等待者的检测成本随 N 增长,整体近似 O(N²) |
| OFF | 不主动找环,事务等到 innodb_lock_wait_timeout(默认 50s)到期,回滚等待时间最长的那个 | 检测成本 O(1),代价是延迟 = 超时时间 |
关键点:
- 检测成本是「每把锁都要付一次」。N 个事务排队争同一把行锁,第 N 个进来时要把前 N-1 个都扫一遍找环——热点行、批量
UPDATE、大批量导入这类「等待者多」的场景,检测成本被放大得最狠。 - 关闭检测 ≠ 死锁消失。死锁该发生还是发生,只是 InnoDB 不再「当场抓现行」,而是让所有当事方都去排队,直到 50s 超时,回滚等待最久的那个,其余继续。死锁被「超时」兜底了,而不是被「消除」了。
- 报错码会变:检测开时死锁返回
1213 (ER_LOCK_DEADLOCK),检测关 + 超时时返回1205 (ER_LOCK_WAIT_TIMEOUT)。
三、具体做法:什么时候关、怎么关、怎么补
适用判断(满足任一才考虑关):
- 监控里
trx_check_deadlock链在perf top占比 > 20%; innodb_trx稳定有 50+ 个LOCK WAIT,但死锁告警稀疏;- 是「热点行 / 批量写」这类等待者天然多的场景。
不适用:低并发、等待者少(<10)的库——检测成本本来就低,关掉反而丢了「即时发现死锁」的能力。
怎么关(MySQL 8.0.32,可在线改、立即生效,无需重启):
-- 看当前值
SHOW VARIABLES LIKE 'innodb_deadlock_detect'; -- ON
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 50
-- 关掉检测,并把超时从 50s 压到 5s(别用默认 50s,否则上游早超时了)
SET GLOBAL innodb_deadlock_detect = OFF;
SET GLOBAL innodb_lock_wait_timeout = 5;
注意:
innodb_lock_wait_timeout是会话级,SET GLOBAL只影响新连接,已有连接要SET SESSION或在连接池初始化里带上。
怎么补监控(关了之后死锁不主动报了,得自己盯):
-- 每 30s 采一次:等待者数 + 最长等待时间
SELECT
COUNT(*) AS waiters,
MAX(TIMESTAMPDIFF(SECOND, trx_started, NOW())) AS max_wait_s
FROM information_schema.innodb_trx
WHERE trx_state = 'LOCK WAIT';
告警规则:waiters > 50 持续 2 个周期 → P2;max_wait_s > 3 → P3。同时保留 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段做事后取证(关了检测后它不再实时更新,但最后一次抓到的死锁记录还在,可回溯)。
四、坑 / 反直觉
- 「1213 没了」≠「死锁治好了」。最典型的误判:关完检测,应用侧
1213归零,值班同学据此写周报「死锁问题已解决」。实际是「检测被关了」,死锁照常发生,只是改口报1205。判定死锁是否真消失,要看业务重试率与回滚量,而不是看错误码。 1205默认 50s 才返回,上游早炸了。关检测但没压innodb_lock_wait_timeout,等于把「毫秒级死锁回滚」换成「50 秒超时」,连接池在 50s 里被LOCK WAIT占满,比开检测时还惨。- 关检测后回滚的是「等最久的」,不是「undo 最小的」。开检测时 InnoDB 回滚 undo 较小的一方(损失最小);关检测走超时后回滚等待最久者,不一定是最小代价,可能在热点行上误伤了一个本可以很快拿到锁的大事务。
LATEST DETECTED DEADLOCK段会「冻结」。关了之后这段不再更新,排障时容易误以为「最近没死锁」,其实只是「最近没被检测抓到」。
五、结论与可复用清单
一句话取舍:innodb_deadlock_detect 是「用 CPU 换即时性」的开关——并发低、等待者少时它是免费的保险,该开着;高并发、热点行、批量写这类等待者多的场景,它是 O(N²) 的 CPU 税,该关掉、用「压到 5s 的超时回滚 + 等待者数监控」兜底。
可复用清单:
- 压测先看
perf top:trx_check_deadlock占比 > 20% 才考虑关检测,别凭感觉关。 - 关检测必须同步压
innodb_lock_wait_timeout(5s 量级),且记得它是会话级、要落到连接池初始化。 - 关检测后补两条监控:
innodb_trx里LOCK WAIT计数 + 最长等待时间;保留LATEST DETECTED DEADLOCK做事后取证。 - 判定死锁是否真消失,看业务重试率 / 回滚量,别只看
1213错误码。 - 低并发库(等待者 < 10)不要关检测——检测成本本来就低,关掉只亏「即时发现」的能力。
与其他锁治理手段的关系:死锁检测开关解决的是「检测的 CPU 税」,不解决「为什么会死锁」。真正的死锁来源还是锁顺序不一致、隔离级别下的 gap lock、以及表级锁导致的长排队——先治死锁的成因,再谈要不要关检测,顺序反了只是把「死锁」换成了「超时」,问题一个没少。