必须显式捕获错误码1213(sqlstate '40001'),而非依赖异常消息;重试需包裹整个事务、配合指数退避与连接重置,并结合索引优化、统一加锁顺序及死锁日志分析根治。

死锁错误码1213必须显式捕获
MySQL 报出 Deadlock found when trying to get lock 时,底层错误码固定是 1213(SQLSTATE HY000),但 PDO 默认不抛异常,mysqli 也常返回 false 而非异常。不检查这个码,重试逻辑就形同虚设。
实操建议:
-
PDO必须设置PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,否则execute()失败静默失败 -
mysqli推荐用mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)开启异常模式 - 只判断异常消息字符串(如包含
"Deadlock")不可靠——不同 MySQL 版本、语言环境可能变 - 重试前必须调用
beginTransaction(),不能在已中断的事务里继续执行
事务内访问多表必须统一顺序
死锁最常见诱因不是并发高,而是两个事务以相反顺序更新同一组表。比如一个先 UPDATE users 再 UPDATE orders,另一个反过来,InnoDB 检测到循环等待后立即回滚其一。
实操建议:
- 所有业务模块约定全局操作顺序,例如:始终按
user → order → item → log的路径加锁 - 避免在事务中动态拼接表名或条件——顺序可能随参数变化而错乱
- 用
EXPLAIN FORMAT=TRADITIONAL确认UPDATE是否走索引;没走索引会升级为间隙锁甚至表锁,扩大冲突面 - 即使只操作一张表,也可能死锁:比如事务A用
WHERE name = ?更新,事务B用WHERE id = ?更新同一行——前者先锁二级索引再锁主键,后者反之,顺序冲突
SHOW ENGINE INNODB STATUS 是唯一可信现场证据
PHP 日志里看到 1213 错误,只说明“发生了”,但不知道谁在等谁、锁了什么范围、为什么等。真正定位必须看 MySQL 自身的死锁快照。
实操建议:
- 执行
SHOW ENGINE INNODB STATUS\G,重点找LATEST DETECTED DEADLOCK块 - 里面会明确写出两个事务的 SQL、持有锁类型(
RECORD LOCKS/GAP LOCKS)、等待的索引、甚至锁住的具体主键值 - 不要依赖监控工具自动解析——很多工具会丢掉关键字段(如
lock_mode X locks rec but not gap waiting) - 线上环境建议开启
innodb_print_all_deadlocks = ON,把每次死锁写入错误日志,避免手动抓取遗漏
重试不能简单 sleep 固定毫秒
并发下所有进程同时休眠 50ms 后重试,等于集体撞墙,第二次大概率再死锁。指数退避不是优化技巧,是避免重试风暴的必要设计。
实操建议:
- 第1次重试前
usleep(50000)(50ms),第2次usleep(100000)(100ms),第3次usleep(200000)(200ms) - 重试上限严格控制在 ≤ 3 次;超过说明根本问题没解决,比如索引缺失、事务粒度太大、或业务逻辑本身存在资源竞争路径
- 禁止在事务内做任何非数据库操作:HTTP 请求、
file_get_contents、sleep()都会拉长锁持有时间,把“瞬间冲突”变成“稳定死锁” - 如果重试后仍失败,应记录完整上下文(SQL、参数、trace_id)并告警,而不是静默丢弃或转给前端重试
EXPLAIN 和 INFORMATION_SCHEMA.STATISTICS 查清索引使用路径,否则永远在修表面。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











