本文目录导读:

在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('无法获取锁,请稍后重试');
}
关键建议
- 始终设置超时:所有锁操作都必须有超时机制
- 使用唯一标识:每个锁请求使用唯一标识防止误释放
- 原子操作:使用Redis的SET NX EX命令确保原子性
- 错误处理:确保在
finally块中释放锁 - 监控告警:监控锁超时和死锁情况
- 分级超时:根据业务重要性设置不同的超时策略
通过这些方法,可以有效防止PHP项目中的死锁常驻问题,选择哪种方案取决于你的项目架构(单体/分布式)、性能要求和复杂性。