mysql 5.7存储过程不适合死锁重试,因其无法精准捕获error 1213、事务回滚后不能安全重启、重试会延长锁持有时间并加剧死锁,且缺乏错误码细分与动态退避能力;可靠方案必须由应用层控制显式事务与重试逻辑。

MySQL 5.7 存储过程中无法原生实现死锁重试逻辑,必须靠应用层兜底;存储过程里硬写循环重试不仅不可靠,还会放大锁持有时间、加剧死锁概率。
为什么存储过程不适合做死锁重试
MySQL 5.7 的存储过程缺乏对 ERROR 1213(Deadlock found when trying to get lock)的细粒度异常捕获能力。虽然能用 DECLARE HANDLER FOR SQLEXCEPTION 捕获异常,但无法区分是死锁、超时还是其他 SQL 错误;且一旦进入异常处理分支,当前事务已回滚,START TRANSACTION 无法在存储过程内安全重启——InnoDB 不允许在已回滚事务中隐式开启新事务。
更关键的是:存储过程执行期间若加了行锁,重试逻辑会反复尝试获取相同锁资源,相当于把两个并发事务的“抢锁竞争”拖进同一个会话里,反而延长了锁等待链。
- 存储过程无事务上下文隔离,重试时容易误复用旧变量或临时表状态
-
GET DIAGNOSTICS在 5.7 中不支持获取错误码细分(如区分 1205/1213),只能靠字符串匹配Deadlock,脆弱易错 - 重试次数和退避策略(如指数退避)无法动态配置,写死在过程里就失去灵活性
真正可行的重试位置:应用层 + 显式事务控制
死锁重试必须发生在应用代码中,由业务逻辑决定何时重试、重试几次、是否降级。MySQL 只负责抛出 ERROR 1213,应用收到后主动 ROLLBACK 再 BEGIN 新事务。
以 Java JDBC 为例,核心逻辑是:
try {
conn.setAutoCommit(false);
// 执行含 UPDATE/INSERT ... ON DUPLICATE KEY UPDATE 的语句
stmt.executeUpdate("INSERT INTO song_rank(songId,weight) VALUES(?,?) ON DUPLICATE KEY UPDATE weight=weight+1");
conn.commit();
} catch (SQLException e) {
if ("40001".equals(e.getSQLState()) && e.getMessage().contains("Deadlock")) {
// 记录日志,触发重试(最多 3 次)
retryCount++;
if (retryCount
- 务必在
catch块中显式调用conn.rollback(),否则连接可能处于未定义状态 - 避免在重试中复用 PreparedStatement 对象,防止参数残留或状态污染
- 重试间隔不能太短(如 10ms),否则高并发下所有线程几乎同时重试,再次撞上死锁
存储过程能做的配合优化(非重试,但减死锁)
如果坚持要用存储过程封装业务逻辑,它唯一该做的事是「降低死锁发生概率」,而不是「处理死锁」。重点在锁行为收敛:
- 所有 DML 操作按主键或唯一索引顺序执行,例如传入 ID 列表时先
ORDER BY再遍历更新,避免锁序不一致 - 避免在过程中执行无索引的
WHERE条件(如status = 0无索引),防止间隙锁扩散成 Next-Key Lock 覆盖大片范围 - 用
SELECT ... FOR UPDATE时,只查必要字段,且尽量使用覆盖索引,减少锁住的记录数 - 不在存储过程中调用其他可能加锁的存储过程,防止锁嵌套加深
比如这个高危写法:
UPDATE order SET status = 1 WHERE user_id = ? AND created_time > '2026-09-29'; -- created_time 无索引 → 全表扫描加锁
应改为先查出主键再更新:
SELECT id FROM order WHERE user_id = ? AND created_time > '2026-09-29' ORDER BY id; -- 走 user_id 索引 UPDATE order SET status = 1 WHERE id IN (?, ?, ?); -- 精确行锁
最容易被忽略的点:ON DUPLICATE KEY UPDATE 的锁行为
这是 MySQL 5.7 并发插入死锁的头号诱因。它不是简单的“有就更新、没就插入”,而是先尝试插入,失败时再做 UPDATE —— 这个过程会在唯一键冲突的记录上加 X 锁,且在 RR 隔离级别下还会对冲突键值的间隙加 Gap Lock。
当两个事务并发执行:
- 事务 A 插入
songId=16→ 加记录锁 - 事务 B 插入
songId=17→ 加记录锁 - 事务 A 接着插
songId=17→ 等待 B 的锁 - 事务 B 接着插
songId=16→ 等待 A 的锁 → 死锁
解决方法只有两个:一是应用层重试(必须);二是改用 INSERT IGNORE + 单独 UPDATE(但要自己处理竞态),或者直接在应用里先 SELECT 判断是否存在,再决定走 INSERT 还是 UPDATE —— 虽然多一次查询,但锁行为完全可控。











