重试机制本身不引发死锁,但无节制、无协调的重试会放大锁冲突,将偶发死锁演变为持续雪崩;关键在于让重试“守规矩”:检查事务状态、验证业务有效性、确认连接未卡在锁等待,并采用退避策略与锁范围优化。

重试机制本身不引发死锁,但无节制、无协调的重试会放大已有锁冲突,把偶发死锁变成持续雪崩。关键不是禁用重试,而是让重试“守规矩”。
为什么重试会让死锁变雪崩
应用层捕获到 ERROR 1213 (40001): Deadlock found when trying to get lock 后立即重试,看似合理,实则危险:
- 多个失败事务在毫秒级内几乎同时重试,复刻原始执行路径,大概率再次撞上相同锁顺序
- 若原事务因锁等待超时(
innodb_lock_wait_timeout)被中断而非死锁检测回滚,重试会叠加新事务,进一步挤占连接池和锁资源 - PHP-FPM 或 Java 应用未限制并发重试数,导致线程/进程数陡增,数据库负载翻倍,死锁频率呈指数上升
重试前必须做三件事
在 try-catch 捕获死锁异常后,不能直接 sleep(100) 然后重试。必须先确认环境是否允许安全重试:
- 检查当前事务是否已提交或回滚:PHP 中用
$pdo->inTransaction()判断;Java 中通过TransactionStatus.isCompleted()验证,避免对已终止事务二次 commit - 验证业务状态是否仍有效:例如订单创建重试前,先查
SELECT status FROM orders WHERE order_id = ?,防止重复下单 - 确认数据库连接未处于
LOCK WAIT状态:查询information_schema.INNODB_TRX,排除连接卡在锁等待中却还被复用的情况
带退避与顺序控制的重试实现
简单 for($i=0; $i 是最常见错误。正确做法是结合指数退避 + 锁顺序固化:
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
- 退避策略:首次重试延迟
50ms,第二次150ms,第三次400ms,避免重试请求扎堆 - 强制统一锁顺序:重试时对涉及的 ID 列表显式排序,例如 PHP 中用
sort($ids, SORT_NUMERIC),再拼WHERE id IN (1,3,7),确保所有重试都按相同物理顺序加锁 - 限制最大重试次数为
3,超过则记录告警并返回用户“操作繁忙,请稍后重试”,不继续消耗资源
示例(PHP PDO):
if ($e->getCode() === '40001') {
$backoff = [50, 150, 400];
for ($i = 0; $i doTransfer($targetIds);
} catch (PDOException $retryE) {
if ($retryE->getCode() !== '40001') throw $retryE;
}
}
throw new RuntimeException('Deadlock retry exhausted');
}
最容易被忽略的点:间隙锁与唯一索引
很多重试雪崩发生在范围查询或非唯一字段更新场景,比如 UPDATE inventory SET stock = stock - 1 WHERE sku LIKE 'A%'。这类语句在 RR 隔离级别下会持有大量间隙锁,重试只是反复申请同一片间隙,必然死锁。
解决方法不是改重试逻辑,而是从源头收缩锁范围:
- 把模糊匹配改成精确查询,配合唯一索引:例如用
sku_hash字段(MD5 值)替代LIKE,加UNIQUE KEY idx_sku_hash (sku_hash) - 若必须范围操作,拆成小批次:每次只处理 10 条,用
LIMIT 10+ORDER BY id固定扫描顺序,降低单次锁粒度 - 确认
innodb_deadlock_detect保持ON(默认),不要为了“减少开销”关掉它——死锁检测耗 CPU 远低于雪崩带来的连接池耗尽










