本文目录导读:

我来详细讲解PHP中悲观锁死锁的检测和处理方法。
什么是悲观锁死锁
基本概念
// 悲观锁:假设会发生冲突,在整个数据处理过程中将数据锁定 // 死锁:两个或多个事务互相等待对方释放锁,导致永久阻塞
死锁示例场景
// 事务A: 锁定资源1,等待资源2 // 事务B: 锁定资源2,等待资源1 // 结果:互相等待,形成死锁
数据库层面的死锁检测
MySQL死锁检测配置
// 开启死锁检测(默认开启) SET GLOBAL innodb_deadlock_detect = ON; // 设置锁等待超时时间(秒) SET GLOBAL innodb_lock_wait_timeout = 5;
死锁日志查看
-- 查看最近的死锁信息 SHOW ENGINE INNODB STATUS\G; -- 查看当前事务 SELECT * FROM information_schema.INNODB_TRX; -- 查看锁信息 SELECT * FROM information_schema.INNODB_LOCKS;
PHP实现死锁检测
1 基础死锁检测类
<?php
class DeadlockDetector
{
private $pdo;
private $maxRetry = 3;
private $retryDelay = 100; // 毫秒
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 执行带死锁检测的事务
*/
public function executeWithDeadlockDetection(callable $callback)
{
$attempt = 0;
while ($attempt < $this->maxRetry) {
try {
$this->pdo->beginTransaction();
$result = $callback($this->pdo);
$this->pdo->commit();
return $result;
} catch (PDOException $e) {
$this->pdo->rollBack();
// 检查是否是死锁错误
if ($this->isDeadlockError($e)) {
$attempt++;
echo "检测到死锁,重试第 {$attempt} 次\n";
if ($attempt >= $this->maxRetry) {
throw new Exception('多次重试仍无法解决死锁问题');
}
// 随机延时,避免再次形成死锁
usleep($this->retryDelay * 1000 * random_int(1, 3));
continue;
}
throw $e;
}
}
}
/**
* 判断是否死锁错误
*/
private function isDeadlockError(PDOException $e)
{
// MySQL错误码:1213 死锁,1205 锁等待超时
$deadlockCodes = ['1213', '1205', '40001'];
$sqlState = $e->getCode();
$message = $e->getMessage();
// 检查错误码
foreach ($deadlockCodes as $code) {
if ($sqlState == $code || strpos($message, $code) !== false) {
return true;
}
}
// 检查错误信息
return (strpos($message, 'Deadlock found') !== false ||
strpos($message, 'Lock wait timeout') !== false);
}
}
2 乐观锁与死锁检测对比
<?php
/**
* 乐观锁实现(解决并发冲突的另一种方式)
*/
class OptimisticLockHandler
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 使用版本号实现乐观锁
*/
public function updateWithVersion($table, $id, $data)
{
$sql = "UPDATE {$table} SET
data = :data,
version = version + 1
WHERE id = :id AND version = :version";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':data' => json_encode($data),
':id' => $id,
':version' => $data['version']
]);
if ($stmt->rowCount() === 0) {
throw new Exception('数据已被其他用户修改,请刷新后重试');
}
}
}
3 死锁预防策略
<?php
class DeadlockPrevention
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 统一的锁获取顺序
*/
public function lockMultipleResources(array $resources)
{
// 排序,保证所有事务按相同顺序获取锁
sort($resources);
$this->pdo->beginTransaction();
foreach ($resources as $resource) {
$sql = "SELECT * FROM resources
WHERE resource_id = :id
FOR UPDATE";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $resource]);
if ($stmt->rowCount() === 0) {
$this->pdo->rollBack();
throw new Exception("资源 {$resource} 不存在");
}
}
}
/**
* 设置合理的锁等待时间
*/
public function setLockTimeout($seconds)
{
$this->pdo->exec("SET innodb_lock_wait_timeout = {$seconds}");
}
/**
* 使用GET_LOCK实现应用级锁
*/
public function acquireAppLock($lockName, $timeout = 5)
{
$sql = "SELECT GET_LOCK(:lock_name, :timeout)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':lock_name' => $lockName,
':timeout' => $timeout
]);
return (bool)$stmt->fetchColumn();
}
/**
* 释放应用级锁
*/
public function releaseAppLock($lockName)
{
$sql = "SELECT RELEASE_LOCK(:lock_name)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':lock_name' => $lockName]);
}
}
4 实时死锁监控类
<?php
class DeadlockMonitor
{
private $pdo;
private $logFile;
public function __construct(PDO $pdo, $logFile = '/tmp/deadlock.log')
{
$this->pdo = $pdo;
$this->logFile = $logFile;
}
/**
* 主动检测当前死锁状态
*/
public function detectActiveDeadlocks()
{
// 查询当前有等待的事务
$sql = "
SELECT
trx_id,
trx_state,
trx_started,
trx_wait_started,
trx_mysql_thread_id,
trx_query
FROM information_schema.INNODB_TRX
WHERE trx_state = 'LOCK WAIT'
";
$stmt = $this->pdo->query($sql);
$waitingTransactions = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($waitingTransactions) > 0) {
$this->logDeadlockSituation($waitingTransactions);
return $this->analyzeDeadlockPotential($waitingTransactions);
}
return false;
}
/**
* 分析死锁可能性
*/
private function analyzeDeadlockPotential($transactions)
{
// 简化分析:如果多个事务都在等待,很可能发生死锁
$waitMap = [];
foreach ($transactions as $trx) {
$waitMap[$trx['trx_mysql_thread_id']] = [
'query' => $trx['trx_query'],
'started' => $trx['trx_started']
];
}
// 如果等待关系形成环,则存在死锁
return $this->hasCircularWait($waitMap);
}
/**
* 检查是否有循环等待
*/
private function hasCircularWait($waitMap)
{
// 简化实现,实际需要构建等待图并检测环
// 这里可以根据具体业务逻辑实现更复杂的检测
return count($waitMap) > 1;
}
/**
* 记录死锁日志
*/
private function logDeadlockSituation($transactions)
{
$logData = [
'time' => date('Y-m-d H:i:s'),
'transactions' => $transactions
];
file_put_contents(
$this->logFile,
json_encode($logData, JSON_PRETTY_PRINT) . "\n",
FILE_APPEND
);
}
/**
* 获取数据库死锁状态
*/
public function getInnoDBStatus()
{
$stmt = $this->pdo->query("SHOW ENGINE INNODB STATUS");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// 解析死锁部分
if (preg_match('/LATEST DETECTED DEADLOCK(.*?)----------------------/s',
$result['Status'], $matches)) {
return $matches[1];
}
return null;
}
}
5 完整使用示例
<?php
// 数据库连接
$pdo = new PDO(
'mysql:host=localhost;dbname=test;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5 // 连接超时
]
);
// 死锁检测器
$detector = new DeadlockDetector($pdo);
try {
// 执行带死锁检测的业务操作
$result = $detector->executeWithDeadlockDetection(function ($pdo) {
// 业务逻辑
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
return true;
});
echo "操作成功";
} catch (Exception $e) {
echo "操作失败: " . $e->getMessage();
}
// 死锁监控
$monitor = new DeadlockMonitor($pdo);
$hasDeadlock = $monitor->detectActiveDeadlocks();
if ($hasDeadlock) {
// 处理死锁情况
echo "检测到可能的死锁";
}
// 查看数据库死锁状态
$status = $monitor->getInnoDBStatus();
if ($status) {
echo "最近死锁详情:\n" . $status;
}
死锁处理的最佳实践
1 预防措施
// 1. 保持事务简短
$pdo->beginTransaction();
// 快速执行必要的操作
$pdo->commit();
// 2. 按固定顺序访问资源
function updateUsers($pdo, $userId1, $userId2) {
$ids = [$userId1, $userId2];
sort($ids); // 保证顺序
$pdo->beginTransaction();
foreach ($ids as $id) {
$pdo->exec("SELECT * FROM users WHERE id = {$id} FOR UPDATE");
}
// 继续业务逻辑
$pdo->commit();
}
// 3. 使用索引减少锁的范围
CREATE INDEX idx_user_id ON accounts(user_id) FOR UPDATE;
2 监控和告警
// 定时任务脚本
while (true) {
$monitor = new DeadlockMonitor($pdo);
if ($monitor->detectActiveDeadlocks()) {
// 发送告警
sendAlert('检测到数据库死锁');
// 记录日志
logDeadlock($monitor->getInnoDBStatus());
}
sleep(30); // 每30秒检查一次
}
推荐的死锁处理策略
- 设置合理的超时:避免无限等待
- 使用重试机制:自动重试失败的事务
- 死锁监控:实时监控并告警
- 优化查询:减少锁的范围和持有时间
- 统一锁顺序:避免循环等待
- 使用隔离级别:根据需求选择合适的隔离级别
通过以上方法,可以有效检测和处理PHP中的悲观锁死锁问题。