PHP执行SQL死锁必然发生,需通过捕获错误码1213自动重试、统一多表更新顺序、确保索引匹配WHERE/JOIN条件、主动控制隔离级别四方面系统性防控。

PHP 执行 SQL 时死锁不是“会不会发生”的问题,而是“什么时候发生、由谁承担代价”的问题——MySQL 会自动选一个事务回滚,但你的 PHP 代码得接住这个错误并正确重试,同时从源头压低触发概率。
捕获死锁错误并自动重试
MySQL 报错码 1213 是死锁专属标识,必须在 PDO 或 MySQLi 异常路径中显式识别,不能靠通用异常兜底。
- 使用
PDO::ERRMODE_EXCEPTION,否则$pdo->errorCode()可能拿不到准确状态 - 重试前加随机微小延迟(如
usleep(rand(10000, 50000))),避免所有重试请求立刻撞在一起 - 限制最大重试次数(通常 ≤ 3),防止无限循环掩盖逻辑缺陷
- 不要在事务外重试——必须整个事务块重入,否则状态已污染
示例片段:
for ($i = 0; $i < 3; $i++) {
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE orders SET status = ? WHERE id = ?")->execute(['paid', $id]);
$pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?")->execute([$amount, $uid]);
$pdo->commit();
break;
} catch (PDOException $e) {
$pdo->rollback();
if ($e->getCode() === '1213') {
usleep(rand(10000, 50000));
continue;
}
throw $e;
}
}统一多表更新顺序
死锁高频场景是两个事务以相反顺序更新同一组表,比如 A→B 和 B→A。PHP 层必须硬编码顺序,不能依赖 SQL 写法或 ORM 默认行为。
立即学习“PHP免费学习笔记(深入)”;
- 按表名字母序约定(
accounts→orders→users),简单、无歧义、可 grep 审计 - 跨服务调用时,该顺序需同步到其他语言服务(如 Go/Java),靠文档或契约无法保证,必须代码级对齐
- 避免用业务语义排序(如“先主后子”),因为主子关系在退款、补偿等逆向流程中会反转
- ORM 如 Laravel Eloquent 的
updateOrCreate()或 Django 的get_or_create()隐含 SELECT+INSERT 两步,若没加锁且并发高,可能绕过你定的顺序
索引与 WHERE 条件必须严格匹配
死锁日志里反复出现某张表,90% 是因为 JOIN 或 WHERE 字段没走索引,导致 MySQL 锁了成百上千行,极大增加交叉持锁概率。
-
EXPLAIN显示type = ALL或type = index(全索引扫描)就是危险信号 - JOIN 条件字段(如
ON a.user_id = b.user_id)和 WHERE 等值条件(如WHERE b.status = 'paid')必须有联合索引,且满足最左前缀,例如INDEX idx_user_status (user_id, status) - 禁止在索引字段上做函数操作:
WHERE DATE(created_at) = '2026-05-19'会让索引失效,改用created_at >= '2026-05-19' AND created_at < '2026-05-20' - 隐式类型转换也会断索引:
user_id = '123'(字段是 INT)会触发全表扫描,必须确保传入类型一致
隔离级别与锁提示要主动控制
MySQL 默认 REPEATABLE READ 在大表 JOIN 场景下是死锁温床,因为 Gap Lock 会锁住插入间隙;但降级到 READ COMMITTED 不是开关一按就完事。
- PHP 中必须显式设置:
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"),连接池配置无效 - Spring Boot 的
@Transactional(isolation = Isolation.READ_COMMITTED)才真正起效,别信框架默认值 - 即使用了 RC,
SELECT ... FOR UPDATE仍会加行锁,如果 WHERE 条件没索引,照样锁一堆行 - 对单行确定性更新,优先用
UPDATE ... WHERE id = ?+ 唯一索引,比SELECT FOR UPDATE后再 UPDATE 更安全——后者中间有竞态窗口
真正难的不是写对这几行代码,而是让所有团队成员在每次加新表、改旧 SQL、引入新 SDK 时,都条件反射检查锁顺序、索引覆盖、隔离级别这三件事。漏掉任意一环,死锁就会在流量高峰准时报到。



















