实现一个高并发的Redis分布式锁

news2024/11/19 19:17:27

1. 无锁场景

下面是一个扣减库存逻辑, 由于查库存和扣减库存两个操作不是原子的,明显存在并发超卖问题

    // 假设初始库存200
    @GetMapping("/stock")
    public String stock(@RequestParam(value = "name", defaultValue = "World") String name) {
        String key = "product:101";
        Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
        if (stock > 0) {
            stock = stock - 1;
            redisTemplate.opsForValue().set(key, stock.toString());
            System.out.println("成功扣减库存, 还剩" + stock);
        } else {
            throw new RuntimeException("缺货");
        }
        return "200";
    }

压测结果: 1000人抢200库存商品, 卖出731件,存在超卖问题

2. 单机环境,加synchronized锁

    private static Object STOCK_LOCK = new Object();
    
    // 假设初始库存200
    @GetMapping("/stock")
    public String stock(@RequestParam(value = "name", defaultValue = "World") String name) {
        String key = "product:101";
        synchronized (STOCK_LOCK) {
            Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
            if (stock > 0) {
                stock = stock - 1;
                redisTemplate.opsForValue().set(key, stock.toString());
                System.out.println("成功扣减库存, 还剩" + stock);
                return "200";
            }
        }
        throw new RuntimeException("缺货");
    }

压测结果:1000人抢200库存商品, 卖出200件,用例成功

3. 分布式环境,加synchronized锁

准备:这里启动两个节点, 用nginx负载均衡

压测结果:1000人抢200库存商品, 卖出310件,存在超卖问题

4. 分布式环境,redis setnx分布式锁

基础版

主要代码逻辑:

  1. 用setIfAbsent(setnx封装)加锁,同时设置超时时间,锁力度到具体商品
  2. 获取锁后执行减库存逻辑
  3. 执行成功释放锁

代码:

// 假设初始库存200
    @GetMapping("/stock2")
    public String stock2(@RequestParam(value = "name", defaultValue = "World") String name) {
        String key = "product:101";
        String lockKey = "lock:" + key;
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
        if (result) {
            try {
                Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
                if (stock > 0) {
                    stock = stock - 1;
                    redisTemplate.opsForValue().set(key, stock.toString());
                    System.out.println("成功扣减库存, 还剩" + stock);
                    return "200";
                }
            } finally {
                redisTemplate.delete(lockKey);
            }
        }
        throw new RuntimeException("缺货");
    }

压测结果:1000人抢200库存商品, 卖出182件,剩余库存18件,业务正常

在低并发,服务器理想情况下, 业务正常,但是还存在一些问题

问题1

现在写死的锁过期时间30秒,但是在服务器压力大时, 接口耗时不稳定, 可能超过过期时间, 锁自动失效, 可能导致超卖

解决:锁续命, 开启一个后台线程, 如果业务没执行完,给锁延长过期时间.

问题2

A线程业务执行完, 准备释放锁时, 肯能刚好锁自动过期,这时候B线程进来抢占到锁正在执行业务,A线程开始删除锁, 此时其他线程都可能去拿到锁,保证不了同步

解决: 释放锁时,判断只有加锁线程才有资格去删除锁

    @GetMapping("/stock3")
    public String stock3(@RequestParam(value = "name", defaultValue = "World") String name) {
        String key = "product:101";
        String lockKey = "lock:" + key;
        String clientId = UUID.randomUUID().toString();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        if (result) {
            try {
                Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
                if (stock > 0) {
                    stock = stock - 1;
                    redisTemplate.opsForValue().set(key, stock.toString());
                    System.out.println("成功扣减库存, 还剩" + stock);
                    return "200";
                }
            } finally {
                // 只能删除自己加的锁, 不让其他线程删
                if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
                    /* ...  */
                    redisTemplate.delete(lockKey);
                }
            }
        }
        throw new RuntimeException("缺货");
    }

问题3

但是问题2还没彻底解决, 因为比较clientId和删除锁这两个操作不是原子的, 如果中间卡顿,卡顿期间锁刚好自动过期,其他线程占有锁, 这里再执行删除锁就会误删别人锁.

解决: 可用lua脚本执行批量命令,保证原子性

Redisson分布式锁

Redisson是专门处理分布式场景使用Redis的组件, 里面就封装了锁续命,只删自己加的锁,lua脚本,锁重入等功能.

示例:

@Bean
    public Redisson redisson(RedisProperties redisProperties) {
        // 此为单机模式
        Config config = new Config();
        config.useClusterServers().setNodeAddresses(redisProperties.getCluster().getNodes()
                .stream().map(node -> "redis://" + node).collect(Collectors.toList()));
        return (Redisson) Redisson.create(config);
    }

    @Autowired
    private Redisson redisson;

    // 假设初始库存200
    @GetMapping("/stock4")
    public String stock4(@RequestParam(value = "name", defaultValue = "World") String name) {
        String key = "product:101";
        String lockKey = "lock:" + key;
        RLock rLock = redisson.getLock(lockKey);
        // 尝试加锁, 加锁失败会间歇阻塞再次加锁, 直至成功
        rLock.lock();
        try {
            Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
            if (stock > 0) {
                stock = stock - 1;
                redisTemplate.opsForValue().set(key, stock.toString());
                System.out.println("成功扣减库存, 还剩" + stock);
                return "200";
            }
        } finally {
            rLock.unlock();
        }
        throw new RuntimeException("缺货");
    }

压测结果:1000人抢200库存商品, 卖出200件,用例成功

ReadLock

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1269744.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux下配置邮箱客户端MUTT,整合msmtp + procmail + fetchmail

一、背景 在向 Linux kernel 社区提交patch补丁步骤总结(已验证成功)_kernel补丁-CSDN博客文章中提到如何向kernel社区以及其他类似如qemu、libvirt社区提交patch的详细步骤,但还有一点不足的是通过git send-email这种方法基本是只能发送patc…

详解原生Spring当中的额外功能开发MethodBeforeAdvice与MethodInterceptor接口!

😉😉 学习交流群: ✅✅1:这是孙哥suns给大家的福利! ✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 🥭🥭3:QQ群:583783…

论文学习-Attention Is All You Need

Attention Is All You Need 目前暂时不会用到,大概了解一下即可。 Recurrent model 序列化的计算方式,难以并行,随着序列的增长,以前的记忆会逐渐丢失。而Attention机制可以观察到句子中所有的信息,不受距离影响&…

国密加密工业路由器 数据安全升级

国密加密工业路由器,简称国密加密路由器,是指遵循“商用密码管理规范”中规定的国家商用密码算法,采用国密加密芯片和密码算法的专业路由器。相比-般路由器,国密加密路由器具有更高级别的加密保护,可以有效提高数据传输…

线性表——(2)线性表的顺序存储及其运算的实现

归纳编程学习的感悟, 记录奋斗路上的点滴, 希望能帮到一样刻苦的你! 如有不足欢迎指正! 共同学习交流! 🌎欢迎各位→点赞 👍 收藏⭐ 留言​📝 看到美好,感受美好&a…

Java微服务框架 HP-SOA 1.0.5 — 完整支持 Spring Cloud 和 Dubbo

HP-SOA 功能完备,简单易用,高度可扩展的Java微服务框架。 项目主页 : https://www.oschina.net/p/hp-soa下载地址 : https://github.com/ldcsaa/hp-soa开发文档 : https://gitee.com/ldcsaa/hp-soa/blob/master/README.mdQQ Group: 44636872, 66390394…

SQL语法实践(三):一些问题

Q:What’s the difference between INNER JOIN, LEFT JOIN, RIGHT JOIN and F ULL JOIN? [duplicate] A:点击跳转链接到原文 There are different types of joins available in SQL: INNER JOIN: returns rows when there is a match in both tables. LEFT JOIN: returns al…

J2EE征程——第一个纯servletCURD

第一个纯servletCURD 前言在此之前 一,概述二、CURD1介绍2查询并列表显示准备实体类country编写 CountryListServlet配置web.xml为web应用导入mysql-jdbc的jar包 3增加准备增加的页面addc.html编写 CAddServlet配置web.xml测试 4删除修改CountryListServlet&#xf…

[Docker]十二.Docker consul集群搭建、微服务部署,Consul集群+Swarm集群部署微服务实战

一.Docker consul集群搭建 Consul 是 Go 语言写的开源的服务发现软件, Consul 具有 服务发现、健康检查、 服务治理、微服务熔断处理 等功能,在微服务中讲过如何搭建consul集群,接下来看看在 Dokcer 中如何去创建搭建consul 集群 1.linux上面部署consul集…

【Flutter】graphic图表实现tooltip一段时间后自动隐藏

概述 graphic图表中提供了自定义tooltip的事件,可通过selections中on和clear配置手势选项和可识别设备,默认情况下tooltip需要双击隐藏,但这并不符合我们的需求。通过调研发现,若想实现tooltip隔几秒后隐藏,可通过Str…

从0开始学习JavaScript--JavaScript 中 `let` 和 `const` 的区别及最佳实践

在JavaScript中,let 和 const 是两个用于声明变量的关键字。尽管它们看起来很相似,但它们之间有一些重要的区别。本篇博客将深入探讨 let 和 const 的用法、区别,并提供一些最佳实践,以确保在代码中正确使用它们。 let 和 const …

36 - 电商系统表设计优化案例分析

如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈。如果表结构扩展性差,业务耦合度将会越来越高,系统的复杂度也将随之增加。这一讲我将以电商系统中的表结构设计为例…

iOS NSDate的常用API

目录 一、创建日期 1.获取当前时间 2.当前时间指定秒数之后/前的时间 3.指定日期之后/后的时间 4.2001年之后/前指定秒数的时间 5.1970年之后/后指定秒数的时间 二、初始化日期 1.init 2.时间间指定秒数的时间 3.指定时间指定秒数之前/后的时间 4.2001年指定秒数之后…

网络相关-面试高频

网络 当前的应用系统主要分两大类,一类是C/S(Client/Server)客户端/服务器架构的,一类是B/S(Browser/Server)浏览器/服务器架构的[3],例如:PC上安装的QQ程序是典型的C/S架构中的客户…

C# Onnx 阿里达摩院开源DAMO-YOLO目标检测

效果 模型信息 Inputs ------------------------- name:images tensor:Float[1, 3, 192, 320] --------------------------------------------------------------- Outputs ------------------------- name:output tensor:Float…

人工智能-优化算法之动量法

对于嘈杂的梯度,我们在选择学习率需要格外谨慎。 如果衰减速度太快,收敛就会停滞。 相反,如果太宽松,我们可能无法收敛到最优解。 泄漏平均值 小批量随机梯度下降作为加速计算的手段。 它也有很好的副作用,即平均梯度…

HMM(Hidden Markov Model)详解——语音信号处理学习(三)(选修一)

参考文献: Speech Recognition (Option) - HMM哔哩哔哩bilibili 2020 年 3月 新番 李宏毅 人类语言处理 独家笔记 HMM - 6 - 知乎 (zhihu.com) 隐马尔可夫(HMM)的解码问题维特比算法 - 知乎 (zhihu.com) 本次省略所有引用论文 目录 一、介绍 二、建模单…

解决uview中uni-popup弹出层不能设置高度问题

开发场景:点击条件筛选按钮,在弹出的popup框中让用户选择条件进行筛选 但是在iphone12/13pro展示是正常,但是切换至其他手机型号就填充满了整个屏幕,需要给这个弹窗设置一个固定的高度 iphone12/13pro与其他型号手机对比 一开始…

关于使用若依,并不会自动分页的解决方式

关于使用若依,并不会自动分页的解决方式 如果只是单纯的使用一次查询list,并不会触发这个bug 例如: 但是我们如果对里面的数据进行调整修改的话就会触发这个bug 例如: 此时可以看到我对数据进行了转换!!!,这时如果超出数据10条,实际我们拿到的永远是10条,具体原因这里就不展…