Redis实现分布式锁的五种方法是什么

1. 单机数据一致性

单机数据一致性架构如下图所示:多个可客户访问同一个服务器,连接同一个数据库。

Redis实现分布式锁的五种方法是什么

场景描述:客户端模拟购买商品过程,在Redis中设定库存总数剩100,多个客户端同时并发购买。

Redis实现分布式锁的五种方法是什么

@RestControllerpublic class IndexController1 {    @Autowired    StringRedisTemplate template;    @RequestMapping("/buy1")    public String index(){        // Redis中存有goods:001号商品,数量为100        String result = template.opsForValue().get("goods:001");        // 获取到剩余商品数        int total = result == null ? 0 : Integer.parseInt(result);        if( total > 0 ){            // 剩余商品数大于0 ,则进行扣减            int realTotal = total -1;            // 将商品数回写数据库            template.opsForValue().set("goods:001",String.valueOf(realTotal));            System.out.println("购买商品成功,库存还剩:"+realTotal +"件, 服务端口为8001");            return "购买商品成功,库存还剩:"+realTotal +"件, 服务端口为8001";        }else{            System.out.println("购买商品失败,服务端口为8001");        }        return "购买商品失败,服务端口为8001";    }}

使用Jmeter模拟高并发场景,测试结果如下:

Redis实现分布式锁的五种方法是什么

测试结果出现多个用户购买同一商品,发生了数据不一致问题!

解决办法:单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性

  • synchronized

  • ReentrantLock

@RestControllerpublic class IndexController2 {// 使用ReentrantLock锁解决单体应用的并发问题Lock lock = new ReentrantLock();@AutowiredStringRedisTemplate template;@RequestMapping("/buy2")public String index() {    lock.lock();    try {        String result = template.opsForValue().get("goods:001");        int total = result == null ? 0 : Integer.parseInt(result);        if (total > 0) {            int realTotal = total - 1;            template.opsForValue().set("goods:001", String.valueOf(realTotal));            System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001");            return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001";        } else {            System.out.println("购买商品失败,服务端口为8001");        }    } catch (Exception e) {        lock.unlock();    } finally {        lock.unlock();    }    return "购买商品失败,服务端口为8001";}}

Redis实现分布式锁的五种方法是什么

2. 分布式数据一致性

上面解决了单体应用的数据一致性问题,但如果是分布式架构部署呢,架构如下:

提供两个服务,端口分别为80018002,连接同一个Redis服务,在服务前面有一台Nginx作为负载均衡

Redis实现分布式锁的五种方法是什么

两台服务代码相同,只是端口不同

80018002两个服务启动,每个服务依然用ReentrantLock加锁,用Jmeter做并发测试,发现会出现数据一致性问题!

Redis实现分布式锁的五种方法是什么

3. Redis实现分布式锁

3.1 方式一

取消单机锁,下面使用redisset命令来实现分布式加锁

SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds 设置指定的到期时间(以秒为单位)

  • PX milliseconds 设置指定的到期时间(以毫秒为单位)

  • NX 仅在键不存在时设置键

  • XX 只有在键已存在时才设置

@RestControllerpublic class IndexController4 {    // Redis分布式锁的key    public static final String REDIS_LOCK = "good_lock";    @Autowired    StringRedisTemplate template;    @RequestMapping("/buy4")    public String index(){        // 每个人进来先要进行加锁,key值为"good_lock",value随机生成        String value = UUID.randomUUID().toString().replace("-","");        try{            // 加锁            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);            // 加锁失败            if(!flag){                return "抢锁失败!";            }            System.out.println( value+ " 抢锁成功");            String result = template.opsForValue().get("goods:001");            int total = result == null ? 0 : Integer.parseInt(result);            if (total > 0) {                int realTotal = total - 1;                template.opsForValue().set("goods:001", String.valueOf(realTotal));                // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,                // 释放锁操作不能在此操作,要在finally处理				// template.delete(REDIS_LOCK);                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001");                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001";            } else {                System.out.println("购买商品失败,服务端口为8001");            }            return "购买商品失败,服务端口为8001";        }finally {            // 释放锁            template.delete(REDIS_LOCK);        }    }}

上面的代码,可以解决分布式架构中数据一致性问题。但再仔细想想,还是会有问题,下面进行改进。

3.2 方式二(改进方式一)

在上面的代码中,如果程序在运行期间,部署了微服务jar包的机器突然挂了,代码层面根本就没有走到finally代码块,也就是说在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁

所以,这里需要对这个key加一个过期时间,Redis中设置过期时间有两种方法

  • template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)

  • template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)

名列前茅种方法需要单独的一行代码,且并没有与加锁放在同一步操作,所以不具备原子性,也会出问题

第二种方法在加锁的同时就进行了设置过期时间,所有没有问题,这里采用这种方式

调整下代码,在加锁的同时,设置过期时间:

// 为key加一个过期时间,其余代码不变Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);

这种方式解决了因服务突然宕机而无法释放锁的问题。但再仔细想想,还是会有问题,下面进行改进。

3.3 方式三(改进方式二)

方式二设置了key的过期时间,解决了key无法删除的问题,但问题又来了

上面设置了key的过期时间为10秒,如果业务逻辑比较复杂,需要调用其他微服务,处理时间需要15秒(模拟场

景,别较真),而当10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key,此时如果耗时15

的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题!

所以,谁上的锁,谁才能删除

@RestControllerpublic class IndexController6 {    public static final String REDIS_LOCK = "good_lock";    @Autowired    StringRedisTemplate template;    @RequestMapping("/buy6")    public String index(){        // 每个人进来先要进行加锁,key值为"good_lock"        String value = UUID.randomUUID().toString().replace("-","");        try{            // 为key加一个过期时间            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);            // 加锁失败            if(!flag){                return "抢锁失败!";            }            System.out.println( value+ " 抢锁成功");            String result = template.opsForValue().get("goods:001");            int total = result == null ? 0 : Integer.parseInt(result);            if (total > 0) {                // 如果在此处需要调用其他微服务,处理时间较长。。。                int realTotal = total - 1;                template.opsForValue().set("goods:001", String.valueOf(realTotal));                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001");                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001";            } else {                System.out.println("购买商品失败,服务端口为8001");            }            return "购买商品失败,服务端口为8001";        }finally {            // 谁加的锁,谁才能删除!!!!            if(template.opsForValue().get(REDIS_LOCK).equals(value)){                template.delete(REDIS_LOCK);            }        }    }}

这种方式解决了因服务处理时间太长而释放了别人锁的问题。这样就没问题了吗?

3.4 方式四(改进方式三)

在上面方式三下,规定了谁上的锁,谁才能删除,但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性,较好要保证对数据的操作具有原子性。

Redisset命令介绍中,最后推荐Lua脚本进行锁的删除,地址

@RestControllerpublic class IndexController7 {    public static final String REDIS_LOCK = "good_lock";    @Autowired    StringRedisTemplate template;    @RequestMapping("/buy7")    public String index(){        // 每个人进来先要进行加锁,key值为"good_lock"        String value = UUID.randomUUID().toString().replace("-","");        try{            // 为key加一个过期时间            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);            // 加锁失败            if(!flag){                return "抢锁失败!";            }            System.out.println( value+ " 抢锁成功");            String result = template.opsForValue().get("goods:001");            int total = result == null ? 0 : Integer.parseInt(result);            if (total > 0) {                // 如果在此处需要调用其他微服务,处理时间较长。。。                int realTotal = total - 1;                template.opsForValue().set("goods:001", String.valueOf(realTotal));                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001");                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001";            } else {                System.out.println("购买商品失败,服务端口为8001");            }            return "购买商品失败,服务端口为8001";        }finally {            // 谁加的锁,谁才能删除,使用Lua脚本,进行锁的删除            Jedis jedis = null;            try{                jedis = RedisUtils.getJedis();                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +                        "then " +                        "return redis.call('del',KEYS[1]) " +                        "else " +                        "   return 0 " +                        "end";                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));                if("1".equals(eval.toString())){                    System.out.println("-----del redis lock ok....");                }else{                    System.out.println("-----del redis lock error ....");                }            }catch (Exception e){            }finally {                if(null != jedis){                    jedis.close();                }            }        }    }}

3.5 方式五(改进方式四)

在方式四下,规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了。所以直接上RedLockRedisson落地实现。

@RestControllerpublic class IndexController8 {    public static final String REDIS_LOCK = "good_lock";    @Autowired    StringRedisTemplate template;    @Autowired    Redisson redisson;    @RequestMapping("/buy8")    public String index(){        RLock lock = redisson.getLock(REDIS_LOCK);        lock.lock();        // 每个人进来先要进行加锁,key值为"good_lock"        String value = UUID.randomUUID().toString().replace("-","");        try{            String result = template.opsForValue().get("goods:001");            int total = result == null ? 0 : Integer.parseInt(result);            if (total > 0) {                // 如果在此处需要调用其他微服务,处理时间较长。。。                int realTotal = total - 1;                template.opsForValue().set("goods:001", String.valueOf(realTotal));                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001");                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001";            } else {                System.out.println("购买商品失败,服务端口为8001");            }            return "购买商品失败,服务端口为8001";        }finally {            if(lock.isLocked() && lock.isHeldByCurrentThread()){                lock.unlock();            }        }    }}

读到这里,这篇“Redis实现分布式锁的五种方法是什么”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。

文章标题:Redis实现分布式锁的五种方法是什么,发布者:亿速云,转载请注明出处:https://worktile.com/kb/p/25427

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
亿速云的头像亿速云认证作者
上一篇 2022年9月15日
下一篇 2022年9月15日

相关推荐

  • easyrecovery如何恢复U盘

    easyrecovery恢复U盘的方法 1、打开软件,找到你要恢复的文件类型,点击下一步。 2、选择你要恢复的硬盘。 3、手动查找需要恢复文件,这里有三种板块功能,可以都点开看看,找到你想要的那个文件。 4、等你找到你想要恢复的文件之后,点击恢复就可以了。 到此,相信大家对“easyrecovery…

    2022年9月26日
    40500
  • 管理事业单位和行政机关有啥具体区别

    管理事业单位和行政机关的具体区别在性质和法律地位、组织目标、组织结构和管理方式、财务状况和资金来源、绩效评估和监督机制等方面。详细介绍:1、性质和法律地位,管理事业单位是指由国家机关、地方政府或其他法定机构委托或授权管理的、具有独立法人地位的非营利性组织,在法律上独立于政府,有自己的法人资格,享有相应的权益和责任,而行政机关是政府的一部分,属于行政机构的一种等等。

    2023年10月19日
    42700
  • TraceId怎么搭配ELK使用

    需求分析 先分析一下,我们想实现的核心功能是搜索,必然是用 ES 实现,那问题就转换成如何将日志收集并存储到 ES。 日志大家都不陌生了,可以在控制台打印,也可以存入文件,那能不能直接输入 ES 呢,好像没听说过。 这里就要用到 Logstash 来收集日志,Spring 默认的日志框架 Logba…

    2022年9月20日
    70100
  • Java Bean作用域是什么及怎么实现

    PS:Java 中的公共类可称之为 Bean 或 Java Bean。 1.作用域 Bean 的作用域是指 Bean 在 Spring 整个框架中的某种行为模式。比如 singleton 单例作用域,就表示 Bean 在整个 Spring 中只有一份,它是全局共享的,当有人修改了这个值之后,那么另一…

    2022年8月31日
    1.1K00
  • 如何浅谈APP存在的安全问题

    1 背景分析 互联网时代到来时,人们曾经感慨,一切都在被数字化。而今天,一切都在移动化。在大街小巷民众低头忙着划动屏幕成为一景。据国外权威机构统计,中国智能手机已经占到手机总销量的96%,古老的功能手机,基本上退出了历史舞台。据美国市场研究公司eMarketer最近的一个报告称,权威市调机构Gart…

    2022年9月18日
    56500
  • windows todesk远程软件安全吗

    todesk远程软件安全吗: 答:todesk远程软件非常安全。 1、客户端与中心服务器和高速通道集群通讯都采用高强度AEAD(Authenticated Encryption with Additional Data) xchacha20-ietf-poly1305算法。 2、在用户完成远程连接后…

    2022年9月20日
    1.1K00
  • ASP.NET Core实现中间件的方式有哪些

    匿名函数 通常新建一个空的 ASP.NET Core Web Application,项目名字无所谓啦 在启动类里可以看到这么一句: // Startup.cs// …app.Run(async (context) =>{ await context.Response.WriteAsync…

    2022年8月29日
    47700
  • 电脑0x0000001a蓝屏代码怎么解决

    解决方法: 名列前茅种方法: 1、通过蓝屏代码提示,我们分析出这个蓝屏出错代码的缘由是: 代码:0x0000001a 原因:指定的磁盘或磁盘无法存取。 2、ntoskrnl.exe文件是N卡(显卡)的驱动程序文件。如果之前更新过了显卡的驱动程序,那就是它惹的祸。不妨先回滚驱动程序。 3、不行,再找到…

    2022年9月16日
    1.7K00
  • javascript数值型只有一种吗

    javascript数值型只有一种:浮点类型。JavaScript内部存储数字都是按64位浮点类型存储的,所以在JavaScript中实际上是没有整数类型的。按照JavaScript中的数字格式能够表示的整数范围为“[-2^53 ~ 2^53]”,包含边界值;但需要注意的是,数组索引、位操作符等使用…

    2022年9月24日
    50600
  • 如何进行Web权限维持分析

    前言 权限维持,在红蓝对抗中,我觉得其意义在于两点:一是防止已获取的权限被蓝队破坏;二是防止其他红队获取到相同的权限(虽然有点缺德。。。)。 其他情况下的非法用途就不说了。 权限维持的原则我觉得就是不能影响原来业务的正常运行。(比如改后台密码导致管理员不能登录,修改文件夹读写权限导致正常的文件不能上…

    2022年9月24日
    49200
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部