本文目录导读:

在 PHP 中实现加锁超时放弃策略,主要有以下几种方案,我会逐一详细讲解:
文件锁(flock)
基础实现
<?php
class FileLock {
private $fp;
private $lockFile;
public function __construct($lockFile = '/tmp/lockfile.lock') {
$this->lockFile = $lockFile;
}
/**
* 获取锁(带超时)
* @param float $timeout 超时时间(秒)
* @param int $blocking 是否阻塞
* @return bool 是否获取成功
*/
public function acquire($timeout = 5, $blocking = true) {
$this->fp = fopen($this->lockFile, 'c');
if (!$blocking) {
return flock($this->fp, LOCK_EX | LOCK_NB);
}
$startTime = microtime(true);
// 循环尝试获取锁,直到超时
while (true) {
if (flock($this->fp, LOCK_EX | LOCK_NB)) {
return true;
}
// 检查是否超时
if (microtime(true) - $startTime >= $timeout) {
fclose($this->fp);
return false;
}
// 短暂休眠,避免CPU空转
usleep(100000); // 100ms
}
}
/**
* 释放锁
*/
public function release() {
if ($this->fp) {
flock($this->fp, LOCK_UN);
fclose($this->fp);
}
}
}
// 使用示例
$lock = new FileLock('/tmp/data.lock');
if ($lock->acquire(5)) { // 5秒超时
try {
// 执行需要加锁的操作
file_put_contents('/tmp/data.txt', 'new data');
echo "操作成功\n";
} finally {
$lock->release();
}
} else {
echo "获取锁超时,任务放弃\n";
}
Redis 锁(SET NX EX)
<?php
class RedisLock {
private $redis;
private $prefix = 'lock:';
public function __construct($host = '127.0.0.1', $port = 6379) {
$this->redis = new Redis();
$this->redis->connect($host, $port);
}
/**
* 获取分布式锁
* @param string $key 锁的名称
* @param int $timeout 锁的自动过期时间(秒)
* @param int $waitTimeout 等待获取锁的超时时间(秒)
* @param string $token 唯一标识,防止误删
* @return bool|string 成功返回token,失败返回false
*/
public function acquire($key, $timeout = 30, $waitTimeout = 5) {
$token = uniqid('lock_', true);
$lockKey = $this->prefix . $key;
$startTime = microtime(true);
while (true) {
// 使用SET NX EX命令原子设置
$result = $this->redis->set($lockKey, $token, ['NX', 'EX' => $timeout]);
if ($result) {
return $token;
}
// 检查是否超时
if (microtime(true) - $startTime >= $waitTimeout) {
return false;
}
usleep(50000); // 50ms后重试
}
}
/**
* 释放分布式锁
* @param string $key 锁的名称
* @param string $token 之前获取的token
*/
public function release($key, $token) {
$lockKey = $this->prefix . $key;
// 使用Lua脚本确保原子性
$script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval($script, [$lockKey, $token], 1);
}
/**
* 带自动重试的锁使用
*/
public function withLock($key, $callback, $timeout = 30, $waitTimeout = 5) {
$token = $this->acquire($key, $timeout, $waitTimeout);
if (!$token) {
return false;
}
try {
return $callback();
} finally {
$this->release($key, $token);
}
}
}
// 使用示例
$redisLock = new RedisLock();
$result = $redisLock->withLock(
'payment:order:1001',
function() {
// 执行关键业务
return "处理订单成功";
},
30, // 锁的过期时间
5 // 等待超时时间
);
if ($result === false) {
echo "获取锁超时,请稍后重试\n";
}
数据库锁(悲观锁)
<?php
class DatabaseLock {
private $pdo;
public function __construct($dsn, $username, $password) {
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* 获取数据库行锁
* @param int $id 记录ID
* @param int $timeout 超时时间(秒)
* @return bool
*/
public function acquireLock($id, $timeout = 5) {
try {
// 设置锁等待超时时间
$this->pdo->exec("SET innodb_lock_wait_timeout = $timeout");
// 开始事务
$this->pdo->beginTransaction();
// 使用 SELECT ... FOR UPDATE 获取行锁
$stmt = $this->pdo->prepare(
"SELECT id FROM resources WHERE id = :id FOR UPDATE"
);
$stmt->execute(['id' => $id]);
if ($stmt->rowCount() === 0) {
$this->pdo->rollBack();
return false;
}
return true;
} catch (Exception $e) {
// 超时或获取失败
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
return false;
}
}
/**
* 释放锁(提交事务)
*/
public function releaseLock() {
$this->pdo->commit();
}
}
信号量锁(System V Semaphore)
<?php
class SemaphoreLock {
private $sem;
private $key;
public function __construct($key = 12345, $maxLocks = 1) {
$this->key = $key;
$this->sem = sem_get($key, $maxLocks);
}
/**
* 获取信号量锁
* @param int $timeout 超时时间(秒)
* @return bool
*/
public function acquire($timeout = 5) {
// sem_acquire 不支持超时,需要自己实现
$startTime = microtime(true);
while (true) {
// 非阻塞获取
if (sem_acquire($this->sem, false)) {
return true;
}
// 检查超时
if (microtime(true) - $startTime >= $timeout) {
return false;
}
usleep(100000); // 100ms
}
}
/**
* 释放信号量锁
*/
public function release() {
sem_release($this->sem);
}
public function __destruct() {
if (isset($this->sem)) {
sem_remove($this->sem);
}
}
}
高级封装 - 通用锁类
<?php
interface LockInterface {
public function acquire($key, $timeout = 5);
public function release($key, $token = null);
public function withLock($key, $callback, $timeout = 5);
}
abstract class AbstractLock implements LockInterface {
protected $timeout = 5;
protected $retryInterval = 100000; // 100ms
public function withLock($key, $callback, $timeout = 5) {
$token = $this->acquire($key, $timeout);
if (!$token) {
throw new Exception("获取锁超时: $key");
}
try {
return $callback();
} catch (Exception $e) {
throw $e;
} finally {
$this->release($key, $token);
}
}
protected function sleepBeforeRetry() {
usleep($this->retryInterval);
}
}
// Redis实现
class RedisLockImpl extends AbstractLock {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
public function acquire($key, $timeout = 5) {
$token = uniqid('', true);
$startTime = microtime(true);
while (true) {
if ($this->redis->set($key, $token, ['NX', 'EX' => $timeout])) {
return $token;
}
if (microtime(true) - $startTime >= $timeout) {
return false;
}
$this->sleepBeforeRetry();
}
}
public function release($key, $token = null) {
if (!$token) return false;
$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, [$key, $token], 1);
}
}
// 使用
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lock = new RedisLockImpl($redis);
try {
$result = $lock->withLock(
'inventory:product:1001',
function() {
// 更新库存
return "库存更新成功";
},
10 // 10秒超时
);
echo $result;
} catch (Exception $e) {
echo "操作失败: " . $e->getMessage();
}
最佳实践建议
选择合适的超时时间
$timeout = 5; // 一般任务 $timeout = 30; // 长时间任务 $timeout = 0; // 有冲突就立即放弃
实现优雅降级
function executeWithFallback($lock, $key, $task, $timeout = 5) {
try {
return $lock->withLock($key, $task, $timeout);
} catch (TimeoutException $e) {
// 超时处理策略
// 1. 重试
// 2. 返回错误
// 3. 使用旧数据
// 4. 排队等待
return ['success' => false, 'message' => '系统繁忙,请稍后重试'];
}
}
监控和日志记录
class MonitoredLock extends RedisLockImpl {
protected function logLockEvent($event, $key, $duration) {
// 记录到日志系统
error_log("[LOCK] $event $key took $duration ms");
}
public function acquire($key, $timeout = 5) {
$start = microtime(true);
$result = parent::acquire($key, $timeout);
$this->logLockEvent(
$result ? 'ACQUIRED' : 'TIMEOUT',
$key,
(microtime(true) - $start) * 1000
);
return $result;
}
}
性能优化建议
- 缩短重试间隔(10-50ms)
- 使用随机退避算法避免惊群效应
- 考虑使用 Redisson 等成熟组件
public function acquireWithExponentialBackoff($key, $maxTimeout = 5) {
$baseDelay = 10; // 10ms
$maxDelay = 500; // 500ms
$attempt = 0;
$startTime = microtime(true);
while (true) {
if ($this->attemptAcquire($key)) {
return true;
}
if (microtime(true) - $startTime >= $maxTimeout) {
return false;
}
$delay = min($baseDelay * pow(2, $attempt), $maxDelay);
usleep($delay * 1000);
$attempt++;
}
}
选择哪种方案取决于你的具体需求:
- 单机应用:文件锁
- 分布式应用:Redis锁
- 强一致性:数据库锁
- 性能需求高:Redis或内存锁