php网站开发过程中,数据库死锁是常见的高并发场景问题,指两个或多个事务在同一资源上相互等待对方释放锁,导致所有事务都无法继续执行的情况,会直接造成接口响应超时、数据写入失败等问题,需要从预防和解决两个维度处理。

数据库死锁的常见产生原因
死锁的出现通常和事务操作逻辑、资源竞争有关,php网站中常见的诱因有以下几类:
- 事务执行顺序不一致,多个事务交叉锁定相同资源,比如事务A先锁表1再锁表2,事务B先锁表2再锁表1,就容易形成互相等待
- 未合理使用索引,导致锁范围扩大,比如update语句没有命中索引会触发表锁或者大范围行锁,增加资源竞争概率
- 事务持有锁的时间过长,比如事务中包含大量业务逻辑、外部接口调用,导致锁长时间不释放,提升死锁发生可能性
- 高并发场景下无限制的并发写操作,大量请求同时竞争同一批数据资源,超出数据库的锁调度能力
php网站预防数据库死锁的方法
1. 优化事务设计与执行逻辑
首先尽量缩短事务的执行时间,把非数据库操作的逻辑放在事务外部,避免事务长时间持有锁。其次统一事务中资源的访问顺序,所有事务按照相同的顺序锁定表或者行,从根源上避免交叉等待的情况。
以下是一个规范的事务操作示例:
<?php
// 假设使用PDO连接数据库
$db = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'root', 'password');
$db->beginTransaction();
try {
// 统一先操作user表,再操作order表,所有事务都遵循这个顺序
$stmt1 = $db->prepare('UPDATE user SET balance = balance - 100 WHERE id = 1');
$stmt1->execute();
// 非数据库逻辑放在事务外,这里仅为示例,实际不要放在事务内
// $result = call_external_api();
$stmt2 = $db->prepare('UPDATE order SET status = 1 WHERE user_id = 1');
$stmt2->execute();
$db->commit();
} catch (Exception $e) {
$db->rollBack();
echo '操作失败:' . $e->getMessage();
}
?>2. 优化索引设计减少锁范围
确保所有update、delete等写操作的where条件都能命中有效索引,避免触发全表扫描导致的大范围锁。可以通过EXPLAIN命令分析sql的执行计划,检查是否使用了合适的索引。
比如下面的user表如果id是主键,以下sql只会锁定id=1这一行,锁范围极小:
-- 命中主键索引,行锁范围小 UPDATE user SET balance = balance - 100 WHERE id = 1; -- 未命中索引,可能触发表锁或者大量行锁 UPDATE user SET balance = balance - 100 WHERE name = 'test';
3. 控制并发访问强度
对于高并发的写接口,可以在php层面使用队列或者限流机制,把并发写请求改成串行或者小批量处理,减少同一时间对相同资源的竞争。也可以使用数据库的乐观锁机制,通过版本号控制更新,避免加锁等待。
乐观锁的实现示例如下:
<?php
$db = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'root', 'password');
// 先查询当前数据的版本号
$stmt = $db->prepare('SELECT balance, version FROM user WHERE id = 1');
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$oldVersion = $row['version'];
$newBalance = $row['balance'] - 100;
// 更新时校验版本号,版本一致才更新,同时版本号加1
$updateStmt = $db->prepare('UPDATE user SET balance = ?, version = version + 1 WHERE id = 1 AND version = ?');
$updateStmt->execute([$newBalance, $oldVersion]);
// 判断受影响行数,如果为0说明版本已被修改,需要重试或者提示
if ($updateStmt->rowCount() == 0) {
echo '数据已被修改,请重试';
}
?>死锁发生后的解决步骤
如果已经出现死锁,首先可以通过数据库的命令查看死锁日志,比如mysql可以执行SHOW ENGINE INNODB STATUS\G查看最近的死锁信息,定位死锁涉及的事务和sql。
然后可以临时终止阻塞的事务,释放锁资源,再排查对应的代码逻辑,按照预防方法优化事务顺序、索引、锁范围等。如果是偶发的死锁,也可以在php代码中增加重试机制,当捕获到死锁异常时,等待一小段时间后重新执行事务,通常重试1-3次即可成功。
以下是带重试机制的事务示例:
<?php
$db = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'root', 'password');
$retryCount = 0;
$maxRetry = 3;
while ($retryCount < $maxRetry) {
$db->beginTransaction();
try {
$stmt1 = $db->prepare('UPDATE user SET balance = balance - 100 WHERE id = 1');
$stmt1->execute();
$stmt2 = $db->prepare('UPDATE order SET status = 1 WHERE user_id = 1');
$stmt2->execute();
$db->commit();
echo '操作成功';
break;
} catch (Exception $e) {
$db->rollBack();
// 判断是否为死锁异常,不同数据库异常码不同,mysql死锁异常码为1213
if (strpos($e->getMessage(), '1213') !== false && $retryCount < $maxRetry - 1) {
$retryCount++;
usleep(100000); // 等待100毫秒后重试
continue;
}
echo '操作失败:' . $e->getMessage();
break;
}
}
?>额外注意事项
不要在事务中执行用户交互操作或者长时间等待的逻辑,比如sleep、等待用户输入等,这些都会延长锁的持有时间。同时定期监控数据库的锁等待情况,提前发现潜在的死锁风险,及时调整业务逻辑或者数据库结构。