Redis高并发分布式锁

这篇具有很好参考价值的文章主要介绍了Redis高并发分布式锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

分布式环境下高并发访问衍生的问题

针对单机环境下的并发访问,可以通过锁机制(Syschronized或独占锁等)来进行控制,使得一个资源在一段时间内只能被一个线程访问;但在多服务器的分布式环境下,并发访问同一个资源,可能会导致被同时修改或更新,原因在于juc包下的并发控制机制,都是基于JVM层面的,而分布式环境下的多服务器场景,每一个部署了应用的Tomcat服务器都有一个自己的JVM,属于JVM层的锁是无法跨JVM进行资源独占保护的。

以商品减少库存为例,假设库存是100

在redis上设置初始库存

Redis高并发分布式锁

 背景

正常单机环境下,库存会按顺序减少,且不同的线程在库存足够的情况下都可以获得不同的商品。但在集群的环境下,为了减轻服务器的压力,都会有一个负载均衡,或是nginx或是ribbon或F5等其他的方式,根据不同的负载均衡策略,将请求分发到集群的各个服务器上,此时可能会有多个请求同时下单成功,且库存只减少了一次

解决方法

为了解决该问题,可以在减库存的操作发生前,加一把锁,只有获取这把锁的请求,才能下单成功;当一个请求获取到了锁,在它执行完减库存释放锁之前,其他请求只能等待。

通过Redis的SetNx命令,其特点为,当key不存在才能设置成功;否则不做任何操作返回false

@RequestMapping("/deduct_stock")
public String deductStock() {
    String countKey = "商品id";
    
    boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, "zhangsan");
    if(!lock){
        return "没获取到锁";
    }
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
    if (stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
        System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
        System.out.println("扣减失败,库存不足");
    }
    //下单成功 释放锁
    stringRedisTemplate.delete(countKey);
    return "end";
    }

衍生问题1

针对无异常的情况,上述方案可以解决同一商品被下单多次的问题,但若是下单过程中出现异常,锁未释放就中断,会导致后续的请求都无法下单这一类的商品,因此要有异常处理

@RequestMapping("/deduct_stock")
public String deductStock() {
    String countKey = "商品id";
    
    boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, "zhangsan");
    if(!lock){
        return "没获取到锁";
    }
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //下单成功 释放锁
        stringRedisTemplate.delete(countKey);
    }
    
    return "end";
    }

衍生问题2

即使是出现异常的情况,该锁也会释放,不会影响到其他的请求访问,但也有可能出现应用宕机或投产关停在释放锁之前的情况,因此,最合适的方法是设置过期时间

@RequestMapping("/deduct_stock")
public String deductStock() {
    String countKey = "商品id";
    //设置值和key的过期时间,最好是合并成一个原子操作,保证上了锁不会因程序中断而无法释放
    //            boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, "zhangsan");
    //            stringRedisTemplate.expire(countKey, 10, TimeUnit.SECONDS);
    boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, "zhangsan",10,TimeUnit.SECONDS);
    if(!lock){
        return "没获取到锁";
    }
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //下单成功 释放锁
        stringRedisTemplate.delete(countKey);
    }
    
    return "end";
    }

衍生问题3

这种处理可以在并发不大的场景下进行使用,但在高并发的大量请求下,还有可能衍生一种新的问题,锁失效:指的是因为并发高了或者其他原因,导致业务逻辑执行的时间超过了锁的过期时间,在请求1未执行完时锁就过期了,此时库存的数量可能还未减少;但另一个请求2因为锁过期,可以进行下单操作,在该请求2还未执行结束时,请求1执行结束了,此时它会执行delete锁的操作,这个时候会导致请求2的锁被请求1释放,以此类推,redis中设置的这个锁的存在时间可能会越来越短,甚至会刚设置好就被上一个或其他请求给清理。

针对这种情况,在释放锁的时候,应该添加一个唯一标识,只有是当前请求进来设置的锁,才能释放

@RequestMapping("/deduct_stock")
public String deductStock() {
    String countKey = "商品id";
    //
    //            boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, "zhangsan");
    //            stringRedisTemplate.expire(countKey, 10, TimeUnit.SECONDS);
    //增加唯一标识
    String primaryKey = UUID.randomUUID().toString();
    boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(countKey, primaryKey,10,TimeUnit.SECONDS);
    if(!lock){
        return "没获取到锁";
    }
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //下单成功 有当前请求设置的锁,才能删除
        if(primaryKey.equals(stringRedisTemplate.opsForValue().get(countKey))){
            stringRedisTemplate.delete(countKey);
        }
        
    }
    
    return "end";
    }

但即使这种情况,由于判断+释放是两步操作,也不是一个原子性的,可以通过锁续命来处理,其实现机制为,启动一个子线程来判断执行业务逻辑的线程是否执行完,若没有,重置其过期时间,以避免,判断后卡顿导致锁未释放。

RedisSon

是一个基于Redis的扩展类库,针对以上的过期时间早于程序的释放锁问题,其提供了良好的解决方案,即通过类似redis中的setNx的操作,加上lua脚本来达到控制分布式系统下并发请求的安全性。

Lua脚本

在Redis中,它的好处在于,可以将多个操作写到一起,组成一个原子性的操作,例如:下面的操作是要先获取key的值进行判断,然后才能进行delete这是两步的操作;但若是使用Lua脚本,就可以将这两个步骤整合到一个Lua脚本中去执行,而Lua脚本的执行是具有原子性的。文章来源地址https://www.toymoban.com/news/detail-430927.html

@RequestMapping("/deduct_stock")
public String deductStock() {
    String countKey = "商品id";
    //获取锁对象
    RLock rLock = redisson.getLock(countKey);
    //加锁
    rLock.lock();
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    }
    //解锁
    rLock.unlock();  
    return "end";
    }

到了这里,关于Redis高并发分布式锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 削峰填谷与应用间解耦:分布式消息中间件在分布式环境下并发流量控制的应用

    这是《百图解码支付系统设计与实现》专栏系列文章中的第(18)篇,也是流量控制系列的第(4)篇。点击上方关注,深入了解支付系统的方方面面。 本篇重点讲清楚分布式消息中间件的特点,常见消息中间件的简单对比,在支付系统的应用场景,比如削峰填谷,系统应用间

    2024年01月20日
    浏览(66)
  • 集群高并发环境下如何保证分布式唯一全局ID生成?

    在集群高并发环境下保证分布式唯一全局ID生成是一个具有挑战性的问题。下面笔者将为大家提供几种常见的解决方案: UUID是一个128位的全局唯一标识符,它可以在不同的计算机和时间上生成。UUID的生成是基于MAC地址、时间戳等信息,因此可以保证在分布式环境下的唯一性

    2024年02月13日
    浏览(47)
  • 精确掌控并发:滑动时间窗口算法在分布式环境下并发流量控制的设计与实现

    这是《百图解码支付系统设计与实现》专栏系列文章中的第(15)篇,也是流量控制系列的第(2)篇。点击上方关注,深入了解支付系统的方方面面。 上一篇介绍了固定时间窗口算法在支付渠道限流的应用以及使用redis实现的核心代码。 本篇重点讲清楚滑动时间窗口算法原理

    2024年01月22日
    浏览(61)
  • 精确掌控并发:固定时间窗口算法在分布式环境下并发流量控制的设计与实现

    这是《百图解码支付系统设计与实现》专栏系列文章中的第(14)篇。点击上方关注,深入了解支付系统的方方面面。 本篇主要介绍分布式场景下常用的并发流量控制方案,包括固定时间窗口、滑动时间窗口、漏桶、令牌桶、分布式消息中间件等,并重点讲清楚固定时间窗口

    2024年01月19日
    浏览(58)
  • Redis集群(分布式缓存):详解持久化、主从同步原理、哨兵机制、Cluster分片集群,实现高并发高可用

            单机式Redis存在以下问题,因此需要Redis集群化来解决这些问题        Redis数据快照,简单来说就是 把内存中的所有数据都记录到磁盘中 。当Redis实例故障重启后,从 磁盘读取快照文件,恢复数据 。快照文件称为RDB文件,默认是保存在当前运行目录。     (1)

    2024年02月08日
    浏览(58)
  • redis集群和分片-Redis Cluster:分布式环境中的数据分片、主从复制和 Sentinel 哨兵

    当涉及到 Redis 中的集群、分片、主从复制和 Sentinel 哨兵时,这些是构建分布式 Redis 环境中非常重要的概念和组件。下面详细介绍这些概念以及它们在分布式环境中的作用。 Redis Cluster 是 Redis 官方提供的分布式解决方案,用于管理和维护多个 Redis 节点的分布式数据存储。R

    2024年02月13日
    浏览(60)
  • Redis实现方式开启新篇章,解决分布式环境下的资源竞争问题,提升系统稳定性

    分布式锁一般有三种实现方式: 数据库乐观锁; 基于Redis的分布式锁; 基于ZooKeeper的分布式锁 本篇博客将介绍第二种方式,基于Redis实现分布式锁。 虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将

    2024年02月07日
    浏览(44)
  • 分布式调用与高并发处理 Zookeeper分布式协调服务

    单机架构 一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上,整个项目所有的服务都由这台服务器提供。 缺点: 服务性能存在瓶颈,用户增长的时候性能下降等。 不可伸缩性 代码量庞大,系统臃肿,牵一发动全身 单点故障

    2024年02月12日
    浏览(64)
  • 论文-分布式-并发控制-并发控制问题的解决方案

    目录 参考文献 问题 解法与证明 易读版本 参考文献 Dijkstra于1965年发表文章Solution of a Problem in Concurrent Programming Control,引出并发系统下的互斥(mutual exclusion)问题,自此开辟了分布式计算领域 Dijkstra在文中给出了基于共享存储原子性访问的解决方案只有十多行代码,但阅读起来

    2024年02月08日
    浏览(49)
  • 服务端⾼并发分布式结构演进之路

    应⽤(Application)/系统(System) 为了完成一整套服务的一个程序或相互配合的程序群 模块(Module)/组件(Component) 当应⽤较复杂时,为了分离职责,将其中具有清晰职责的、内聚性强的部分,抽象出概念,便于理解 分布式(Distributed) 分布式(Distributed)是指将计算、任务

    2024年02月13日
    浏览(95)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包