PHP 加锁超时放弃策略

wen PHP项目 2

本文目录导读:

PHP 加锁超时放弃策略

  1. 文件锁(flock)
  2. Redis 锁(SET NX EX)
  3. 数据库锁(悲观锁)
  4. 信号量锁(System V Semaphore)
  5. 高级封装 - 通用锁类
  6. 最佳实践建议

在 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或内存锁

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