当前位置: 首页 > 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: …...

Sunshine游戏串流服务器:重新定义跨设备游戏体验的技术架构

Sunshine游戏串流服务器&#xff1a;重新定义跨设备游戏体验的技术架构 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 你是否曾因高性能游戏PC被束缚在书房而烦恼&#xff1f;是否…...

GHelper终极指南:3个步骤释放华硕笔记本隐藏性能

GHelper终极指南&#xff1a;3个步骤释放华硕笔记本隐藏性能 【免费下载链接】g-helper G-Helper is a fast, native tool for tuning performance, fans, GPU, battery, and RGB on any Asus laptop or handheld - ROG Zephyrus, Flow, Strix, TUF, Vivobook, Zenbook, ProArt…...

番茄小说下载器终极指南:5分钟打造个人离线图书馆

番茄小说下载器终极指南&#xff1a;5分钟打造个人离线图书馆 【免费下载链接】Tomato-Novel-Downloader 番茄小说下载器不精简版 项目地址: https://gitcode.com/gh_mirrors/to/Tomato-Novel-Downloader 番茄小说下载器是一款功能强大的开源工具&#xff0c;专为数字阅…...

THINKROUTER:大模型推理的置信度路由优化技术

1. THINKROUTER&#xff1a;大模型推理的置信度路由革命 当大型语言模型&#xff08;LLM&#xff09;在解决复杂数学题时突然"固执己见"地给出错误答案&#xff0c;或者在代码生成时陷入无意义的循环&#xff0c;这些现象背后往往隐藏着一个关键问题&#xff1a;模型…...

Chapter 5:深度章 - AI 编程思维转变

Chapter 5:深度章 - AI 编程思维转变 学习目标 理解从传统开发到 AI Engineering 的角色重塑 掌握 SDD(规格驱动开发)的核心理念 理解从"写代码"到"设计系统"的思维转变 能够用新思维指导日常工作 一、工程师角色的重塑 1.1 传统开发中的工程师角色 …...

独立开发记录:我怎么把一个专注计时器做成了「声音护照」— iOS端技术拆解

为什么要做这个App 去年我给自己定了个规矩&#xff0c;每天至少专注写稿25分钟。试了一圈市面上的番茄钟&#xff0c;发现都是同一个套路&#xff1a;倒计时、叮一声、结束。 用了两周就不想打开了。没有留存感&#xff0c;没有成就感&#xff0c;就是个闹钟。 我想要的是——…...

手把手教你用Arduino+DS1302做个带断电记忆的电子时钟(附完整代码与接线图)

从零打造断电记忆电子钟&#xff1a;Arduino与DS1302的完美组合 1602液晶屏上跳动的数字突然熄灭又亮起&#xff0c;但时间却分秒不差——这就是带断电记忆功能的电子钟魅力所在。对于刚接触Arduino的创客来说&#xff0c;用DS1302时钟模块构建一个永不掉时的电子钟&#xff0c…...

别再只用来识别人了!解锁YOLOv8-pose的隐藏玩法:精准圆检测与圆心预测实战

解锁YOLOv8-pose的几何魔法&#xff1a;从人体姿态到工业圆检测的跨界实战 在计算机视觉领域&#xff0c;模型的能力边界往往比我们想象的更为宽广。当大多数开发者还在用YOLOv8-pose模型追踪人体关节时&#xff0c;一些前沿实践者已经发现了它隐藏的几何分析天赋——这个原本为…...

Adobe-GenP:智能破解工具如何3分钟内解锁Adobe全家桶?

Adobe-GenP&#xff1a;智能破解工具如何3分钟内解锁Adobe全家桶&#xff1f; 【免费下载链接】Adobe-GenP Adobe CC 2019/2020/2021/2022/2023 GenP Universal Patch 3.0 项目地址: https://gitcode.com/gh_mirrors/ad/Adobe-GenP 在创意设计领域&#xff0c;Adobe Cre…...

手把手教你用Pinecone和Hugging Face数据集,5步搞定一个多模态混合搜索Demo

5步实战&#xff1a;基于Pinecone与Hugging Face打造时尚商品多模态搜索系统 当你在电商平台搜索"复古风蓝色牛仔裤"时&#xff0c;是否遇到过返回结果与预期不符的情况&#xff1f;传统关键词搜索的局限性正在被新一代多模态混合搜索技术打破。本文将带你用Pinecone…...