解决php mysql事务死锁的核心做法是:缩短持有锁的时间、统一加锁顺序,并在代码中捕获异常进行重试。当发生deadlock时,mysql会回滚其中一个事务并抛出错误,php脚本应捕获该错误(通常是1213或40001),然后自动重新执行整个事务逻辑,直到成功或达到最大重试次数。

如何识别和捕获死锁异常
在PHP中,死锁表现为PDOException或mysqli_sql_exception,错误码为1213 (ER_LOCK_DEADLOCK)。你需要在try-catch块中包裹事务逻辑,专门检查这个错误码。一旦捕获到,不要直接终止程序,而是进入重试机制。这是处理并发冲突最标准且有效的第一步。

$maxRetries = 3;
$retryCount = 0;
do {
try {
$pdo->beginTransaction();
// 执行数据库操作...
$pdo->commit();
break; // 成功则跳出循环
} catch (\Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
if ($e->getCode() == '1213' && $retryCount < $maxRetries) {
$retryCount++;
usleep(rand(1000, 5000)); // 随机等待避免再次冲突
continue; // 重试
}
throw $e; // 非死锁错误或超过重试次数,抛出异常
}
} while (true);
上述代码展示了基本的重试结构。关键点在于:beginTransaction()必须在循环内或每次重试前调用,确保状态重置。usleep引入微小随机延迟,防止多个进程同时重试导致“活锁”。如果错误码不是1213,或者重试次数耗尽,则视为严重错误向上抛出。
立即学习“PHP免费学习笔记(深入)”;
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
如何通过统一加锁顺序减少死锁概率
死锁的根本原因是两个或多个事务以不同顺序获取资源锁。例如,事务A先锁行1再锁行2,事务B先锁行2再锁行1。解决方法是强制所有事务按相同的逻辑顺序(如主键ID升序)访问数据。这样即使发生并发,也不会形成环路等待。

// 假设更新两个账户余额
$ids = [101, 102];
sort($ids); // 关键:统一排序
foreach ($ids as $id) {
$stmt = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ? FOR UPDATE");
$stmt->execute([10, $id]);
}
在上面的示例中,无论业务逻辑原本如何处理这两个ID,代码都先对ID数组进行排序。这确保了所有并发事务都以相同的顺序获取FOR UPDATE锁,从而消除因锁顺序不一致导致的死锁风险。这种方法比依赖重试更主动,能显著降低系统负载。
基于SELECT ... FOR UPDATE的具体问题与优化
在使用SELECT ... FOR UPDATE进行读锁定以准备更新时,如果查询范围过大或索引失效,会导致间隙锁(Gap Lock)或Next-Key Lock,进而引发大量死锁。具体问题是:当查询条件无法有效利用索引时,MySQL会锁定更多行甚至整个表区间,增加冲突概率。
优化方法是确保查询条件有索引支持,并尽量缩小锁定范围。例如,避免使用LIKE '%...'或函数转换字段作为条件。如果必须扫描多行,考虑分批处理或使用乐观锁(版本号机制)替代悲观锁。此外,检查执行计划(EXPLAIN)确认是否使用了索引,这是排查此类死锁的关键步骤。


















