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

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 脚本确保加锁和释放锁的操作是原子性的,避免了竞态条件,效率很高。

相关文章:

lua脚本+Redission实现分布式锁

实现分布式锁最简单的一种方式&#xff1a;基于Redis 不论是本地锁还是分布式锁&#xff0c;核心都在于“互斥”。 在 Redis 中&#xff0c; SETNX 命令是可以帮助我们实现互斥。SETNX 即 set if not exists (对应 Java 中的 setIfAbsent 方法)&#xff0c;如果 key 不存在的…...

JVM之jcmd命令详解

jcmd 是 Oracle JDK&#xff08;Java Development Kit&#xff09;自 JDK 7 起引入的一个强大的诊断工具&#xff0c;用于与正在运行的 JVM&#xff08;Java Virtual Machine&#xff09;实例进行交互。它允许用户执行各种诊断命令&#xff0c;比如线程堆栈分析、堆转储、GC 信…...

Go语言:json 作用和语法

在 Go 语言中&#xff0c;JSON 字段&#xff08;也称为 JSON Tag&#xff09;是附加在结构体字段上的元数据&#xff0c;用于控制该字段在 JSON 编码&#xff08;序列化&#xff09;和解码&#xff08;反序列化&#xff09; 时的行为。它的语法是&#xff1a; type StructName…...

Hive HA配置高可用

Hive的高可用性(HA)通过消除关键组件的单点故障来实现,确保系统在部分故障时仍能正常运行。其基本原理涉及以下核心组件和策略: ‌1. Hive Metastore 的高可用‌ ‌ 多实例部署‌:部署多个Metastore服务实例,每个实例连接到共享的后端数据库(如MySQL、PostgreSQ…...

Ubuntu 第11章 网络管理_常用的网络配置命令

为了管理网络&#xff0c;Linux提供了许多非常有用的网络管理命令。利用这些命令&#xff0c;一方面可以有效地管理网络&#xff0c;另一方面出现网络故障时&#xff0c;可以快速进行诊断。本节将对Ubuntu提供的网络管理命令进行介绍。 11.2.1 ifconfig命令 关于ifconfig命令&…...

【Qt】Qt 构建系统详解:qmake 入门到项目实战

Qt 构建系统详解&#xff1a;qmake 入门到项目实战 本文将系统介绍 Qt 构建工具 qmake 的用法&#xff0c;并通过一个完整的项目结构示例&#xff0c;帮助你掌握 .pro 文件编写、子项目管理、模块依赖等核心技能。 &#x1f9ed; 一、什么是 qmake&#xff1f; qmake 是 Qt 提…...

Python实例题:pygame开发打飞机游戏

目录 Python实例题 题目 pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本 代码解释 初始化部分&#xff1a; 游戏主循环&#xff1a; 退出部分&#xff1a; 运行思路 注意事项 Python实例题 题目 pygame开发打飞机游戏 pygame-aircraft-game使用 Pygame 开发…...

《Zabbix Proxy分布式监控实战:从安装到配置全解析》

注意&#xff1a;实验所需的zabbix服务器的搭建可参考博客 zabbix 的docker安装_docker安装zabbix-CSDN博客 1.1 实验介绍 1.1.1 实验目的 本实验旨在搭建一个基于Zabbix的监控系统&#xff0c;通过安装和配置Zabbix Proxy、MySQL数据库以及Zabbix Agent&#xff0c;实现分…...

华为配置篇-RSTP/MSTP实验

MSTP 一、简介二、常用命令总结三、实验 一、简介 RSTP&#xff08;快速生成树协议&#xff09;​ RSTP&#xff08;Rapid Spanning Tree Protocol&#xff09;是 STP 的改进版本&#xff0c;基于 ​​IEEE 802.1w 标准​​&#xff0c;核心目标是解决传统 STP 收敛速度慢的问…...

git如何将本地 dev 分支与远程 dev 分支同步

要让本地 dev 分支与远程 dev 分支完全同步&#xff08;丢弃本地多余的提交记录&#xff09;&#xff0c;可以按照以下步骤操作&#xff1a; 1. ​​获取远程最新状态​ git fetch origin dev # 拉取远程 dev 分支的最新提交&#xff0c;但不会修改本地代码 IDEA中点击fetc…...

使用Python和OpenCV实现实时人脸检测与识别

前言 在计算机视觉领域&#xff0c;人脸检测与识别是两个非常重要的任务。人脸检测是指在图像中定位人脸的位置&#xff0c;而人脸识别则是进一步识别出人脸的身份。随着深度学习的发展&#xff0c;这些任务的准确性和效率都有了显著提升。OpenCV是一个开源的计算机视觉库&…...

Unity基础学习(九)输入系统全解析:鼠标、键盘与轴控制

目录 一、Input类 1. 鼠标输入 2. 键盘输入 3. 默认轴输入 &#xff08;1&#xff09; 基础参数 &#xff08;2&#xff09;按键绑定参数 &#xff08;3&#xff09;输入响应参数 &#xff08;4&#xff09;输入类型与设备参数 &#xff08;5&#xff09;不同类型轴的参…...

QT:获取软件界面窗口的尺寸大小2025.5.8

在Windows系统中&#xff0c;获取软件界面窗口的尺寸大小可以通过多种方法实现&#xff0c;以下是基于不同场景的详细解决方案&#xff1a; 方法1&#xff1a;使用Windows API获取窗口尺寸 适用于获取外部应用程序窗口的尺寸&#xff08;如记事本、计算器等&#xff09;。 步…...

【ML-Agents】ML-Agents示例项目导入unity报错解决

最近在跑ML-Agents的示例代码&#xff0c;无奈往unity中导入项目后&#xff0c;就出现报错。本文简要描述了各个报错的解决方法。 文章目录 一、error CS0234: The type or namespace name InputSystem does not exist in the namespace UnityEngine (are you missing an assem…...

【网络安全】SQL注入

如果文章不足还请各位师傅批评指正&#xff01; 想象一下&#xff0c;你经营着一家咖啡店&#xff0c;顾客可以通过店内的点单系统下单。这个系统会根据顾客的输入&#xff0c;向后厨发送指令&#xff0c;比如“为顾客A准备一杯拿铁”。 然而&#xff0c;如果有个不怀好意的顾客…...

Transformer Decoder-Only 参数量计算

Transformer 的 Decoder-Only 架构&#xff08;如 GPT 系列模型&#xff09;是当前大语言模型的主流架构&#xff0c;其参数量主要由以下几个部分组成&#xff1a; 嵌入层&#xff08;Embedding Layer&#xff09;自注意力层&#xff08;Self-Attention Layers&#xff09;前馈…...

uni-app 中的条件编译与跨端兼容

uni-app 为了实现一套代码编译到多个平台&#xff08;包括小程序&#xff0c;App&#xff0c;H5 等&#xff09;&#xff0c;引入了条件编译机制。 通过条件编译&#xff0c;我们可以针对不同的平台编写特定的代码&#xff0c;从而实现跨端兼容。 一、条件编译的作用 平台差异…...

<C#>log4net 的配置文件配置项详细介绍

log4net 是一个功能强大的日志记录工具&#xff0c;通过配置文件可以灵活地控制日志的输出方式、格式、级别等。以下是对 log4net 配置文件常见配置项的详细介绍&#xff1a; 根元素 <log4net> 这是 log4net 配置文件的根元素&#xff0c;所有配置项都要包含在该元素内…...

excel单元格如果是日期格式,在C#读取的时候会变成45807,怎么处理

excel单元格如果是日期格式&#xff0c;在C#读取的时候会变成45807&#xff0c;怎么处理 excel单元格如果是日期格式&#xff0c;在C#读取的时候会变成45807&#xff0c;怎么处理 在 C# 中&#xff0c;Excel 日期被表示为一个数字&#xff0c;这是因为 Excel 内部将日期存储为…...

Unity接入SDK之修改Unity启动页面

原理就是在Android Studio新建Activity继承UnityPlayerActivity&#xff0c;然后再Unity中修改启动页面。 一&#xff0c;Android Studio篇 首先新建一个项目&#xff0c; 新建完成之后基于新建的项目新建一个module&#xff0c;选择为Android Library类型 新建的Library再目…...

yarn workspace使用指南

作用 Yarn workspace 是 Yarn 包管理工具中的一个功能&#xff0c;主要用于管理多包项目&#xff08;monorepo&#xff09;。它的主要作用如下&#xff1a; 支持多包结构&#xff1a;允许在一个仓库中管理多个独立的包或项目。项目间依赖管理&#xff1a;方便地在不同包之间添…...

[学习]RTKLib详解:datum.c、download.c与lambda.c

RTKLib详解: datum.c、download.c 与 lambda.c 本文是 RTKLlib详解 系列文章的一篇&#xff0c;目前该系列文章还在持续总结写作中&#xff0c;以发表的如下&#xff0c;有兴趣的可以翻阅。 [学习] RTKlib详解&#xff1a;功能、工具与源码结构解析 [学习]RTKLib详解&#xff…...

VUE el-select下拉框动态设置禁用,删除后恢复可选择

场景&#xff1a;点击新增添加按钮&#xff0c;列表table会新增一条包含下拉菜单的数据&#xff0c;如果其中任何一个下拉框选择了某个值&#xff0c;那么新增的下拉菜单的选项中该值是禁用状态&#xff0c;只能选择其他未被选中过的值。点击删除按钮后&#xff0c;已禁用的选项…...

FPGA----基于ALINX提供的debian实现TCF

引言:接上问,我们使用自制的image.ub和boot.bin以及ALINX提供的debian8根文件系统,构建了petalinux,但是经测试,该文件系统无法启用TCF服务,即无法与Xilinx SDK建立连接,那么我们应该如何解决? FPGA----基于ZYNQ 7020实现定制化的EPICS通信系统-CSDN博客文章浏览阅读4…...

木马查杀篇—Opcode提取

【前言】 介绍Opcode的提取方法&#xff0c;并探讨多种机器学习算法在Webshell检测中的应用&#xff0c;理解如何在实际项目中应用Opcode进行高效的Webshell检测。 Ⅰ 基本概念 Opcode&#xff1a;计算机指令的一部分&#xff0c;也叫字节码&#xff0c;一个php文件可以抽取出…...

1.7 方向导数

&#xff08;底层逻辑演进脉络&#xff09;从"单车道"到"全路网"的导数进化史&#xff1a; 一、偏导数奠基&#xff08;1.6核心&#xff09; 诞生背景&#xff1a;多元函数分析需求 当变量间存在耦合关系时&#xff08;如房价面积单价装修成本&#xff09…...

设计模式系列(01):总览与引导

设计模式系列(01):总览与引导 本文为设计模式系列第1篇,定位为总览和引导,系统梳理设计模式的核心思想、分类、UML、设计原则、典型场景、学习建议与常见误区,适合系统学习与团队协作。 目录 1. 前言2. 设计模式简介3. UML与设计模式4. 术语解释5. UML工具与PlantUML6. 面…...

国产化Excel处理控件Spire.XLS系列教程:如何通过 C# 删除 Excel 工作表中的筛选器

在 Excel 文件中&#xff0c;筛选器&#xff08;Filter&#xff09;是一个常用的数据处理工具&#xff0c;可以帮助用户快速按条件筛选数据行。但在数据整理完成、导出、共享或打印之前&#xff0c;往往需要 删除 Excel 工作表中的筛选器&#xff0c;移除列标题中的下拉筛选按钮…...

第二篇 客户端脚本安全

同源策略 限制了来自不同的"dociment"或脚本&#xff0c;对当前"dociment"读取或设置一些属性。 不同源&#xff1a;host&#xff08;域名或ip&#xff09;&#xff0c;子域名&#xff0c;端口&#xff0c;协议。 对于当前页面来说&#xff0c;页面的源…...

[sklearn] 特征工程

一.字典数据抽取 def dictvec():"""字典数据抽取:return: None"""# 实例化# sparse改为True,输出的是每个不为零位置的坐标&#xff0c;稀疏矩阵可以节省存储空间dict DictVectorizer(sparseFalse) #矩阵中存在大量的0&#xff0c;sparse存储只…...