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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于docker的onlyoffice使用--运行JavaSpringExample

背景 我之前看到有开源项目很好地集成了onlyoffice,效果要比kkfilepreview好(应当说应用场景不太一样)。本文是在window10环境,安装完Docker Desktop的基础上运行onlyoffice,并利用官网JavaSpringExample进行了集成。 …

福德植保无人机:农业科技的新篇章

一、引言随着科技的不断发展,无人机技术在许多领域中都得到了广泛的应用。近年来,福德植保无人机在农业领域大放异彩,成为了现代化农业的重要一环。本篇文章将为您详细介绍福德植保无人机的优势、特点以及未来发展趋势。 二、福德植保无人机的…

Linux socket编程(8):shutdown和close的区别详解及例子

在Linux中有两种操作可以终止socket间的进程通信:close和shutdown。但这两种函数在使用时有着不同的行为和效果。在网络编程中,正确地选择和使用这些操作至关重要,因为它们直接影响着通信的结束和资源的释放。本文将介绍close和shutdown函数&…

Thrift RPC Java、Go、PHP使用例子

文章目录 1、Thrift RPC介绍1.1、Protocol 支持的数据传输协议1.2、Transport 支持的数据传输方式1.3、Server 支持的服务模型1.4、IDL语法数据类型1.5、开发步骤 2、接口定义文件2.1、创建接口定义文件2.2、生成对应平台语言代码2.2.1、下载生成工具2.2.2、生成各平台语言代码…

单片机----串行通信

目录 串行通信的两种方式 串行通信的传输模式 串行通信的错误校验 1.奇偶校验 2.代码和校验 3.循环冗余码校验 串行口结构 串行口控制寄存器SCON 特殊功能寄存器PCON 串行口的4种工作方式 方式0: (1)方式0的发送过程 &#xff0…

数据结构---堆

1.堆的概念及结构 堆的性质&#xff1a; 堆中某个节点的值总是不大于或不小于其父节点的值堆总是一棵完全二叉树 2.举例说明 堆一般是把数组数据看做是一棵完全二叉树 小堆要求&#xff1a;任意一个父亲<孩子大堆要求&#xff1a;任意一个父亲>孩子 比如&#xff1…

计算机图形学:直线的扫描转换算法解析与实现

直线的扫描转换&#xff1a; DDA算法&#xff1a; 推理&#xff1a; 在计算机显示图形时&#xff0c;由于显示计算机的分辨率是有限的所以我们在绘制图形时需要将图形从连续量转换成离散量才能完成图形的绘制&#xff0c;直线的扫描转换就是将连续量转换为离散量的过程。 对…

UE Web Remote Control

前言 最近在研究UE自启WEB服务和网页通信以此来通过网页与UE进行数据交互&#xff0c;这样最好的方式就是可以摒弃掉整个繁琐的通信连接流程如TCP UDP&#xff0c;但是找到的一些方法都不是很适用&#xff0c;尤其是WEBUI这个插件它只适合内嵌到UE本身才能完成交互&#xff0c;…

mybatis关于namespace以及id以及Mapper接口命名的说明(了解)

1、建库建表 CREATE DATABASE mybatis-example;USE mybatis-example;CREATE TABLE t_emp(emp_id INT AUTO_INCREMENT,emp_name CHAR(100),emp_salary DOUBLE(10,5),PRIMARY KEY(emp_id) );INSERT INTO t_emp(emp_name,emp_salary) VALUES("tom",200.33); INSERT INTO…

Pytorch从零开始实战11

Pytorch从零开始实战——ResNet-50V2算法实战 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——ResNet-50V2算法实战环境准备数据集模型选择开始训练可视化总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c;Pyt…

CentOS 8 安装国内、本地YUM源

环境&#xff1a;windows 11、 VMware 17、Cent OS 8 目的&#xff1a;加快软件下载速度 1、国内YUM源安装 使用国外的源&#xff0c;速度卡到不显示 备份默认YUM源文件 [rootlocalhost ~]# cd /etc/yum.repos.d/ [rootlocalhost yum.repos.d]# mkdir yum.bak [rootlocalho…

Drawer抽屉(antd-design组件库)简单用法

1.Drawer抽屉 屏幕边缘滑出的浮层面板。 2.何时使用 抽屉从父窗体边缘滑入&#xff0c;覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务&#xff0c;操作完成后&#xff0c;可以平滑地回到原任务。 需要一个附加的面板来控制父窗体内容&#xff0c;这个面板在需要时…

3D场景建模工具

在线工具推荐&#xff1a; 三维数字孪生场景工具 - GLTF/GLB在线编辑器 - Three.js AI自动纹理化开发 - YOLO 虚幻合成数据生成器 - 3D模型在线转换 - 3D模型预览图生成服务 1. 什么是3D场景建模&#xff1f; 3D场景建模是一种通过计算机图形学技术&#xff0c;将现实世…

<Linux>冯诺依曼体系结构||操作系统||系统调用于用户操作接口

前言:本文从软硬件角度计算机解释软硬件结构 硬件—冯诺依曼体系结构 软件—操作系统 文章目录 冯诺依曼计算机体系结构背景理解举例 操作系统(OS)OS的管理为什么要有操作系统? 系统调用与用户操作接口系统调用用户操作接口引入:printf&&scanf的重新理解库函数 计算机…

Could NOT find resource [logback-test.xml]

修改 之后就可以正常启动了

Node.js案例 - 记账本

目录 项目效果 项目的搭建 ​编辑 响应静态网页 ​编辑 ​编辑 结合MongoDB数据库 结合API接口 进行会话控制 项目效果 该案例实现账单的添加删除查看&#xff0c;用户的登录注册。功能比较简单&#xff0c;但是案例主要是使用前段时间学习的知识进行实现的&#xff0c…

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

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

软件测试测试文档的编写和阅读

在软件测试中的流程中&#xff0c;测试文档也是一个重要的流程&#xff0c;所以测试人员也需要学习测试文档的编写和阅读。 一、定义&#xff1a; 测试文档&#xff08;Testing Documentation&#xff09;记录和描述了整个测试流程&#xff0c;它是整个测试活动中非常重要的文…

layui提示框没有渲染bug解决

bug&#xff1a;使用layui时或许是依赖导入又或是ideal和浏览器缓存问题导致前面明明正常的页面显示&#xff0c;后面出现提示框没有css样式&#xff0c;弹出框没有背景css 效果如下 解决后 解决方法 在你的代码中引入layer.js 我这是jsp页面 <script type"text/jav…

idea方法注释模版设置

方法上面的注释模版&#xff1a; Template text: ** Description $desc$ $param$ $return$* Aauthor yimeng* date $DATE$ $TIME$ **/param&#xff1a; groovyScript("def result ;def params \"${_1}\".replaceAll([\\\\[|\\\\]|\\\\s], ).split(,).toLis…