使用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…...
数据结构中的邻接表
一、概念 邻接表(Adjacency List)是一种用于表示图(Graph)数据结构的常用方法。它特别适用于稀疏图,即边的数量远小于顶点数量平方的图。邻接表通过为每个顶点维护一个列表来存储与该顶点相邻的顶点,从而高…...
js第九题
题九:放大镜效果 要求: 1.鼠标移至图片上方,鼠标周围出现黄色的的正方形框,黄色矩形 框会随着鼠标的移动而移动; 2.将黄色正方形框里的内容的长和宽均放大2.4倍,并在图片右边进 行显示。 html <div …...
基于单片机ht7038 demo
单片机与ht7038 demo,三相电能表,电量数据包括电流电压功能,采用免校准方法 列表 ht7038模块/CORE/core_cm3.c , 17273 ht7038模块/CORE/core_cm3.h , 85714 ht7038模块/CORE/startup_stm32f10x_hd.s , 15503 ht7038模块/CORE/startup_stm32…...
轮播图html
题十二:轮播图 要求: 1.鼠标不在图片上方时,进行自动轮播,并且左右箭头不会显示;当鼠标放在图片上方时,停止轮播,并且左右箭头会显示; 2.图片切换之后,图片中下方的小圆…...
Nginx内存池源代码剖析----ngx_create_pool函数
ngx_create_pool 是 Nginx 内存池 的初始化函数,负责创建并初始化一个内存池对象。它的作用是 为后续的内存分配操作提供统一的管理入口,通过预分配一块较大的内存区域,并基于此区域实现高效的内存分配、对齐管理和资源回收。 源代码定义&…...
DeepSeek 开放平台无法充值 改用其他平台API调用DeepSeek-chat模型方法
近几天DeepSeek开放平台无法充值目前已经关闭状态,大家都是忙着接入DeepSeek模型 ,很多人想使用DeepSeek怎么办? 当然还有改用其他平台API调用方法,本文以本站的提供chatgpt系统为例,如何修改DeepSeek-chat模型API接口…...
QT基础一、学会建一个项目
注:因为CSDN有很多付费才能吃到的史,本人对此深恶痛绝,所以我打算出一期免费的QT基础入门专栏,这是QT基础知识的第一期,学会建一个项目,本专栏是适用于c / c基础不错的朋友的一个免费专栏,接下来…...
科技引领未来,中建海龙C-MiC 2.0技术树立模块化建筑新标杆
在建筑行业追求高效与品质的征程中,中建海龙科技有限公司(简称“中建海龙”)以其卓越的创新能力和强大的技术实力,不断书写着装配式建筑领域的新篇章。1 月 10 日,由深圳安居集团规划,中建海龙与中海建筑共…...
解锁养生秘籍,拥抱健康生活
在这个快节奏的时代,人们行色匆匆,常常在忙碌中忽略了健康。其实,养生并非遥不可及,它就藏在生活的细微之处,等待我们去发现和实践。 规律作息是健康的基础。日出而作,日落而息,顺应自然规律&am…...
STM32 如何使用DMA和获取ADC
目录 背景 摇杆的原理 程序 端口配置 ADC 配置 DMA配置 背景 DMA是一种计算机技术,允许某些硬件子系统直接访问系统内存,而不需要中央处理器(CPU)的介入,从而减轻CPU的负担。我们可以通过DMA来从外设…...
细胞计数专题 | LUNA-FX7™新自动对焦算法提高极低细胞浓度下的细胞计数准确性
现代细胞计数仪采用自动化方法,在特定浓度范围内进行细胞计数。其上限受限于在高浓度条件下准确区分细胞边界的能力,而相机视野等因素则决定了下限。在图像中仅包含少量可识别细胞或特征的情况下,自动对焦可能会失效,从而影响细胞…...
DeepSeek教unity------MessagePack-01
中文:GitCode - 全球开发者的开源社区,开源代码托管平台 MessagePack是C# 的极速 MessagePack 序列化器。它比 MsgPack-Cli 快 10 倍,并且性能超过其他 C# 序列化器。MessagePack for C# 还内置支持 LZ4 压缩——一种极其快速的压缩算法。性能在诸如游戏…...
vite+vue3开发uni-app时低版本浏览器不支持es6语法的问题排坑笔记
重要提示:请首先完整阅读完文章内容后再操作,以免不必要的时间浪费!切记!!!在使用vitevue3开发uni-app项目时,存在低版本浏览器不兼容es6语法的问题,如“?.” “??” 等。为了方便…...
WPF-数据转换器
一、单值转换器 1.不传参数 转换器 当Value值大于100时返回红色 public class DataConverter : IValueConverter{/// <summary>/// 表示从源到目标数据转换/// </summary>/// <param name"value">数据源的值</param>/// <param name&q…...
蓝桥杯备考:贪心算法之纪念品分组
P1094 [NOIP 2007 普及组] 纪念品分组 - 洛谷 这道题我们的贪心策略就是每次找出最大的和最小的,如果他们加起来不超过我们给的值,就分成一组,如果超过了,就把大的单独成一组,小的待定 #include <iostream> #i…...
Win11配置wsl、ubuntu、docker
系统要求 安装WSL。 开通虚拟化: 准备工作 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestartdism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestartwsl --set-default-versi…...
以mysql驱动为案例,从源码角度深入分析Java的SPI机制
本文将以mysql驱动为案例,深入跟踪源码分析Java的SPI(Service Provider Interface)机制。 环境 java 8,mysql8.0,mysql-connector-java 8.0.20 代码 public class MysqlConnectorTest {public static void main(St…...
市盈率(P/E Ratio):理解股票价格与盈利的关系(中英双语)
市盈率(P/E Ratio):理解股票价格与盈利的关系 今天在阅读《漫步华尔街》(原书第13版)的过程中,看到了“股票价格是每股盈利的 6 倍”的类似表述,于是产生了本文。 在投资股票时,投资…...
尚硅谷爬虫note008
一、handler处理器 定制更高级的请求头 # _*_ coding : utf-8 _*_ # Time : 2025/2/17 08:55 # Author : 20250206-里奥 # File : demo01_urllib_handler处理器的基本使用 # Project : PythonPro17-21# 导入 import urllib.request from cgitb import handler# 需求ÿ…...
AWS上基于高德地图API验证Amazon Redshift里国内地址数据正确性的设计方案
该方案通过无服务架构实现高可扩展性,结合分页查询和批量更新确保高效处理海量数据,同时通过密钥托管和错误重试机制保障安全性及可靠性。 一、技术栈 组件技术选型说明计算层AWS Lambda无服务器执行,适合事件驱动、按需处理,成…...
matlab汽车动力学半车垂向振动模型
1、内容简介 matlab141-半车垂向振动模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
【新品解读】AI 应用场景全覆盖!解码超高端 VU+ FPGA 开发平台 AXVU13F
「AXVU13F」Virtex UltraScale XCVU13P Jetson Orin NX 继发布 AMD Virtex UltraScale FPGA PCIE3.0 开发平台 AXVU13P 后,ALINX 进一步研究尖端应用市场,面向 AI 场景进行优化设计,推出 AXVU13F。 AXVU13F 和 AXVU13P 采用相同的 AMD Vir…...
智能硬件定位技术发展趋势
在科技飞速进步的当下,智能硬件定位技术作为众多领域的关键支撑,正沿着多元且极具创新性的路径蓬勃发展,持续重塑我们的生活与工作方式。 一、精度提升的极致追求 当前,智能硬件定位精度虽已满足诸多日常应用,但未来…...
【Elasticsearch】`nested`和`flattened`字段在索引时有显著的区别
有同学问,nested查询效率不高为啥不直接扁平化查询呢?就跟之前的普通结构查询一样,这就有些想当然了,因为扁平化的结构在存储时,其实跟我们想的不一样,接下来给出扁平化在索引时的存储结构(尤其是当嵌套对象…...
【Linux探索学习】第二十七弹——信号(上):Linux 信号基础详解
Linux学习笔记: https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言: 前面我们已经将进程通信部分讲完了,现在我们来讲一个进程部分也非常重要的知识点——信号,信号也是进程间通信的一…...
redis解决高并发看门狗策略
当一个业务执行时间超过自己设定的锁释放时间,那么会导致有其他线程进入,从而抢到同一个票,所有需要使用看门狗策略,其实就是开一个守护线程,让守护线程去监控key,如果到时间了还未结束,就会将这个key重新s…...
Ollama+DeepSeek+Open-WebUi
环境准备 Docker Ollama Open-WebUi Ollama 下载地址:Ollama docker安装ollama docker run -d \ -v /data/ollama/data:/root/.ollama \ -p 11434:11434 \ --name ollama ollama/ollama 下载模型 Ollama模型仓库 # 示例:安装deepseek-r1:7b doc…...
MySQL-事务隔离级别
事务有四大特性(ACID):原子性,一致性,隔离性和持久性。隔离性一般在事务并发的时候需要保证事务的隔离性,事务并发会出现很多问题,包括脏写,脏读,不可重复读,…...
对于简单的HTML、CSS、JavaScript前端,我们可以通过几种方式连接后端
1. 使用Fetch API发送HTTP请求(最简单的方式): //home.html // 示例:提交表单数据到后端 const submitForm async (formData) > {try {const response await fetch(http://your-backend-url/api/submit, {method: POST,head…...
