死锁是事务竞争资源的必然现象,需通过捕获错误码1213后重试、统一sql执行顺序、缩小事务粒度、索引优化及分析innodb状态日志来应对。

死锁不是代码写错了,是事务竞争资源的必然现象
PHP 应用在高并发下出现数据库死锁,本质不是 PHP 本身的问题,而是多个事务同时申请互斥资源(如行锁、间隙锁)且顺序不一致导致的循环等待。MySQL 会自动检测并回滚其中一个事务(报错 Deadlock found when trying to get lock),但如果不处理,用户请求就会失败。关键不是“避免所有死锁”,而是让应用能稳定重试、快速恢复。
捕获并重试死锁异常必须用 try-catch 包裹事务
MySQL 的死锁错误码是 1213(HY000 SQLSTATE),PDO 和 mysqli 都能抛出异常或返回错误信息,但必须显式检查。只靠前端重试或 Nginx 重发是无效的——事务已中断,连接状态不可复用。
- PDO 示例中需开启
ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,否则execute()失败不会抛异常 - 重试次数建议 ≤ 3 次,超过说明逻辑或索引设计有根本问题
- 每次重试前应重新开始事务(
beginTransaction()),不能在原事务里继续 - 避免在重试逻辑里 sleep 固定时间,可用指数退避(如 50ms → 100ms → 200ms)减少再次碰撞
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE orders SET status = ? WHERE id = ?")->execute(['paid', $id]);
$pdo->prepare("INSERT INTO payments (...) VALUES (...)")->execute([...]);
$pdo->commit();
} catch (PDOException $e) {
if ($e->getCode() == '1213') {
// 死锁,重试
usleep(50000);
continue;
}
throw $e;
}
减少死锁概率的关键是统一访问顺序和缩小事务粒度
两个事务如果都按「先更新用户余额,再插入订单」的顺序执行,就不会死锁;但如果一个按「用户→订单」、另一个按「订单→用户」,就极易触发。另外,事务越长,持有锁的时间越久,冲突窗口越大。
- 所有涉及多表更新/插入的业务,必须约定严格的表操作顺序(例如:user → order → item → log)
- 避免在事务内做 HTTP 请求、文件读写、慢查询等阻塞操作
- SELECT … FOR UPDATE 或 UPDATE 语句尽量走索引,否则可能升级为表锁或锁住更大范围(如间隙锁)
- 用
EXPLAIN确认 UPDATE/SELECT 的执行计划是否命中索引,尤其注意 WHERE 条件字段是否有联合索引覆盖
监控和定位要靠 MySQL 自身日志,不是靠 PHP 日志
PHP 层只能记录“发生了死锁”,但无法知道哪两条 SQL 在争抢什么资源。真正排查必须看 MySQL 的 InnoDB 状态:
立即学习“PHP免费学习笔记(深入)”;
- 执行
SHOW ENGINE INNODB STATUS\G,重点关注LATEST DETECTED DEADLOCK区块,它会显示两个事务各自的 SQL、持有的锁、等待的锁 - 开启
innodb_print_all_deadlocks = ON(MySQL 5.6.2+),把每次死锁写入 error log,便于批量分析 - 不要依赖
SHOW PROCESSLIST抓现场——死锁发生太快,很难命中 - 如果死锁频繁出现在某张表,检查该表是否有缺失的索引,或是否存在
UPDATE ... WHERE non_indexed_column = ?这类全表扫描加锁操作











