PHP 悲观锁死锁检测

wen PHP项目 2

本文目录导读:

PHP 悲观锁死锁检测

  1. 什么是悲观锁死锁
  2. 数据库层面的死锁检测
  3. PHP实现死锁检测
  4. 死锁处理的最佳实践
  5. 推荐的死锁处理策略

我来详细讲解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秒检查一次
}

推荐的死锁处理策略

  1. 设置合理的超时:避免无限等待
  2. 使用重试机制:自动重试失败的事务
  3. 死锁监控:实时监控并告警
  4. 优化查询:减少锁的范围和持有时间
  5. 统一锁顺序:避免循环等待
  6. 使用隔离级别:根据需求选择合适的隔离级别

通过以上方法,可以有效检测和处理PHP中的悲观锁死锁问题。

抱歉,评论功能暂时关闭!