PHP项目怎么使用Redis分布式锁?

wen java案例 2

本文目录导读:

PHP项目怎么使用Redis分布式锁?

  1. 核心原则
  2. 方法一:使用 set 命令 + Lua 脚本(最推荐)
  3. 方法二:使用 Predis 或 phpredis 扩展的 set() 方法
  4. 方法三:使用 Redlock 算法(适用于高可靠场景)
  5. 常见问题与注意事项

在PHP项目中使用Redis分布式锁,主要用来解决高并发场景下的资源竞争问题(如秒杀、抢红包、防止重复提交等)。

以下是几种常见、规范的实现方式,从简单到可靠逐步说明。


核心原则

一个可靠的分布式锁需要满足:

  1. 互斥性:任何时刻只有一个客户端能持有锁。
  2. 防死锁:即使客户端崩溃,锁也能自动释放(设置过期时间)。
  3. 解铃还须系铃人:只能释放自己加的锁,不能误删别人的锁。
  4. 高可用:Redis本身(或集群)稳定。

使用 set 命令 + Lua 脚本(最推荐)

这是目前最通用、最被推荐的方式,避免了竞态条件。

加锁(原子操作)

利用 Redis 的 SET key value NX EX 指令,NX 保证不存在才设置(互斥),EX 设置过期时间(防死锁)。

<?php
class RedisDistributedLock {
    private $redis;
    private $lockKey;
    private $lockValue; // 唯一标识,用于安全解锁
    private $ttl;       // 锁过期时间(秒)
    public function __construct($redis, $lockKey, $ttl = 10) {
        $this->redis = $redis;
        $this->lockKey = $lockKey;
        $this->ttl = $ttl;
        // 生成唯一标识(可以用 UUID,或用 uniqid + 随机数)
        $this->lockValue = uniqid('lock_', true);
    }
    /**
     * 尝试获取锁
     * @param int $timeout 等待超时时间(秒),0 表示不等待立即返回
     * @return bool
     */
    public function acquire($timeout = 0) {
        $start = time();
        while (true) {
            // 关键:SET NX EX 原子操作
            $result = $this->redis->set($this->lockKey, $this->lockValue, ['NX', 'EX' => $this->ttl]);
            if ($result) {
                return true;
            }
            // 不需要等待
            if ($timeout <= 0) {
                return false;
            }
            // 等待一段时间再重试(200ms),避免忙等待
            usleep(200000); // 0.2秒
            // 检查是否超时
            if ((time() - $start) >= $timeout) {
                return false;
            }
        }
    }
    /**
     * 释放锁
     * 必须使用 Lua 脚本保证原子性:先判断 value 是否自己,再删除
     */
    public function release() {
        // Lua 脚本:value 匹配,则删除 key
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        ";
        // 传入 KEYS 和 ARGV
        return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
    }
    // 可选:自动续期(看门狗)—— 一般用不到,除非业务执行时间可能超过 TTL
    public function renew() {
        // 可以用 Lua 脚本检查 value 并重置过期时间
    }
}

使用示例:

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lock = new RedisDistributedLock($redis, 'my_lock_key', 10);
if ($lock->acquire(5)) { // 等待最多 5 秒
    try {
        // 执行临界区代码(如扣库存、写订单)
        echo "执行业务逻辑...\n";
    } finally {
        $lock->release(); // 确保释放锁
    }
} else {
    echo "获取锁失败,请稍后重试\n";
}

使用 Predis 或 phpredis 扩展的 set() 方法

如果你使用 Predisphpredis 扩展,写法类似,但注意参数格式。

使用 phpredis(如上例):

// 注意:phpredis 的 set 参数需要写成 ['NX', 'EX' => 10] 或 'NX', 'EX', 10
$redis->set($key, $value, ['NX', 'EX' => 10]); // 推荐
// 或者老版本
// $redis->set($key, $value, ['nx', 'ex' => 10]);

使用 Predis:

$client->set($key, $value, 'NX', 'EX', 10);

使用 Redlock 算法(适用于高可靠场景)

Redis 是主从架构,且需要严格防止锁丢失(例如主节点挂掉后,从节点还没同步就接管),可以考虑 Redlock 算法。

Redlock 需要在 多个独立的 Redis 节点 上同时加锁,只要大多数节点(N/2+1)加锁成功就算成功。

但注意:普通业务用不到 Redlock,它很重,只在金融、高一致性场景下才考虑,PHP 中可以使用封装好的库如 illuminate/redis(Laravel 的 Redis 锁)或 redlock-php


常见问题与注意事项

  1. 锁超时时间(TTL)如何设定?

    • 需要大于业务最大执行时间,例如业务平均 1 秒,最大可能 3 秒,TTL 建议设 5-10 秒。
    • 如果业务执行可能超时,可以加一个看门狗(Watchdog) 机制:在锁持有期间,每隔 1/3 TTL 时间自动续期。
  2. 为什么要用唯一标识(lockValue)?

    • 防止误删:如果线程A持锁超时后锁被释放,线程B拿到锁,此时线程A突然完成并调用 del,会错误删掉线程B的锁。
    • 通过 Lua 脚本判断 value 确保是自己删自己的。
  3. Redis 单点故障?

    Redis 挂了,锁功能不可用,可以用 Redis 哨兵或集群提高可用性,追求高可用可以考虑 Redlock。

  4. 可重入锁?

    • 上述实现是不可重入的(一个线程不能重复获取同一把锁),如果需要可重入,可以用 incr 或维护一个 hash 结构记录持有者和重入次数。
  5. Laravel / Symfony 框架?

    • Laravel 内置了基于 Redis 的锁:
      Cache::lock('my_lock', 10)->get(function () {
          // 业务逻辑
      });
    • Symphony 的 Lock 组件也支持 Redis 锁。

方式 可靠性 复杂度 推荐场景
SET NX EX + Lua 脚本 (满足大部分需求) 通用方案,强烈推荐
纯 SET NX(无 Lua) 中(有误删风险) 不推荐
Redlock 非常高(跨节点) 金融级、多节点强一致

对于绝大多数 PHP 项目,方法一(SET NX EX + Lua 释放锁)已经足够,简单、可靠、高效。

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