使用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实现分布式锁,基于原本单体系统进行业务改造
一、单体系统下,使用锁机制实现秒杀功能,并限制一人一单功能 1.流程图: 2.代码实现: 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,这是一个基于网络的计算平台,旨在简化和加速 EnzyDock 对接模拟的设置过程&…...
c# 2025/2/17 周一
16. 《表达式,语句详解4》 20 未完。。 表达式,语句详解_4_哔哩哔哩_bilibili...
vite【详解】常用配置 vite.config.js / vite.config.ts
官网 https://cn.vitejs.dev/guide/ vite 常用配置 Vite 配置文件通常是 vite.config.js (使用 CommonJS 语法)或者 vite.config.ts(使用 TypeScript 语法),默认内容为 import { defineConfig } from vite import vue…...
最新智能优化算法: 阿尔法进化(Alpha Evolution,AE)算法求解23个经典函数测试集,MATLAB代码
一、阿尔法进化算法 阿尔法进化(Alpha Evolution,AE)算法是2024年提出的一种新型进化算法,其核心在于通过自适应基向量和随机步长的设计来更新解,从而提高算法的性能。以下是AE算法的主要步骤和特点: 主…...
用于可靠工业通信的5G-TSN集成原型:基于帧复制与消除可靠性的研究
论文标题 中文标题:用于可靠工业通信的5G-TSN集成原型:基于帧复制与消除可靠性的研究 英文标题:5G-TSN Integrated Prototype for Reliable Industrial Communication Using Frame Replication and Elimination for Reliability 作者信息 …...
HaProxy源码安装(Rocky8)
haproxy具有高性能、高可用性、灵活的负载均衡策略和强大的将恐和日志功能,是法国开发者 威利塔罗(Willy Tarreau)在2000年使用C语言开发的一个开源软件,是一款具 备高并发(一万以上)、高性能的TCP和HTTP负载均衡器,支持基于cookie的持久性&a…...
shell脚本备份MySQL数据库和库下表
目录 注意: 一.脚本内容 二.执行效果 三.创建定时任务 注意: 以下为对MySQL5.7.42版本数据库备份shell脚本参考运行备份的机器请确认mysqldump版本>5.7,否则备份参数--set-gtid-purgedOFF无效,考虑到一般数据库节点和备份…...
23. AI-大语言模型
文章目录 前言一、LLM1. 简介2. 工作原理和结构3. 应用场景4. 最新研究进展5. 比较 二、Transformer架构1. 简介2. 基本原理和结构3. 应用场景4. 最新进展 三、开源1. 开源概念2. 开源模式3. 模型权重 四、再谈DeepSeek 前言 AI 一、LLM LLM(Large Language Mod…...
Linux /dev/null
/dev/null 是 Linux 和类 Unix 系统中一个特殊且非常有用的设备文件,也被称为空设备。下面为你详细介绍它的特点、用途和使用示例。 特点 写入丢弃:当向 /dev/null 写入数据时,这些数据会被立即丢弃,不会被保存到任何地方&#…...
Unity CommandBuffer绘制粒子系统网格显示
CommandBuffer是 Unity 提供的一种在渲染流程中插入自定义渲染命令的机制。在渲染粒子系统时,常规的渲染流程可能无法满足特定的渲染需求,而CommandBuffer允许开发者灵活地设置渲染参数、控制渲染顺序以及执行自定义的绘制操作。通过它,可以精…...
Java延时定时刷新Redis缓存
延时定时刷新Redis缓存 一、背景 项目需求:订阅接收一批实时数据,每分钟最高可接收120万条数据,并且分别更新到redis和数据库中;而用户请求查询消息只是低频操作。资源限制:由于项目预算有限,只有4台4C16…...
智能硬件定位技术发展趋势
在科技飞速进步的当下,智能硬件定位技术作为众多领域的关键支撑,正沿着多元且极具创新性的路径蓬勃发展,持续重塑我们的生活与工作方式。 一、精度提升的极致追求 当前,智能硬件定位精度虽已满足诸多日常应用,但未来…...
全单模矩阵及其在分支定价算法中的应用
全单模矩阵及其在分支定价算法中的应用 目录 全单模矩阵的定义与特性全单模矩阵的判定方法全单模矩阵在优化中的核心价值分支定价算法与矩阵单模性的关系非全单模问题的挑战与系统解决方案总结与工程实践建议 1. 全单模矩阵的定义与特性 关键定义 单模矩阵(Unimo…...
DeepSeek 的创新融合:多行业应用实践探索
引言 在数字化转型的浪潮中,技术的融合与创新成为推动各行业发展的关键力量。蓝耘平台作为行业内备受瞩目的创新平台,以其强大的资源整合能力和灵活的架构,为企业提供了高效的服务支持。而 DeepSeek 凭借先进的人工智能技术,在自然…...
利用SkinMagic美化MFC应用界面
MFC(Microsoft Foundation Class)应用程序的界面设计风格通常比较保守,而且虽然MFC框架的控件功能强大且易于集成,但视觉效果较为朴素,缺乏现代感。尤其是MFC应用程序的设计往往以功能实现为核心,界面设计可能显得较为简洁甚至略显呆板,用户体验可能不如现代应用程序流畅…...
IMX6ULL的公板的以太网控制器(MAC)与物理层(PHY)芯片(KSZ8081RNB)连接的原理图分析(包含各引脚说明以及工作原理)
目录 什么叫以太网?它与因特网有何区别?公板实现以太网的原理介绍(MII/RMII协议介绍)公板的原理图下载地址公板中IMX6ULL处理器与MAC(以太网控制器)有关的原理图IMX6ULL处理器的MAC引脚说明1. **ENET1_TX_DATA0**2. **ENET1_TX_DATA1**3. **ENET1_TX_EN*…...
采用分布式部署deepseek
分布式部署DeepSeek涉及使用多个计算节点来加速模型训练或提升推理效率。下面是一个基本的指南,帮助您了解如何进行分布式部署。 1. 环境准备 硬件需求:确保您的集群环境中有足够的GPU资源,并且所有机器之间可以通过高速网络互联。软件依赖…...
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: …...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
