PHP 开发实战:Redis 分布式锁实现

栏目: PHP · 发布时间: 5年前

内容简介:锁在我们的日常开发可谓用得比较多。通常用来解决资源并发的问题。特别是多机集群情况下,资源争抢的问题。但是,很多新手在锁的处理上常常会犯一些问题。今天我们来深入理解锁。在本文最后,博主也会放出最佳的 Redis 分布式锁的 PHP 代码。一、Redis 锁错误使用之一我曾经见过有的项目把查询结果存储到 Redis 当中时的伪代码如下:

锁在我们的日常开发可谓用得比较多。通常用来解决资源并发的问题。特别是多机集群情况下,资源争抢的问题。但是,很多新手在锁的处理上常常会犯一些问题。今天我们来深入理解锁。在本文最后,博主也会放出最佳的 Redis 分布式锁的 PHP 代码。

一、Redis 锁错误使用之一

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

$redis    = new \Redis('127.0.0.1', 6379);
$cacheKey = 'query_cache';
$result   = $redis->get($cacheKey);
if ($result) { // 缓存有效则直接返回。
    return $result;
} else { // 缓存失效则重新获取并存储到 Redis。
    $mysqlResult = []; // 这一步我省了。
    $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 = []; // 这一步我省了。
        $redis->set($cacheKey, json_encode($mysqlResult), 3600);
        $redis->delete($lockKey); // 锁用完了要解锁。删掉就是解锁。
        return $mysqlResult;
    }
}

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

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

1)并发越大,第一个取到锁的请求能正常响应,后续的请求就会得到一个“服务器火爆,请稍候重试”的异常提示。

2)没办法对后续请求取锁失效加一个等待时间。

3)如果代码执行到 $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);
    }
}

这个工具类中有两个位置使用了自己封装的方法。大家替换一下就好。非常简单。

大家一要记得获取锁之后,一定要手动释放锁。虽然,锁能自己释放。

由于受限于本人的知识面,文中难免存在不当之处。还请大家指正!谢谢!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程权威指南

游戏编程权威指南

Mike McShaffry 麦克沙福瑞、David “Rez” Graham 格雷海姆 / 师蓉、李静、李青翠 / 人民邮电 / 2016-3 / 99.00元

全书分为4个部分共24章。首部分是游戏编程基础,主要介绍了游戏编程的定义、游戏架构等基础知识。 第二部分是让游戏跑起来,主要介绍了初始化和关闭代码、主循环、游戏主题和用户界面等。 第三部分是核心游戏技术,主要介绍了一些*为复杂的代码 示例,如3D编程、游戏音频、物理和AI编程等。 第四部分是综合应用,主要介绍了网络编程、多道程序设计和用C#创建工具等,并利用前面所讲的 知识开发出......一起来看看 《游戏编程权威指南》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具