PHP 中如何实现Redis分布式锁

152 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 6 月更文挑战」的第 9 天,点击查看活动详情

实现分布式锁用到的Redis命令介绍:

setnx(key, value)
将key的值设为value,当且仅当key不存在。若给定的key已经存在,则SETNX不做任何动作。
SETNX是”SET if Not eXists”(如果不存在,则SET)的简写。返回值:设置成功,返回1。设置失败,返回0。
get(key)
返回key所关联的字符串值。如果key不存在则返回特殊值nil。假如key储存的值不是字符串类型,返回一个错误,因为GET只能用于处理字符串值。返回值:key的值。如果key不存在,返回nil。

getset(key, value)
将给定key的值设为value,并返回key的旧值。当key存在但不是字符串类型时,返回一个错误。返回值:返回给定key的旧值(old value)。当key没有旧值时,返回nil。

expire(key, seconds)
为给定key设置生存时间。当key过期时,它会被自动删除。
在Redis中,带有生存时间的key被称作“易失的”(volatile)。在低于2.1.3版本的Redis中,已存在的生存时间不可覆盖。

ttl(key)
返回给定key的剩余生存时间(time to live)(以秒为单位)。返回值:key的剩余生存时间(以秒为单位)。当key不存在或没有设置生存时间时,返回-1 。

del(key)
移除给定的一个或多个key。返回值:被移除key的数量。

在 PHP 中,可以使用 Redis 实现分布式锁。下面是一个示例代码,演示如何使用 Redis 实现基本的分布式锁:

<?php

$redis = new Redis();
$redis->connect('localhost', 6379);

// 锁的名称
$lockKey = 'my_lock';

// 获取锁
$lockAcquired = $redis->set($lockKey, 'locked', ['nx', 'ex' => 10]);

if ($lockAcquired) {
    // 成功获取到锁
    try {
        // 在这里执行需要加锁的代码

        // 模拟长时间运行的任务
        sleep(5);

        // 任务完成后释放锁
        $redis->del($lockKey);
    } catch (Exception $e) {
        // 处理异常情况
        $redis->del($lockKey);
        // 抛出异常或进行其他处理
    }
} else {
    // 未能获取到锁,可以选择等待或直接返回错误
    echo "Failed to acquire lock.";
}

在这个示例中,我们使用 Redis 的 SET 命令来设置一个键值对,其中键是锁的名称,值是表示锁定状态的标志(在此示例中,我们使用字符串 "locked")。通过将 nx 选项设置为 SET 命令,我们确保只有一个客户端能够成功设置该键,即获取到锁。

另外,我们还设置了 ex 选项,将锁的过期时间设置为 10 秒。这是为了防止死锁,以防锁的持有者在某种情况下未能释放锁。如果任务在规定的时间内未能完成,锁会自动过期,其他客户端可以获取到锁。

注意,这只是一个基本的示例,用于说明 Redis 分布式锁的实现原理。在实际生产环境中,还需要考虑更多的细节,如锁的超时处理、锁的可重入性等。另外,还可以使用 RedLock 算法等更复杂的方案来增强分布式锁的可靠性和性能。

具体实现的PHP完整代码:

<?php
/**
 * 实现Redis分布锁
 */

$key        'test';       //要更新信息的缓存KEY
$lockKey    'lock:'.$key//设置锁KEY
$lockExpire 10;           //设置锁的有效期为10秒

//获取缓存信息
$result $redis->get($key);
//判断缓存中是否有数据
if(empty($result))
{
    $status TRUE;
    while ($status)
    {
        //设置锁值为当前时间戳 + 有效期
        $lockValue time() + $lockExpire;
        /**
         * 创建锁
         * 试图以$lockKey为key创建一个缓存,value值为当前时间戳
         * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
         * 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了
         * @var [type]
         */
        $lock $redis->setnx($lockKey$lockValue);
        /**
         * 满足两个条件中的一个即可进行操作
         * 1、上面一步创建锁成功;
         * 2、   1)判断锁的值(时间戳)是否小于当前时间    $redis->get()
         *      2)同时给锁设置新值成功    $redis->getset()
         */
        if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey$lockValue) < time() ))
        {
            //给锁设置生存时间
            $redis->expire($lockKey$lockExpire);
            //******************************
            //此处执行插入、更新缓存操作...
            //******************************

            //以上程序走完删除锁
            //检测锁是否过期,过期锁没必要删除
            if($redis->ttl($lockKey))
                $redis->del($lockKey);
            $status FALSE;
        }else{
            /**
             * 如果存在有效锁这里做相应处理
             *      等待当前操作完成再执行此次请求
             *      直接返回
             */
            sleep(2);//等待2秒后再尝试执行操作
        }
    }
}