实现一个高并发的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脚本,锁重入等功能.

示例:

@Beanpublic 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);}@Autowiredprivate 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.hqwc.cn/news/232108.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TCP 基本认识

1:TCP 头格式有哪些? 序列号:用来解决网络包乱序问题。 确认应答号:用来解决丢包的问题。 2:为什么需要 TCP 协议? TCP 工作在哪一层? IP 层是「不可靠」的,它不保证网络包的交付…

leetCode 40.组合总和 II + 回溯算法 + 剪枝 + used数组 + 图解

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 注意:解集不能包含重复的组合 示例 1: 输入: candidates [10,1,2,7,6,1,5], t…

AD1668A 双N/P沟道 MOS管 耐压20V 过流2.1A 适用于正反接充电

AD1668A 双N/P沟道 MOS管 耐压20V 过流2.1A 的集成MOS管,封装TSOT23-8封装,体积小,适用于板子较小的板子。相当于2个SI2301、2个SI2302的集成模块。 芯片的内阻 N沟道的基本参数 P沟道的基本参数 这种结构的方式是适用于正反接都能充电的结构…

从0开始学习JavaScript--JavaScript中的解构赋值及使用场景

在现代JavaScript中,解构赋值是一种强大而灵活的语法特性,它允许从数组或对象中提取值并赋给变量。这种语法不仅使代码更简洁,而且提高了可读性。在本篇文章中,将深入探讨JavaScript中解构赋值的基本概念、语法规则以及丰富的使用…

JenKins快速安装与使用

一、JenKins 0.准备,配置好环境 1)Git(yum安装) 2)JDK(自行下载) 3)Jenkins(自行下载) 1.下载安装包 进官网,点Download下方即可下载。要下…

OpenCV项目开发实战--基本图像分割图生成器

欢迎回到我们有关 OpenCV 的系列文章以及我们如何利用其强大的图像预处理功能。在我们之前的文章的基础上,今天我们向您展示如何创建基本的图像分割图生成器。 具体来说,我们的图像掩模应该帮助识别每个像素是否: 背景的一部分(指定值为0)在感兴趣的对象的边缘(指定值 …

相机标定张正友、opencv和halcon对比(1)

本文将从基本标定开始,结合实际工作经验,分析张正友、opencv和halcon三者相机标定的深层原理与不同之处,内容比较多,如果出现错误请指正。 相机光学模型 我们使用的镜头都是由多组镜片组成,它实际上是一种厚透镜模型…

Java—学生信息管理系统(简单、详细)

文章目录 一、主界面展示二、学生类三、系统功能方法3.1 main()方法3.2 添加学生信息3.3 删除学生信息3.4 修改学生信息3.5 查看所有学生信息 四、完整代码4.1 Student .Java4.2 StudentManger.Java 前言:本案例在实现时使用了Java语言中的ArrayList集合来储存数据。…

中国毫米波雷达产业分析3——毫米波雷达市场分析(1~3)

一、总体市场 (一)总规模 近几年,得益于汽车智能化的高速发展与雷达芯片制作工艺的进步,国内毫米波雷达整体市场增速较快。根据初步测算,2022年中国毫米波雷达市场总规模达到86亿元,实现同比增长24.6%。 图…

【JUC】十六、LockSupport类实现线程等待与唤醒

文章目录 1、LockSupport2、wait和notify存在的问题3、await和signal存在的问题4、park和unpark方法5、LockSupport用法示例6、Permit不会累积7、面试 1、LockSupport 线程等待和唤醒的方式有: 使用Object的wait方法让对象上活动的线程等待,使用notify…

Docker中Alpine容器中配置MariaDB

1.更新镜像源 apk update2.安装 Mysql apk add --no-cache mysql mysql-client # 安装命令也可使用 apk add mariadb mariadb-client,alpine 中 mysql 就是 mariadb3. 安装openrc openrc是Alpine服务控制器,负责Alpine服务启动,添加、删除…

基于ASP.Net的图书管理系统的设计与实现

摘 要 图书馆管理系统是一整套高科技技术与书本管理知识结合的产物。它把传统书籍静态的服务这个缺陷完美化,完成多媒体数据的交互、远程网络连接、检查搜索智能化、多数据库无障碍联系、跨时空信息服务。图书管理系统用计算机程序替代了传统手工记录的工作模式&am…