PHP项目锁超时如何设置防止死锁常驻

wen PHP项目 28

本文目录导读:

PHP项目锁超时如何设置防止死锁常驻

  1. 文件锁超时设置
  2. Redis分布式锁超时设置
  3. MySQL行锁超时设置
  4. 高级防死锁方案
  5. 最佳实践总结

在PHP项目中设置锁超时以防止死锁常驻,主要有以下几种方法:

文件锁超时设置

使用flock + 超时机制

class FileLock {
    private $fp;
    private $lockFile;
    public function acquire($key, $timeout = 5) {
        $this->lockFile = sys_get_temp_dir() . '/' . md5($key) . '.lock';
        $this->fp = fopen($this->lockFile, 'c');
        $start = time();
        while (true) {
            if (flock($this->fp, LOCK_EX | LOCK_NB)) {
                return true;
            }
            if (time() - $start > $timeout) {
                // 超时处理 - 强制释放旧锁
                $this->forceRelease();
                // 重试获取锁
                if (flock($this->fp, LOCK_EX | LOCK_NB)) {
                    return true;
                }
                return false;
            }
            usleep(100000); // 等待100ms重试
        }
    }
    public function release() {
        if ($this->fp) {
            flock($this->fp, LOCK_UN);
            fclose($this->fp);
            @unlink($this->lockFile);
        }
    }
    private function forceRelease() {
        // 检查锁文件是否过时
        if (file_exists($this->lockFile)) {
            $pid = file_get_contents($this->lockFile);
            if ($pid && !posix_kill($pid, 0)) {
                // 进程不存在,强制删除锁
                @unlink($this->lockFile);
            }
        }
    }
}

Redis分布式锁超时设置

使用Redis的SET命令+Lua脚本

class RedisLock {
    private $redis;
    private $lockKey;
    private $lockValue;
    private $ttl; // 锁过期时间(秒)
    public function __construct($redis, $key, $ttl = 10) {
        $this->redis = $redis;
        $this->lockKey = 'lock:' . $key;
        $this->lockValue = uniqid('', true); // 唯一标识
        $this->ttl = $ttl;
    }
    public function acquire($timeout = 5) {
        $start = microtime(true);
        while (true) {
            // SET NX EX(原子操作)
            $result = $this->redis->set(
                $this->lockKey,
                $this->lockValue,
                ['NX', 'EX' => $this->ttl]
            );
            if ($result) {
                return true;
            }
            // 检查是否超时
            if ((microtime(true) - $start) > $timeout) {
                return false;
            }
            usleep(100000); // 等待100ms
        }
    }
    public function release() {
        // 使用Lua脚本确保只有锁持有者才能释放
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        ";
        return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
    }
    // 续期锁(防止业务处理时间超过锁过期时间)
    public function extend($extraTtl = null) {
        $ttl = $extraTtl ?: $this->ttl;
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('expire', KEYS[1], ARGV[2])
            else
                return 0
            end
        ";
        return $this->redis->eval($script, [$this->lockKey, $this->lockValue, $ttl], 1);
    }
}

MySQL行锁超时设置

使用GET_LOCK()函数

class MySQLLock {
    private $pdo;
    private $lockName;
    public function __construct($pdo, $name) {
        $this->pdo = $pdo;
        $this->lockName = 'lock_' . $name;
    }
    public function acquire($timeout = 5) {
        // 设置MySQL锁等待超时
        $this->pdo->exec("SET innodb_lock_wait_timeout = {$timeout}");
        // 获取命名锁
        $stmt = $this->pdo->prepare("SELECT GET_LOCK(:name, :timeout)");
        $stmt->execute([
            ':name' => $this->lockName,
            ':timeout' => $timeout
        ]);
        $result = $stmt->fetchColumn();
        return $result == 1;
    }
    public function release() {
        $stmt = $this->pdo->prepare("SELECT RELEASE_LOCK(:name)");
        $stmt->execute([':name' => $this->lockName]);
        return $stmt->fetchColumn() == 1;
    }
    // 设置全局超时
    public static function setGlobalTimeout($pdo, $seconds = 5) {
        $pdo->exec("SET GLOBAL innodb_lock_wait_timeout = {$seconds}");
        $pdo->exec("SET SESSION lock_wait_timeout = {$seconds}");
    }
}

高级防死锁方案

看门狗机制(自动续期)

class WatchdogLock {
    private $redis;
    private $lockKey;
    private $lockValue;
    private $ttl;
    private $watchdogInterval;
    private $running = false;
    public function acquire($key, $ttl = 10, $timeout = 5) {
        $this->lockKey = 'watchdog:' . $key;
        $this->lockValue = uniqid('', true);
        $this->ttl = $ttl;
        $this->watchdogInterval = $ttl / 3; // 每1/3 TTL续期一次
        $start = microtime(true);
        while (true) {
            if ($this->redis->set($this->lockKey, $this->lockValue, ['NX', 'EX' => $ttl])) {
                // 启动看门狗
                $this->startWatchdog();
                return true;
            }
            if ((microtime(true) - $start) > $timeout) {
                return false;
            }
            usleep(50000);
        }
    }
    private function startWatchdog() {
        $this->running = true;
        // 使用popen或pcntl_fork启动后台进程
        $pid = pcntl_fork();
        if ($pid == -1) {
            // fork失败,使用crontab或定时器方式
            return false;
        } elseif ($pid == 0) {
            // 子进程执行续期
            while ($this->running) {
                sleep($this->watchdogInterval);
                $script = "
                    if redis.call('get', KEYS[1]) == ARGV[1] then
                        redis.call('expire', KEYS[1], ARGV[2])
                        return 1
                    end
                    return 0
                ";
                $result = $this->redis->eval(
                    $script,
                    [$this->lockKey, $this->lockValue, $this->ttl],
                    1
                );
                if ($result == 0) {
                    // 锁丢失,退出
                    exit(0);
                }
            }
            exit(0);
        }
    }
    public function stopWatchdog() {
        $this->running = false;
    }
    public function release() {
        $this->stopWatchdog();
        // 等待子进程退出
        pcntl_wait($status, WNOHANG);
        // 释放锁
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            end
            return 0
        ";
        return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
    }
}

死锁检测和恢复

class DeadlockDetector {
    private $redis;
    private $lockRegistry;
    public function __construct($redis) {
        $this->redis = $redis;
        $this->lockRegistry = 'deadlock:registry';
    }
    public function registerLock($lockKey, $processId, $ttl) {
        $lockInfo = [
            'pid' => $processId,
            'acquired_at' => time(),
            'ttl' => $ttl,
            'host' => gethostname()
        ];
        $this->redis->hSet($this->lockRegistry, $lockKey, json_encode($lockInfo));
        $this->redis->expire($this->lockRegistry, 3600); // 1小时清理
    }
    public function detectDeadlocks() {
        $locks = $this->redis->hGetAll($this->lockRegistry);
        $deadlocks = [];
        foreach ($locks as $key => $info) {
            $lockInfo = json_decode($info, true);
            if (time() - $lockInfo['acquired_at'] > $lockInfo['ttl']) {
                // 检查进程是否还活着
                if (!$this->isProcessAlive($lockInfo['pid'], $lockInfo['host'])) {
                    $deadlocks[] = $key;
                }
            }
        }
        return $deadlocks;
    }
    public function releaseDeadlocks($deadlocks) {
        foreach ($deadlocks as $key) {
            // 强制释放死锁
            $this->redis->del($key);
            $this->redis->hDel($this->lockRegistry, $key);
            // 记录日志
            $this->logDeadlock($key);
        }
    }
    private function isProcessAlive($pid, $host) {
        // 根据实际情况实现进程存活检测
        if ($host === gethostname()) {
            return file_exists("/proc/{$pid}");
        }
        // 远程主机检测逻辑
        return false;
    }
    private function logDeadlock($lockKey) {
        // 记录死锁信息到日志
        error_log("Deadlock detected and released: {$lockKey}");
    }
}

最佳实践总结

超时设置原则

class LockConfigurator {
    /**
     * 推荐的超时配置
     */
    public static function getRecommendedConfig($type) {
        $configs = [
            'file_lock' => [
                'acquire_timeout' => 5,     // 获取锁超时
                'lock_ttl' => 30,            // 锁持有时间
                'retry_interval' => 100,     // 重试间隔(ms)
                'max_retries' => 50
            ],
            'redis_lock' => [
                'acquire_timeout' => 3,     // 获取锁超时
                'lock_ttl' => 10,            // 锁过期时间
                'watchdog_ttl' => 3,         // 看门狗续期间隔
                'max_wait_time' => 10
            ],
            'mysql_lock' => [
                'acquire_timeout' => 5,     // MySQL锁等待超时
                'lock_ttl' => 30,            // 最大持有时间
                'innodb_timeout' => 5
            ]
        ];
        return $configs[$type] ?? $configs['redis_lock'];
    }
}
// 使用示例
$config = LockConfigurator::getRecommendedConfig('redis_lock');
$lock = new RedisLock($redis, 'process_data', $config['lock_ttl']);
// 设置PHP执行超时
set_time_limit($config['lock_ttl'] + 5);
if ($lock->acquire($config['acquire_timeout'])) {
    try {
        // 业务逻辑
        processData();
    } finally {
        $lock->release();
    }
} else {
    // 处理获取锁失败的情况
    throw new \RuntimeException('无法获取锁,请稍后重试');
}

关键建议

  1. 始终设置超时:所有锁操作都必须有超时机制
  2. 使用唯一标识:每个锁请求使用唯一标识防止误释放
  3. 原子操作:使用Redis的SET NX EX命令确保原子性
  4. 错误处理:确保在finally块中释放锁
  5. 监控告警:监控锁超时和死锁情况
  6. 分级超时:根据业务重要性设置不同的超时策略

通过这些方法,可以有效防止PHP项目中的死锁常驻问题,选择哪种方案取决于你的项目架构(单体/分布式)、性能要求和复杂性。

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