PHP基于redis实现的并发锁

在用redis做缓存时, 如果不考虑并发问题, 在缓存不存在或过期时, 会导致很多请求直接进入数据库,造成很多"意外"的负载.

所以, 需要对缓存不存在->走数据库查询的处理过程中, 增加一个锁, 来避免该问题, 这就是并发锁.

加锁的过程:

  • 请求的缓存不存在, 尝试加锁(必须使用redis的setnx), 开始循环处理.
  • 如果锁存在, 则休眠, 等待下一次循环.
  • 如果锁不存在
    • 加锁成功, 则执行业务查询(走数据库),然后解锁
    • 加锁失败, 休眠, 等待下一次循环(下一循环时, 锁应该存在, 继续循环)
  • 循环第二次开始, 如果锁不存在, 则认为缓存已经生成, 去查询这个缓存. 如果查询不到, 则认为系统异常.

这里的加锁返回三个状态:

  • 1: 获取锁成功(执行业务后需要unlock)
  • 2: 加锁失败,但是尝试期间锁已经不存在(可以判断缓存数据是否存在)
  • 0: 获取锁失败(或超时)

这里的锁的失效时间, 建议不要设置太长.

类库文件: RedisLockUtility.php

/**
 * redis并发锁
 * @author aben
 *
 * Class RedisLockUtility
 */
class RedisLockUtility {
    /**
     * 给key加锁
     *
     * @param \ClsRedis $redis_connection redis连接对象
     * @param string $key_prefix 指定的key前缀
     * @param int $timeout 锁定/超时时间, 默认2秒
     * @param float $retry_wait_sec 重新尝试获取key的时间间隔, 默认0.1秒
     * @return int 结果. 1:获取锁成功(执行业务后需要unlock), 2:加锁失败,但是尝试期间锁已经不存在(可以判断缓存数据是否存在), 0: 获取锁失败(或超时)
     */
    public static function lock($redis_connection, $key_prefix, $timeout = 2, $retry_wait_sec = 0.1){
        $cache_lock_key = self::getLockKey($key_prefix);

        //重试间隔 (秒转换为微秒)
        $retry_interval = $retry_wait_sec * 1000000;

        //重试次数
        $retry_times = $timeout / $retry_wait_sec;

        for($i=0; $i < $retry_times; $i++) {
            if($redis_connection->get($cache_lock_key)){
                //锁存在, 则休眠
                usleep($retry_interval);
            }
            else {
                //(第一次循环)没有锁, 加锁, 查询数据
                if($i == 0){
                    //准备加锁
                    $rt_lock = $redis_connection->setnx($cache_lock_key, 1);//使用setnx命令, 保证锁的唯一
                    if($rt_lock === true) {
                        //加锁成功
                        //设置`锁的有效时间`
                        $redis_connection->expire($cache_lock_key, $timeout);

                        //加锁成功后, 可以执行数据库查询, 然后解锁(unlock) !!!

                        return 1;
                    }
                    else {
                        //加锁失败, 继续尝试
                        usleep($retry_interval);
                    }
                }
                else {
                    //锁不存在了. 后续可以判断有没有缓存.
                    return 2;
                }
            }
        }

        return 0;
    }

    /**
     * 解锁
     *
     * @param \ClsRedis $redis_connection redis连接对象
     * @param string $key_prefix
     */
    public static function unlock($redis_connection, $key_prefix){
        $redis_connection->del(self::getLockKey($key_prefix));
    }

    /**
     * 拼接完整的锁的key
     *
     * @param string $key_prefix
     * @return string
     */
    public static function getLockKey($key_prefix){
        return $key_prefix.':lock';
    }
}

实际调用:

$cacheKey = 'index_products';
/** @var \ClsRedis $cache */
$cache = ClsRedis::getInstance();//redis连接实例
$res = $cache->get($cacheKey);

$getAllDataSuccess = false;
if ($res === false) {
    //数据缓存不存在
    
    //处理并发: 加锁
    $lock_key_prefix = 'index_products';
    switch(RedisLockUtility::lock($cache, $lock_key_prefix, 2, 0.1)){
        case 1://获取锁成功
            //去数据库查询
            /**
            * 这里执行数据库查询, 得到数据$res
            */

            //写入缓存
            $cache->setex($cacheKey, 180, json_encode($res));
            //sleep(2); //并发测试时可以休眠一下, 注意: 这个时间不要超过`锁的有效时间`!
            $getAllDataSuccess = true;

            //移除锁
            RedisLockUtility::unlock($cache, $lock_key_prefix);

            break;
        case 2://加锁失败,但是尝试期间锁已经不存在(可以判断缓存数据是否存在)
            //锁不存在了, 判断有没有缓存.
            $data = $cache->get($cacheKey);
            if($data !== false){
                $res = json_decode($data, true);
                $getAllDataSuccess = true;
            }
            break;
        default://失败
    }

    if($getAllDataSuccess === false){
        echo json_encode(['code' => 0, 'msg'=>'获取数据失败', 'res' => []]);
        exit;
    }
    if (empty($res)){
        echo json_encode(['code' => 200, 'msg'=>'没有数据', 'res' => []]);
        exit;
    }
}
else {
    $res = json_decode($res, true);
}

这个方案有个问题. 因为setnx和expire是2个操作, 如果expire操作失败, 则这个锁就会一致存在, 导致数据无法获取.

本方案待完善....

关于分布式锁的问题, 可以参考: https://xie.infoq.cn/article/4d571787a3280ef3094338f9b

github上也有其他人写的类, latrell/Lock等, 请自行搜索.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值