当前位置: 首页 > news >正文

使用Redis实现分布式锁,基于原本单体系统进行业务改造

一、单体系统下,使用锁机制实现秒杀功能,并限制一人一单功能

1.流程图:

2.代码实现:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIDWorker redisIDWorker;/*** 下单秒杀券* @param voucherId* @return*/@Overridepublic Result seckillVoucher(Long voucherId) {//1.查询优惠券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);//2.判断当前时间是否在优惠券的开始时间和结束时间内if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){return Result.fail("优惠券秒杀尚未开始");}if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("优惠券秒杀已经结束");}//3.判断库存是否充足if (seckillVoucher.getStock() < 1){return Result.fail("库存不足");}//加上锁Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){//事务提交后才释放锁,避免事务未提交产生的线程安全问题//拿到代理对象-这样事务才会生效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {//4.一人一单//4.1 查询数据库中是否有当前用户购买此秒杀券的记录Long userId = UserHolder.getUser().getId();long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count>0){return Result.fail("此用户已经购买过一次了,不能重复购买");}//5.扣减库存boolean isDiscount = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0)//乐观锁:使用stock代替版本号- 减库存之前判断库存是否充足,防止超卖.update();if(!isDiscount){return Result.fail("库存不足");}//6.创建订单并保存VoucherOrder voucherOrder = new VoucherOrder();long orderId = redisIDWorker.nextId("order");voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(userId);voucherOrder.setId(orderId);save(voucherOrder);//7.返回订单IDreturn Result.ok(orderId);}
}

3.存在的并发安全问题

        这里我们是使用用户ID作为锁对象的,但是在分布式系统下,有多台JVM,其内存空间是各自独立的,此时虽然用户ID的值是一样的,但是其userId.toString().intern()方法返回的对象只能保证在同一个JVM上是相同的,不同的JVM使用serId.toString().intern()方法返回的对象是不同的。

        因此,对于不同JVM的线程,当前使用的方式并不能解决线程安全问题,也即,这种方法无法适配分布式系统。

4.解决方案-引入分布式锁

由于引发的问题是因为 :

        在不同的JVM中获得到的锁对象是不同的,因此只要让它们获得的锁对象是同一个,那就可以解决这个问题。

4.1分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

4.2分布式锁的实现

二、使用Redis实现分布式锁

1.原理:

实现分布式锁时需要实现的两个基本方法:

(1)获取锁:

        互斥:确保只能有一个线程获取锁

        非阻塞:尝试一次,成功返回true,失败返回false

(2)释放锁:

        手动释放

        超时释放:设置过期时间

2.流程图:

3.代码改造:

1.ILock接口:(后续实现的锁都要实现这个接口)

public interface ILock {/*** 尝试获取锁* @param timeOutSec* @return true代表获取锁成功,false代表获取锁失败*/boolean tryLock(Long timeOutSec);/*** 释放锁*/void unlock();
}

2.SimpleRedisLock:

public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;private String name;private static final String KEY_PREFIX = "lock:";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long timeOutSec) {//使用String 类型来实现锁,如果key存在,则无法设置新的值//value 为当前线程ID,为后面释放锁做准备String threadId = Thread.currentThread().getId() + "";Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);return BooleanUtil.isTrue(isLock);}@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_PREFIX+name);}
}

3.Service代码改造:

将 seckillVoucher 中的:

         //加上锁Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){//事务提交后才释放锁,避免事务未提交产生的线程安全问题//拿到代理对象-这样事务才会生效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}

修改为: 

        //加上锁Long userId = UserHolder.getUser().getId();SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);boolean isLock = lock.tryLock(10L);if(!isLock){return Result.fail("请勿重复下单");}//拿到代理对象-这样事务才会生效try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {lock.unlock();}

        这样就实现了redis分布式锁在秒杀业务中的应用。

4.当前存在的问题-误删问题:

如上图,线程1在成功获取到锁,执行业务的时候发生阻塞,可能会由于超时释放锁。

此时线程2成功获取到锁,也在执行自己的业务,在线程2执行业务的时候,线程1又开始运行,在线程1执行完成之后会去释放这个锁。

此时如果线程3去获取锁,也能获取成功。

.....

这就是当前代码存在的线程不安全问题。

5.解决方案-改进Redis的分布式锁

5.1 解决误删除-step1:删前判断

需求:修改之前的分布式锁实现,满足:

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致 如果一致则释放锁 如果不一致则不释放锁

5.1.1 流程图

5.1.2 改造代码:

修改SimpleRedisLock 如下:

public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;private String name;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//用于区分不同jvm的线程public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long timeOutSec) {//使用String 类型来实现锁,如果key存在,则无法设置新的值//value 为当前线程ID,为后面释放锁做准备String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);return BooleanUtil.isTrue(isLock);}@Overridepublic void unlock() {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了//获取redis中的值String key = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);if(threadId.equals(key)){//一致的话,删除锁stringRedisTemplate.delete(KEY_PREFIX+name);}}
}

        可以发现,在释放锁(unlock)代码中,判断一致和删除锁是非原子性的,那么也会引发线程安全问题:

可以看到,线程1在判断一致之后阻塞,在线程1阻塞期间由于超时,锁被释放。

线程2此时获取锁成功,在执行业务的时候,线程1阻塞结束,将锁删除,那还是会存在误删的情况。

这里引入lua脚本来解决原子性问题

5.2 解决误删除-step2:使用Lua脚本实现原子性

        Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法可以参考网站:https://www.runoob.com/lua/lua-tutorial.html

(1)unlock.lua:

-- 比较线程标识与锁中的标识是否一致
if(redis.call('get',KEYS[1])== ARGV[1])then-- 删除锁return redis.call('del',KEYS[1])
end
return 0

(2)修改 SimpleRedisLock如下:

public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;private String name;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//用于区分不同jvm的线程private static final DefaultRedisScript<Long>UNLOCK_SCRIPT;static {//类加载的时候初始化,不用重复初始化UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//设置lua脚本位置UNLOCK_SCRIPT.setResultType(Long.class);//设置返回类型}public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long timeOutSec) {//使用String 类型来实现锁,如果key存在,则无法设置新的值//value 为当前线程ID,为后面释放锁做准备String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);return BooleanUtil.isTrue(isLock);}/*** 使用Lua脚本保证原子性*/@Overridepublic void unlock() {String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),threadId);}//    @Override
//    public void unlock() {
//        //获取线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
//        //获取redis中的值
//        String key = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        if(threadId.equals(key)){//一致的话,删除锁
//            stringRedisTemplate.delete(KEY_PREFIX+name);
//        }
//
//    }
}

        此时这个使用Redis实现的分布式锁就可以满足大部分业务需求了,不过还是存在一些问题:

1.不可重入

2.不可重试

3.超时释放存在的安全隐患

4.集群时,主从节点之间存在的不一致性

        这些问题我们可以直接采用成熟的,已实现的框架来帮助我们解决问题,如Redisson,后续也会出一篇关于Redisson的文章。

相关文章:

使用Redis实现分布式锁,基于原本单体系统进行业务改造

一、单体系统下&#xff0c;使用锁机制实现秒杀功能&#xff0c;并限制一人一单功能 1.流程图&#xff1a; 2.代码实现&#xff1a; Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderSe…...

【MediaTek】 T750 openwrt-23.05编 cannot find dependency libexpat for libmesode

MediaTek T750 T750 采用先进的 7nm 制程,高度集成 5G 调制解调器和四核 Arm CPU,提供较强的功能和配置,设备制造商得以打造精巧的高性能 CPE 产品,如固定无线接入(FWA)路由器和移动热点。 MediaTek T750 平台是一款综合的芯片组,集成了 5G SoC MT6890、12nm 制程…...

CHARMM-GUI EnzyDocker: 一个基于网络的用于酶中多个反应状态的蛋白质 - 配体对接的计算平台

❝ "CHARMM-GUI EnzyDocker for Protein−Ligand Docking of Multiple Reactive States along a Reaction Coordinate in Enzymes"介绍了 CHARMM-GUI EnzyDocker&#xff0c;这是一个基于网络的计算平台&#xff0c;旨在简化和加速 EnzyDock 对接模拟的设置过程&…...

c# 2025/2/17 周一

16. 《表达式&#xff0c;语句详解4》 20 未完。。 表达式&#xff0c;语句详解_4_哔哩哔哩_bilibili...

vite【详解】常用配置 vite.config.js / vite.config.ts

官网 https://cn.vitejs.dev/guide/ vite 常用配置 Vite 配置文件通常是 vite.config.js &#xff08;使用 CommonJS 语法&#xff09;或者 vite.config.ts&#xff08;使用 TypeScript 语法&#xff09;&#xff0c;默认内容为 import { defineConfig } from vite import vue…...

最新智能优化算法: 阿尔法进化(Alpha Evolution,AE)算法求解23个经典函数测试集,MATLAB代码

一、阿尔法进化算法 阿尔法进化&#xff08;Alpha Evolution&#xff0c;AE&#xff09;算法是2024年提出的一种新型进化算法&#xff0c;其核心在于通过自适应基向量和随机步长的设计来更新解&#xff0c;从而提高算法的性能。以下是AE算法的主要步骤和特点&#xff1a; 主…...

用于可靠工业通信的5G-TSN集成原型:基于帧复制与消除可靠性的研究

论文标题 中文标题&#xff1a;用于可靠工业通信的5G-TSN集成原型&#xff1a;基于帧复制与消除可靠性的研究 英文标题&#xff1a;5G-TSN Integrated Prototype for Reliable Industrial Communication Using Frame Replication and Elimination for Reliability 作者信息 …...

HaProxy源码安装(Rocky8)

haproxy具有高性能、高可用性、灵活的负载均衡策略和强大的将恐和日志功能&#xff0c;是法国开发者 威利塔罗(Willy Tarreau)在2000年使用C语言开发的一个开源软件&#xff0c;是一款具 备高并发(一万以上)、高性能的TCP和HTTP负载均衡器&#xff0c;支持基于cookie的持久性&a…...

shell脚本备份MySQL数据库和库下表

目录 注意&#xff1a; 一.脚本内容 二.执行效果 三.创建定时任务 注意&#xff1a; 以下为对MySQL5.7.42版本数据库备份shell脚本参考运行备份的机器请确认mysqldump版本>5.7&#xff0c;否则备份参数--set-gtid-purgedOFF无效&#xff0c;考虑到一般数据库节点和备份…...

23. AI-大语言模型

文章目录 前言一、LLM1. 简介2. 工作原理和结构3. 应用场景4. 最新研究进展5. 比较 二、Transformer架构1. 简介2. 基本原理和结构3. 应用场景4. 最新进展 三、开源1. 开源概念2. 开源模式3. 模型权重 四、再谈DeepSeek 前言 AI‌ 一、LLM LLM&#xff08;Large Language Mod…...

Linux /dev/null

/dev/null 是 Linux 和类 Unix 系统中一个特殊且非常有用的设备文件&#xff0c;也被称为空设备。下面为你详细介绍它的特点、用途和使用示例。 特点 写入丢弃&#xff1a;当向 /dev/null 写入数据时&#xff0c;这些数据会被立即丢弃&#xff0c;不会被保存到任何地方&#…...

Unity CommandBuffer绘制粒子系统网格显示

CommandBuffer是 Unity 提供的一种在渲染流程中插入自定义渲染命令的机制。在渲染粒子系统时&#xff0c;常规的渲染流程可能无法满足特定的渲染需求&#xff0c;而CommandBuffer允许开发者灵活地设置渲染参数、控制渲染顺序以及执行自定义的绘制操作。通过它&#xff0c;可以精…...

Java延时定时刷新Redis缓存

延时定时刷新Redis缓存 一、背景 项目需求&#xff1a;订阅接收一批实时数据&#xff0c;每分钟最高可接收120万条数据&#xff0c;并且分别更新到redis和数据库中&#xff1b;而用户请求查询消息只是低频操作。资源限制&#xff1a;由于项目预算有限&#xff0c;只有4台4C16…...

智能硬件定位技术发展趋势

在科技飞速进步的当下&#xff0c;智能硬件定位技术作为众多领域的关键支撑&#xff0c;正沿着多元且极具创新性的路径蓬勃发展&#xff0c;持续重塑我们的生活与工作方式。 一、精度提升的极致追求 当前&#xff0c;智能硬件定位精度虽已满足诸多日常应用&#xff0c;但未来…...

全单模矩阵及其在分支定价算法中的应用

全单模矩阵及其在分支定价算法中的应用 目录 全单模矩阵的定义与特性全单模矩阵的判定方法全单模矩阵在优化中的核心价值分支定价算法与矩阵单模性的关系非全单模问题的挑战与系统解决方案总结与工程实践建议 1. 全单模矩阵的定义与特性 关键定义 单模矩阵&#xff08;Unimo…...

DeepSeek 的创新融合:多行业应用实践探索

引言 在数字化转型的浪潮中&#xff0c;技术的融合与创新成为推动各行业发展的关键力量。蓝耘平台作为行业内备受瞩目的创新平台&#xff0c;以其强大的资源整合能力和灵活的架构&#xff0c;为企业提供了高效的服务支持。而 DeepSeek 凭借先进的人工智能技术&#xff0c;在自然…...

利用SkinMagic美化MFC应用界面

MFC(Microsoft Foundation Class)应用程序的界面设计风格通常比较保守,而且虽然MFC框架的控件功能强大且易于集成,但视觉效果较为朴素,缺乏现代感。尤其是MFC应用程序的设计往往以功能实现为核心,界面设计可能显得较为简洁甚至略显呆板,用户体验可能不如现代应用程序流畅…...

IMX6ULL的公板的以太网控制器(MAC)与物理层(PHY)芯片(KSZ8081RNB)连接的原理图分析(包含各引脚说明以及工作原理)

目录 什么叫以太网&#xff1f;它与因特网有何区别&#xff1f;公板实现以太网的原理介绍(MII/RMII协议介绍)公板的原理图下载地址公板中IMX6ULL处理器与MAC(以太网控制器)有关的原理图IMX6ULL处理器的MAC引脚说明1. **ENET1_TX_DATA0**2. **ENET1_TX_DATA1**3. **ENET1_TX_EN*…...

采用分布式部署deepseek

分布式部署DeepSeek涉及使用多个计算节点来加速模型训练或提升推理效率。下面是一个基本的指南&#xff0c;帮助您了解如何进行分布式部署。 1. 环境准备 硬件需求&#xff1a;确保您的集群环境中有足够的GPU资源&#xff0c;并且所有机器之间可以通过高速网络互联。软件依赖…...

Cloud: aws:network: limit 含有pps这种限制

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/troubleshooting-ena.html#statistics-ena 这个是调查网络问题的一个网页; 在里面,竟然含有pps这种限制:ethtool -S;其实是比较苛刻的安全相关的策略? [ec2-user ~]$ ethtool -S ethN NIC statistics:tx_timeout: …...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八

现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet&#xff0c;点击确认后如下提示 最终上报fail 解决方法 内核升级导致&#xff0c;需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...

【Go语言基础【13】】函数、闭包、方法

文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数&#xff08;函数作为参数、返回值&#xff09; 三、匿名函数与闭包1. 匿名函数&#xff08;Lambda函…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...

NPOI Excel用OLE对象的形式插入文件附件以及插入图片

static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...

打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用

一、方案背景​ 在现代生产与生活场景中&#xff0c;如工厂高危作业区、医院手术室、公共场景等&#xff0c;人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式&#xff0c;存在效率低、覆盖面不足、判断主观性强等问题&#xff0c;难以满足对人员打手机行为精…...

【LeetCode】3309. 连接二进制表示可形成的最大数值(递归|回溯|位运算)

LeetCode 3309. 连接二进制表示可形成的最大数值&#xff08;中等&#xff09; 题目描述解题思路Java代码 题目描述 题目链接&#xff1a;LeetCode 3309. 连接二进制表示可形成的最大数值&#xff08;中等&#xff09; 给你一个长度为 3 的整数数组 nums。 现以某种顺序 连接…...

WebRTC从入门到实践 - 零基础教程

WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC&#xff1f; WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个支持网页浏览器进行实时语音…...

五、jmeter脚本参数化

目录 1、脚本参数化 1.1 用户定义的变量 1.1.1 添加及引用方式 1.1.2 测试得出用户定义变量的特点 1.2 用户参数 1.2.1 概念 1.2.2 位置不同效果不同 1.2.3、用户参数的勾选框 - 每次迭代更新一次 总结用户定义的变量、用户参数 1.3 csv数据文件参数化 1、脚本参数化 …...