Redis分布式锁原理及用PHP实现(2)


锁在我们的日常开发可谓用得比较多。通常用来解决资源并发的问题。特别是多机集群情况下,资源争抢的问题。但是,很多新手在锁的处理上常常会犯一些问题。今天我们来深入理解锁。

一、Redis 锁错误使用之一

我曾经见过有的项目把查询结果存储到 Redis 当中时的伪代码如下:

$redis = new \Redis('127.0.0.1', 6379);

$cacheKey = 'query_cache';
$result = $redis->get($cacheKey);

if ($result) { // 缓存有效则直接返回
    return $result;
} else { // 缓存失效则从数据库中去获取并存储到 Redis
    $mysqlResult = getDataFromDb();
    $redis->set($cacheKey, json_encode($mysqlResult), 3600);

    return $mysqlResult;
}

初看代码并不会发现问题所在。通常情况下,当服务器资源压力非常小的时候,这段代码不会有任何问题。并且,真的可以提升服务器吞吐性能。

假如,这个位置的代码出现了单点压力呢?比如,这个功能是统计结果,查询数据库需要花 5s。而且,由于该功能比较常用,单位时间内达到了 1000 次/秒。

这时就会出现并发穿透问题。

1000 个请求同时到达这个程序位置,都去读取缓存是否存在。假如此时缓存不存在。这 1000 个请求都会得到不存在的结果。并且都会执行到去数据库取缓存结果的步骤。同时也会把结果重写到 Redis。

那就导致了这一瞬间单点压力导致穿透到数据库,造成数据库压力瞬间到达峰值。如果我们的数据库的性能处理不了这么大的压力,就会导致数据库服务器 CPU 直接爆满。响应给前端的数据就会陷入停顿状态。

所以,这段代码是不正确的锁使用。

二、Redis 锁错误使用之二

在第一点中,我们发现了问题。于是,就有人想着去优化它。于是就有了下面的代码:

$redis = new \Redis('127.0.0.1', 6379);

$lockKey = 'query_cache_lock'; // 锁专用的 KEY
$cacheKey = 'query_cache'; // 存储查询结果的 KEY
$result = $redis->get($cacheKey);

if ($result) { // 缓存有效则直接返回。
    return $result;
} else { // 缓存失效则从数据库中去获取并存储到 Redis
    if ($redis->setNx($lockKey) === false) {
        throw new \Exception("服务器火爆,请稍候重试");
    } else {
        $mysqlResult = getDataFromDb();
        $redis->set($cacheKey, json_encode($mysqlResult), 3600);
        $redis->delete($lockKey); // 锁用完了要解锁。删掉就是解锁。

        return $mysqlResult;
    }
}

这段代码就完全避免了第一点中的并发穿透的问题。但是,相对第一点,代码也多增加了几行。不过性能依然强劲。

即使如此,这段代码依然存在三个问题:

  1. 并发越大,第一个取到锁的请求能正常响应,后续的请求就会得到一个“服务器火爆,请稍候重试”的异常提示。
  2. 没办法对后续请求取锁失效加一个等待时间。
  3. 如果代码执行到 r e d i s − > d e l e t e ( redis->delete( redis>delete(lockKey) 之前程序异常了。那么锁就不能正常释放。后续的锁也无法正常取到锁了。

针对第 (1) 点,这个是用户体验极差的。
针对第 (2) 点,它是解决第一点的方案。
针对第 (3) 点,它是我们必须解决的问题。否则,我们的分布式锁将无法正常使用。

三、正确的分布式锁

正常的分布式锁要满足以下几点要求:

  1. 能解决并发时资源争抢。这是最核心的需求。
  2. 锁能正常添加与释放。不能出现死锁。
  3. 锁能实现等待,否则不能最大保证用户的体验。

针对以上三点,得出 Redis 分布式锁示例

class RedisMutexLock {
    /**
     * 返回 Redis 连接
     *
     * @return void
     */
    public static function getRedis() {
        // 这行代码请根据自己项目替换为自己的获取 Redis 连接
        return YCache::getRedisClient();
    }

    /**
     * 获得锁,如果锁被占用,阻塞,直到获得锁或者超时
     * -- 1、如果 $timeout 参数为 0,则立即返回锁
     * -- 2、建议 timeout 设置为 0,避免 redis 因为阻塞导致性能下降请根据实际需求进行设置
     *
     * @param string $key 缓存KEY
     * @param int $timeout 取锁超时时间单位(秒)等于0,如果当前锁被占用,则立即返回失败如果大于0,则反复尝试获取锁直到达到该超时时间
     * @param int $lockSecond 锁定时间单位(秒)
     * @param int $sleep 取锁间隔时间单位(微秒)当锁为占用状态时每隔多久尝试去取锁默认 0.1 秒一次取锁
     * @return bool 成功:true、失败:false
     */
    public static function lock($key, $timeout = 0, $lockSecond = 20, $sleep = 100000) {
        if (strlen($key) === 0) {
            // 项目抛异常方法
            YCore::exception(500, '缓存KEY没有设置');
        }

        $start = self::getMicroTime();
        $redis = self::getRedis();

        do {
            // [1] 锁的 KEY 不存在时设置其值并把过期时间设置为指定的时间锁的值并不重要重要的是利用 Redis 的特性
            $acquired = $redis->set("Lock:{$key}", 1, ['NX', 'EX' => $lockSecond]);
            if ($acquired) {
                break;
            }
            if ($timeout === 0) {
                break;
            }
            usleep($sleep);
        } while (!is_numeric($timeout) || (self::getMicroTime()) < ($start + ($timeout * 1000000)));

        return $acquired ? true : false;
    }

    /**
     * 释放锁
     *
     * @param mixed $key 被加锁的KEY
     * @return void
     */
    public static function release($key) {
        if (strlen($key) === 0) {
            // 项目抛异常方法
            YCore::exception(500, '缓存KEY没有设置');
        }
        
        $redis = self::getRedis();
        $redis->del("Lock:{$key}");
    }

    /**
     * 获取当前微秒
     *
     * @return bigint
     */
    protected static function getMicroTime() {
        return bcmul(microtime(true), 1000000);
    }
}

转自:https://www.phpernote.com/redis/1621.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis分布式锁实现原理是基于Redis的SETNX指令和过期时间实现的。具体实现步骤是:当某个客户端请求获取锁时,该客户端向Redis服务器发送SETNX命令,若SETNX命令返回值为1,则表示获取锁成功,该客户端接着设置一个过期时间,用以避免锁一直持有;若SETNX命令返回值为0,则表示获取锁失败,此时客户端需要等待一段时间后重新尝试获取锁。 ### 回答2: Redis分布式锁是一种在分布式环境下协调多个进程或线程之间的互斥访问资源的机制。其原理是利用Redis提供的原子操作,通过在Redis中设置一个特定的键值对来实现锁的获取和释放。 具体实现步骤如下: 1. 获取锁:客户端通过执行SETNX命令(SET if Not eXists)来尝试在Redis中设置一个指定的键,并为其设置一个过期时间。如果命令成功执行并返回1,表示获取到了锁,否则表示锁已被其他客户端占用。 2. 使用锁:获取到锁之后,执行需要互斥访问资源的操作。 3. 释放锁:操作完成后,客户端通过执行DEL命令来删除所创建的键,释放锁。 需要注意的是,为了防止锁过期时间过长导致锁永远不会释放,可以使用SET命令来给锁设置一个过期时间,保证即使发生异常情况,锁也会在一段时间后自动释放。 此外,还需要考虑到锁的重入性和死锁的情况。对于锁的重入性,可以在Redis中维护一个计数器,记录获取锁的次数,在释放锁时将计数器减1,直到计数器为0时才真正释放锁。对于死锁的情况,可以为锁设置一个超时时间,如果获取锁的客户端在规定的时间内没有释放锁,则认为发生了死锁,其他客户端可以尝试获取该锁。 总之,Redis分布式锁通过利用Redis的原子操作和过期时间机制,可以实现多个进程或线程之间的资源互斥访问,确保系统在分布式环境下的稳定运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值