lua脚本+Redission实现分布式锁

实现分布式锁最简单的一种方式:基于Redis

不论是本地锁还是分布式锁,核心都在于“互斥”。

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNXset if not exists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0

释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

DEL lockKey
(integer) 1

为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。

选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。

为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间 。

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey:加锁的锁名;
  • uniqueValue:能够唯一标识锁的随机字符串;
  • NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

你或许在想:如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!

好 它来了

Redission+lua脚本实现互斥锁

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。

Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成时,Redisson会引入一个看门狗机制,每隔一段时间检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。

实践一下:优惠券秒杀一人一单防止超卖实现步骤

1 引入依赖
<dependencies><!-- Redisson --><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.1</version></dependency><!-- Spring Boot Data Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Spring Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
2. Redisson 配置
@Configuration
public class RedissonConfig {@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private String port;@Value("${spring.redis.password}")private String password;@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password == null || password.isEmpty() ? null : password).setDatabase(0);return Redisson.create(config);}
}
3. 常量类
public interface RedisConstants {String SECKILL_STOCK_KEY = "seckill:stock:";String SECKILL_ORDER_KEY = "seckill:order:";String LOCK_COUPON_KEY = "lock:coupon:";long LOCK_TIMEOUT = 30; // 锁超时时间(秒)
}
4. service
@Service
public class CouponService {@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate CouponMapper couponMapper;@Autowiredprivate OrderMapper orderMapper;// 秒杀优惠券public Result seckillCoupon(Long couponId, Long userId) {// 1. 生成锁keyString lockKey = RedisConstants.LOCK_COUPON_KEY + couponId;// 2. 获取Redisson锁RLock lock = redissonClient.getLock(lockKey);// 3. 尝试获取锁,等待10秒,自动释放时间30秒 这里没有启用看门狗 因为设置了自动30s超时释放 boolean isLocked = false;try {isLocked = lock.tryLock(10, RedisConstants.LOCK_TIMEOUT, TimeUnit.SECONDS);if (!isLocked) {return Result.fail("抢购失败,请稍后再试");}// 4. 执行Lua脚本校验库存和用户订单String script = buildSeckillScript();List<String> keys = Arrays.asList(couponId.toString());List<String> args = Arrays.asList(userId.toString());Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),keys, args);// 5. 处理脚本返回结果if (result == null) {return Result.fail("系统异常");}if (result == 0) {return Result.fail("库存不足");}if (result == -1) {return Result.fail("每个用户限购一次");}// 6. 创建订单(这里简化处理,实际项目可能需要更复杂的订单创建逻辑)createOrder(couponId, userId);return Result.ok("抢购成功");} catch (InterruptedException e) {Thread.currentThread().interrupt();return Result.fail("系统异常");} finally {// 7. 释放锁if (isLocked && lock.isHeldByCurrentThread()) {lock.unlock();}}}// 构建秒杀Lua脚本private String buildSeckillScript() {return "local stockKey = 'seckill:stock:' .. KEYS[1] " +"local orderKey = 'seckill:order:' .. KEYS[1] " +"local userId = ARGV[1] " +"local stock = tonumber(redis.call('get', stockKey) or 0) " +"if stock <= 0 then return 0 end " +"if redis.call('sismember', orderKey, userId) == 1 then return -1 end " +"redis.call('decr', stockKey) " +"redis.call('sadd', orderKey, userId) " +"return 1";}// 创建订单private void createOrder(Long couponId, Long userId) {// 查询优惠券信息Coupon coupon = couponMapper.selectById(couponId);// 创建订单Order order = new Order();order.setUserId(userId);order.setCouponId(couponId);order.setPayAmount(coupon.getPrice());// 设置其他订单字段...// 保存订单orderMapper.insert(order);}
}
5. controller
@RestController
@RequestMapping("/api/coupon")
public class CouponController {@Autowiredprivate CouponService couponService;@PostMapping("/seckill/{couponId}")public Result seckillCoupon(@PathVariable Long couponId, @RequestHeader("userId") Long userId) {return couponService.seckillCoupon(couponId, userId);}
}
6. 初始化库存和优惠券
@Service
public class InitService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate CouponMapper couponMapper;// 初始化优惠券库存到Redis@PostConstructpublic void initCouponStock() {// 查询所有可用优惠券List<Coupon> coupons = couponMapper.selectList(new QueryWrapper<Coupon>().eq("status", 1).gt("stock", 0).lt("start_time", LocalDateTime.now()).gt("end_time", LocalDateTime.now()));// 将优惠券库存加载到Redisfor (Coupon coupon : coupons) {stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + coupon.getId(), coupon.getStock().toString());}}
}

lua脚本详解

-- 获取库存键和订单键
local stockKey = 'seckill:stock:' .. KEYS[1] 
local orderKey = 'seckill:order:' .. KEYS[1] -- 获取用户ID参数
local userId = ARGV[1] -- 获取当前库存(如果不存在则为0)
local stock = tonumber(redis.call('get', stockKey) or 0) -- 检查库存是否不足
if stock <= 0 then return 0 end -- 检查用户是否已购买过
if redis.call('sismember', orderKey, userId) == 1 then return -1 end -- 扣减库存
redis.call('decr', stockKey) -- 记录用户已购买
redis.call('sadd', orderKey, userId) -- 返回成功标识
return 1

看门狗机制在哪体现捏? 

当你调用tryLock()方法没有显式指定锁的持有时间(即只传等待时间,不传释放时间)时,看门狗机制会自动生效。例如:

// 启用看门狗:不指定leaseTime,使用默认续期时间(默认30秒)
lock.tryLock(10, null, TimeUnit.SECONDS);// 禁用看门狗:显式指定leaseTime,锁到期后不会续期
lock.tryLock(10, 30, TimeUnit.SECONDS); // 你提供的代码使用这种方式

如果启用dog 建议增加配置来调整看门狗的默认续期时间: 

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setLockWatchdogTimeout(60 * 1000); // 设置看门狗续期时间为60秒

Redisson实现的分布式锁是可重入的吗?它是怎么实现的?

是的,Redisson 实现的分布式锁是可重入的。可重入锁允许同一个线程多次获取同一把锁而不会被阻塞,这可以有效避免死锁问题,同时让代码逻辑更清晰。

Redisson 如何实现可重入锁

Redisson 是基于 Redis 的哈希结构来存储锁信息的。打个比方,我们有个叫 “myLock” 的锁,这就是锁的唯一标识,相当于哈希结构里的 Key。而每个尝试获取锁的线程都有自己唯一的标识,像线程 ID 或者 UUID,这就是哈希结构里的 Field。线程获取锁的次数,也就是重入次数,就是哈希结构里的 Value。

加锁过程

当一个线程想去获取锁的时候,Redisson 首先会检查这个锁对应的 Key 存不存在。要是不存在,那就说明当前没有线程持有这把锁,Redisson 就会创建这个锁,把 Field 设成当前线程的标识,Value 设为 1,同时给锁设置一个过期时间。要是锁已经存在,Redisson 就会去检查 Field 是不是和当前线程的标识一样。如果一样,那就意味着当前线程已经持有这把锁了,Redisson 就把 Value 加 1,并且刷新锁的过期时间。要是不一样,那就表示锁被其他线程占着,当前线程就得等着锁被释放。

释放锁过程

释放锁的时候,Redisson 会先看看锁的 Field 和当前线程标识是不是一致。如果一致,就把 Value 减 1。要是减完之后 Value 变成 0 了,那就说明当前线程已经完全释放了这把锁,Redisson 就把锁对应的 Key 删除。要是 Value 还大于 0,说明当前线程还有重入的情况,还持有锁,Redisson 就刷新一下锁的过期时间。

防止死锁

Redisson 在防止死锁方面也有很实用的机制。一方面,加锁的时候会给锁设置过期时间,就算某个线程出问题了,一直不释放锁,到时间了锁也会自动被删除。另一方面,它的可重入机制也能避免因为线程嵌套调用导致的死锁。

可重入锁

Redisson 的可重入锁优势也很明显。从线程安全角度看,只有持有锁的线程才能释放锁,这就保证了不会出现线程安全问题。在性能上,它通过 Lua 脚本确保加锁和释放锁的操作是原子性的,避免了竞态条件,效率很高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/80543.shtml

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

相关文章

设计模式之工厂模式(二):实际案例

设计模式之工厂模式(一) 在阅读Qt网络部分源码时候&#xff0c;发现在某处运用了工厂模式&#xff0c;而且编程技巧也用的好&#xff0c;于是就想分享出来&#xff0c;供大家参考&#xff0c;理解的不对的地方请多多指点。 以下是我整理出来的类图&#xff1a; 关键说明&#x…

MultiTTS 1.7.6 | 最强离线语音引擎,提供多音色无障碍朗读功能,附带语音包

MultiTTS是一款免费且支持离线使用的文本转语音&#xff08;TTS&#xff09;工具&#xff0c;旨在为用户提供丰富的语音包选项&#xff0c;实现多音色无障碍朗读功能。这款应用程序特别适合用于阅读软件中的离线听书体验&#xff0c;提供了多样化的语音选择&#xff0c;使得听书…

歌曲《忘尘谷》基于C语言的歌曲调性检测技术解析

引言 在音乐分析与数字信号处理领域&#xff0c;自动检测歌曲调性是一项基础且关键的任务。本文以C语言为核心&#xff0c;结合音频处理库&#xff08;libsndfile&#xff09;和快速傅里叶变换库&#xff08;FFTW&#xff09;&#xff0c;探讨如何实现调性检测&#xff0c;并通…

大某麦演唱会门票如何自动抢

引言 仅供学习研究&#xff0c;欢迎交流 抢票难&#xff0c;难于上青天&#xff01;无论是演唱会、话剧还是体育赛事&#xff0c;大麦网的票总是秒光。大麦网是国内知名的票务平台&#xff0c;热门演出票往往一票难求。手动抢票不仅耗时&#xff0c;还容易错过机会。作为一名…

1.3.3 tinyalsa详细介绍

一、TinyALSA 的背景与设计目标 1. 诞生背景 Android 音频需求的演变&#xff1a;早期 Android 系统使用标准 ALSA&#xff08;Advanced Linux Sound Architecture&#xff09;的用户空间库 alsa-lib&#xff0c;但因其复杂性&#xff08;代码庞大、依赖较多&#xff09;和资…

超越合并速度(merge speed):AI如何重塑开发者协作

李升伟 编译 AI 关于现代开发的讨论通常围绕着单一指标&#xff1a;合并速度&#xff08;merge speed&#xff09;。但在这一表面测量之下&#xff0c;隐藏着开发团队工作方式的一种更深刻的变革。让我们探讨开发者协作的微妙演变方式以及为什么传统生产力指标只讲述了一部分故…

如何找正常运行虚拟机

1.新建虚拟机。Linux centos7&#xff0c;给虚拟机改个名字不要放在c盘 2.安装操作系统。cd/dvd->2009.iso 启动虚拟机

深度学习:系统性学习策略(二)

深度学习的系统性学习策略 基于《认知觉醒》与《认知驱动》的核心方法论,结合深度学习的研究实践,从认知与技能双重维度总结以下系统性学习策略: 一、认知觉醒:构建深度学习的思维操作系统 三重脑区协同法则 遵循**本能脑(舒适区)-情绪脑(拉伸区)-理智脑(困难区)**的…

如何使用CSS解决一行有三个元素,前两个元素靠左排列,第三个元素靠右排列的问题

如图所示&#xff0c;我要把左边的场馆和区域信息靠左排列&#xff0c;价格信息靠右排列。如何使用CSS实现这种效果&#xff1f; 在这里&#xff0c;我使用了flexbox弹性布局&#xff0c;以下是我的实现代码 .name-info {display: flex;gap: 2px;justify-content: space-betwee…

USB传输模式

USB有四种传输模式: 控制传输, 中断传输, 同步传输, 批量传输 1. 中断传输 中断传输一般用于小批量, 非连续的传输. 对实时性要求较高. 常见的使用此传输模式的设备有: 鼠标, 键盘等. 要注意的是, 这里的 “中断” 和我们常见的中断概念有差异. Linux中的中断是设备主动发起的…

【Python 变量类型】

Python 是一种动态类型语言&#xff0c;变量类型在运行时自动确定&#xff0c;无需显式声明。以下是 Python 中核心变量类型的分类与用法详解&#xff1a; 一、基本数据类型 1. 数值类型 整数 (int) 支持正负数、零和二进制/八进制/十六进制表示&#xff1a; a 42 b 0o52 #…

Python基础:类的深拷贝与浅拷贝-->with语句的使用及三个库:matplotlib基本画图-->pandas之Series创建

一.类的深拷贝与浅拷贝 class CPU():pass class Disk():passclass Computer():#计算机由CPU和硬盘组成def __init__(self):self.cpu CPU()self.disk Disk()cpu CPU()#创建一个CPU对象 disk Disk()#创建一个硬盘对象#创建一个计算机对象 com Computer(cpu,disk) #变量&…

【SSM-SpringMVC(二)】Spring接入Web环境!本篇开始研究SpringMVC的使用!SpringMVC数据响应和获取请求数据

SpringMVC的数据响应方式 页面跳转 直接返回字符串通过ModelAndView对象返回 回写数据 直接返回字符串返回对象或集合 页面跳转&#xff1a; 返回字符串方式 直接返回字符串&#xff1a;此种方式会将返回的字符串与视图解析器的前后缀拼接后跳转 RequestMapping("/con&…

阅文集团C++面试题及参考答案

目录 能否不使用锁保证多线程安全? 面向对象的三个特性是什么?请分别解释。 构造函数和析构函数能否被继承? C++ 中函数重载是如何实现的? C 语言中是否支持函数重载? 什么是左值和右值?请举例说明。 C++ 中子类的构造和析构顺序是怎样的? C++ 中虚函数表的变化过…

【亲测有效】如何清空但不删除GitHub仓库中的所有文件(main分支)

如何清空但不删除GitHub仓库中的所有文件&#xff08;main分支&#xff09; 在项目开发过程中&#xff0c;有时我们需要清空GitHub仓库中的所有文件&#xff0c;同时保留仓库本身。这种情况常见于项目重构、代码重写或者需要重新开始一个项目时。本文将介绍一种有效的方法来清…

前端EXCEL插件,智表ZCELL产品V3.0 版本发布,底层采用canvas全部重构,功能大幅扩展,性能极致提升,满足千万级单元格加载

本次更新是底层全部重构&#xff0c;按照现代浏览器要求&#xff0c;采用canvas方式进行了重构&#xff0c;预留了将来扩展空间&#xff0c;特别是在大数据量性能提升方面有了较大提升&#xff0c;可以满足千万级单元格加载&#xff0c;欢迎大家体验使用。 体验地址&#xff1…

3DGS-to-PC:3DGS模型一键丝滑转 点云 or Mesh 【Ubuntu 20.04】【2025最新版!!】

一、引言 3D高斯泼溅(3DGS)是一种新兴的三维场景表示方法&#xff0c;可以生成高质量的场景重建结果。然而&#xff0c;要查看这些重建场景&#xff0c;需要特殊的高斯渲染器。大多数3D处理软件并不兼容3D高斯分布模型&#xff0c;但它们通常都兼容点云文件。 3DGS-to-PC项目提…

OpenHarmony 以太网卡热插拔事件接口无效

目录 1.背景 2.解决方案 1.背景 在OpenHarmony中调用以太网热插拔时间,发现热插拔没有任何回调,如下接口 import { ethernet } from @kit.NetworkKit;ethernet.on(interfaceStateChange, (data: object) => {console.log(on interfaceSharingStateChange: + JSON.…

C++ 跨平台开发挑战与深度解决方案:从架构设计到实战优化

C 凭借其高性能与底层控制能力&#xff0c;在游戏引擎、嵌入式系统、工业软件等领域占据核心地位。然而&#xff0c;跨平台开发过程中需应对硬件架构多样性、操作系统差异性、编译工具链碎片化等复杂问题。本文将从底层架构到上层应用&#xff0c;系统性剖析 C 跨平台开发的核心…

什么是 ANR 如何避免它

一、什么是 ANR&#xff1f; ANR&#xff08;Application Not Responding&#xff09; 是 Android 系统在应用程序主线程&#xff08;UI 线程&#xff09;被阻塞超过一定时间后触发的错误机制。此时系统会弹出一个对话框提示用户“应用无响应”&#xff0c;用户可以选择等待或强…