使用redis的比较完美的加锁解锁

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 使用redis的比较完美的加锁解锁tags:redis read&write redis加锁和解锁 php习惯性说一下写这篇文章要说明什么,我们经常用redis进行加锁操作,目的是为了解决并发可能带来的问题。

使用redis的比较完美的加锁解锁

tags:redis read&write redis加锁和解锁 php


习惯性说一下写这篇文章要说明什么,我们经常用redis进行加锁操作,目的是为了解决并发可能带来的问题。但是使用redis加锁的方式有多种,本文对常见的几种方式进行解析,并提供一种相对完美的方案。

read & write 问题

这是一个经典问题,请看代码:

    //redis中的某个键自增
    $val = $this->redis->get($key);
    $val ++;
    $this->redis->set($val);

这段代码逻辑没有问题,就是先读取数据,再修改数据,在写回修改,这里是希望每次访问都递增变量$val的值,但在并发情况下,存在情况是两个进程都读取到了一样的初始值,然后都加1,最后写回Redis,这种情况就会统计数据比实际的少。这个问题应该有许多人遇到过,思考过怎么解决这类问题。这里给出一个统一的解决方案,就是尽量保证操作的原子性,比如可以用redis的incr命令来实现自增(可以认为redis的命令是原子的)。

加锁

由上面的问题再进一步,来探讨一个大家常用的,为一个操作进行加锁。

问题场景如下:有一个商品,每个用户都可以去修改商品信息。假设用户id分别为6和8的用户对id为123的商品进行操作。

错误示例1

    $key = '123';
    $val = $this->redis->get($key);
    if(!$val){
        $this->redis->set($key,'123');
        $this->redis->expire($key,'4');
        /**此处修改商品信息操作
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '错误提示';
    }
    

上面这个错误示例,
错误点1:set和expire是分开写的,如果说程序执行中再执行了set()后出现崩溃,则这个就变成了永久锁(虽然这是个小概率事件)。

错误点2:这个商品中设置的key是商品id,val也是商品id,很多人认为只有一个key就可以了,val是什么无所谓。这就缺少了锁的标识,无法判断这个锁的拥有者是谁,从而会带来一系列影响如下。

  1. 用户1进程获取key对应的val,发现没有锁,所以调用了set,可能在set前,另一个用户2的进程也发现没有这个锁,也进行set,就造成了两个进程都认为自己获取到了锁的情况,
  2. 然后继续,如果1用户的进程执行完了操作,删除了key,用户2进程未执行完毕,此时由于无法识别是否是自己加的锁,就删除了key,这时再有新的进程进入,检查不到锁,可以立即执行,则有可能和用户2的修改冲突。

针对错误1和错误2的第1点,我们只需要去除read & write模式就可以解决,解决方案为

    //同时设置val和过期时间,并使用setnx
    $status = $this->redis->setnx($key,$val,$expireTime);
    if($status){
         /**此处修改商品信息操作
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '错误提示';
    }

setnx,可以在设置时检查是否存在锁不存在则设置并返回1,如果存在不覆盖并返回0。

针对错误2第2点,我们需要为每个进程设置一个独立的自己可以识别的val,如果一个用户只能开一个进程,这个val可以为用户id,如果一个用户可以设置多个进程,那么必须按照实际车情况采用其他方式来区分,这里我们以用户id为例,并且在删除的时候只能删除自己的锁。那么这里问题又出现了,如果我们写成这样:

    //同时设置val和过期时间,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此处修改商品信息操作
                ******
        **/
        if($this->redis->get($key) == $userId){
            $this->redis->del($key);
        }
        
    }else{
        echo '错误提示';
    }

这种情况看似没有什么问题,其实不然,大家注意我再设置所得时候,设置了一个过期时间,假如这个时间设置的是4秒,那么如果进程A执行到删除前一刻一不小心超过了4秒,那么这个锁就自动消失了。而另一个进程B查到没有锁,就加了一把自己的锁,此时进程A执行删除,就把B的锁给删除了(极小概率事件)。

这里解决方案有两种

  1. 设置比较长的expire时间,弊端:设置的太长,占用内存时间长,设置的太短不能完全解决问题。(可能有人会想不设置过期时间就可以,那么回到最初的错误点,如果程序设置了锁后崩溃了就变成了永久的锁。)
  2. 把对比和删除弄成一个原子操作,这里呢找到了一个方法,就是用redis的eval,把语句变成原子操作。注意redis用的是lua语法,我也是新学的
     //同时设置val和过期时间,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此处修改商品信息操作
                ******
        **/
        //因为写这个博客的机器没有装redis,所以没有验证这个语法对不对。请大家见谅
         $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        $result = $this->redis->eval(script,array($key,$val),1);
        if ($result) {
            return true;
        }

    }else{
        echo '错误提示';
    }

这里就把两个操作变成了一个原子操作。解决的加锁和解锁可能出现的问题。

我们来说一些题外话拓展:在进程有可能出现冲突的地方,一般我们叫做临界区(操作系统中也有这个概念,是通过另一种叫做PV信号量的方式来解决的,其实可以理解为组织等待进程队列,P操作不能获取到资源使用权的则进入等待队列,等待V操作释放资源后,检查是否有等待队列,进行进程释放。当然PV操作也是原子性的。所以说解决相似问题的办法也有一定的相似性)。

                                                                 欢迎大家评论补充   ---  vinter_he
希望大家多评论交流,互相学习
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
NoSQL 算法 安全
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
154 0
|
19天前
|
NoSQL Java Redis
lua脚本做redis的锁
这段内容是关于使用Redis实现分布式锁的Java代码示例。`RedisLock`类包含`lock`和`unlock`方法,使用`StringRedisTemplate`和Lua脚本进行操作。代码展示了两种加锁方式:一种带有过期时间,另一种不带。还提到了在加锁和解锁过程中的异常处理,并提供了相关参考资料链接。
17 3
|
5月前
|
缓存 NoSQL 安全
Redis缓存雪崩、击穿、穿透解释及解决方法,缓存预热,布隆过滤器 ,互斥锁
Redis缓存雪崩、击穿、穿透解释及解决方法,缓存预热,布隆过滤器 ,互斥锁
185 5
|
1月前
|
NoSQL Java Redis
Redis分布式锁和Java锁的区别
Redis分布式锁和Java锁的主要区别在于它们的适用范围和实现机制。
41 2
|
1月前
|
运维 NoSQL Java
【Redis】6、Redisson 分布式锁的简单使用(可重入、重试机制...)
【Redis】6、Redisson 分布式锁的简单使用(可重入、重试机制...)
103 1
|
2月前
|
存储 监控 NoSQL
解锁Redis Stream新境界:高级用法大揭秘【二】
解锁Redis Stream新境界:高级用法大揭秘【二】
42 0
|
7月前
|
消息中间件 缓存 NoSQL
|
7月前
|
NoSQL Java 关系型数据库
基于springboot+Redis的前后端分离项目之分布式锁(四)-【黑马点评】
基于springboot+Redis的前后端分离项目之分布式锁(四)-【黑马点评】
|
4月前
|
NoSQL 算法 安全
用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁3
用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁
87 0
|
4月前
|
存储 NoSQL Java
用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁2
用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁
41 1

热门文章

最新文章