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

对Redis锁延期的一些讨论与思考

上一篇文章提到使用针对不同的业务场景如何合理使用Redis分布式锁,并引入了一个新的问题

若定义锁的过期时间是10s,此时A线程获取了锁然后执行业务代码,但是业务代码消耗时间花费了15s。这就会导致A线程还没有执行完业务代码,A线程却释放了锁(因为10s到了),第11s B线程发现锁已经释放,重新获取锁也开始执行业务代码。
此时多个线程同时执行业务代码,我们使用锁就是为了保证仅有一个线程执行这一块业务代码,说明这个锁是失效的!

本文将尝试探讨如何处理这个问题!

在这里插入图片描述

下面这个图解释了重置超时时间是什么意思,写一个定时任务,并单独使用一个线程每3s去检查一下是否到终点(任务是否执行完毕),第3s时发现没到终点,重置时间。 假设任务执行完毕需要花费11s。那么锁一共会延期3次,第11s之后,锁被手动释放,如果没释放。等到第19s时,会被自动释放。
在这里插入图片描述
如何实现锁的延期

伪代码:定义锁的结构
key:uuid
value:订单服务if key(锁的唯一标识)是否存在存在,if 锁是否被修改未修改,重置超时时间

这部分有一点需要解释:

  1. 为什么判断锁是否被修改?
    A线程获取了锁之后,B线程修改锁的value为 “文件下载服务”,不加一层校验,A线程就会对修改后的锁操作,而不是原始的锁。

此时你会直接写一个定时任务去实现,会有什么问题吗?
锁延期分为2步(第一步:判断锁;第二步:重置锁),这2步之间是存在间隙的,完全可以在判断锁后,重置锁前发生一些事情(例如恰巧在重置时间前锁被其他线程修改了)。如何才能避免这个间隙不发生意外?

使用lua脚本:使用lua语法实现锁的延期,然后执行这个脚本。lua语法将这两个步骤绑定成一个操作。这也就是为什么提到锁延期的实现,基本都是采用lua实现的根本原因。redis分布式锁自身是有局限性的,不能满足我们的需求,所以我们提出了锁延期。

巧在Redis很支持lua语法,我们只需要按照lua语法要求写好命令,调用Redis提供的方法入口传进去,Redis会自动解析这些命令。更巧在lua语法实现锁延期解决了上面的隐患。。。

        /*** 锁续期*/if (redis.call('exists', KEYS[1]) == 1) then // 锁还存在if (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间return 1endendreturn 0

接下来完整的看一下如何使用Redis锁延期


/*** redis分布式锁* 为了文件拉取加的,可能存在拉取任务耗时很久的情况,增加锁延时操作* @author lixinyu*/
public class RedisDistributeLock {private static final Logger log = LoggerFactory.getLogger(RedisDistributeLock.class);// 默认30秒后自动释放锁private static long defaultExpireTime = 10 * 60 * 1000; // 默认10分钟// 用于锁延时任务的执行private static ScheduledThreadPoolExecutor renewExpirationExecutor;// 加锁和解锁的lua脚本 重入和不可重入两种private static String lockScript;private static String unlockScript;private static String renewScript;// 锁延时脚本private static String lockScript_reentrant;private static String unlockScript_reentrant;private static String renewScript_reentrant;// 锁延时脚本static {/*** 如果指定的锁键(KEYS[1])不存在,则通过set命令设置锁的值(ARGV[1])和超时时间(ARGV[2])。* 如果锁键已存在,则通过pttl命令返回锁的剩余超时时间。*/StringBuilder sb = new StringBuilder();sb.setLength(0);sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 如果不存在这个lockKey锁sb.append("     redis.call('set', KEYS[1], ARGV[1]) ");// 设置锁 ,key-value结构sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 设置锁超时时间sb.append("     return nil ");sb.append(" end ");sb.append(" return redis.call('pttl', KEYS[1]) ");// 如果别的线程已经加锁,返回剩余时间lockScript = sb.toString();/*** 如果锁存在,则删除锁*/sb.setLength(0);sb.append(" if (redis.call('get', KEYS[1]) == ARGV[1]) then ");sb.append("      return redis.call('del', KEYS[1]) ");sb.append(" else return 0 ");sb.append(" end");unlockScript = sb.toString();/*** 可重入锁主要解决的是同一个线程能够多次获取锁的问题,而不是防止多个线程同时获取锁* 这通常发生在方法递归调用、嵌套调用或者同一个方法内部多次执行加锁操作的情况下*/sb.setLength(0);sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 如果不存在这个lockKey锁sb.append("     redis.call('hset', KEYS[1], ARGV[1], 1) ");// 设置锁 ,hash结构,hashkey为当前线程id,加锁数为1sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 设置锁超时时间sb.append("     return nil ");sb.append(" end ");sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 如果当前线程已经加锁sb.append("     redis.call('hincrby', KEYS[1], ARGV[1], 1) ");// 可重入,增加锁计数sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重设置锁超时时间sb.append("     return nil ");sb.append(" end ");sb.append(" return redis.call('pttl', KEYS[1]) ");// 如果别的线程已经加锁,返回剩余时间lockScript_reentrant = sb.toString();/*** 释放锁,通过判断锁的存在、当前线程是否是加锁的线程、以及锁的计数器等条件来实现解锁的操作*/sb.setLength(0);sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 不存在锁,返回1表示解锁成功sb.append("     return 1 ");sb.append(" end ");sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then ");// 存在锁,不是本人加的,返回0失败sb.append("     return 0 ");sb.append(" end ");sb.append(" local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1) ");// 存在自己加的锁,锁计数减一sb.append(" if (counter > 0) then ");// 判断是否要删除锁,或重置超时时间sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");sb.append("     return 0 ");sb.append(" else ");sb.append("     redis.call('del', KEYS[1]) ");sb.append("     return 1 ");sb.append(" end ");sb.append(" return nil ");unlockScript_reentrant = sb.toString();/*** 锁续期*/sb.setLength(0);sb.append(" if (redis.call('exists', KEYS[1]) == 1) then ");// 锁还存在sb.append("     if (redis.call('get', KEYS[1]) == ARGV[1]) then ");sb.append("        redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间sb.append("        return 1");sb.append("     end ");sb.append(" end ");sb.append(" return 0 ");renewScript = sb.toString();/*** 可重入锁续期*/sb.setLength(0);sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 锁还存在sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间sb.append("     return 1 ");sb.append(" end ");sb.append(" return 0 ");renewScript_reentrant = sb.toString();renewExpirationExecutor = new ScheduledThreadPoolExecutor(2);}private String uuid;// 当前锁对象标识private boolean reentrant;// 当前锁是可重入还是不可重入private RedisUtils redisUtils;public RedisDistributeLock(boolean reentrant) {this.uuid = UUIDUtils.randomUUID8();this.reentrant = reentrant;this.redisUtils = SpringApplicationUtils.getBean(RedisUtils.class);}/*** 尝试对lockKey加锁* @author: lixinyu 2023/4/25**/public boolean tryLock(String lockKey) {String script = lockScript;if (reentrant) {script = lockScript_reentrant;}Object result = redisUtils.evalScript(script, ReturnType.INTEGER, 1, lockKey, uuid, String.valueOf(defaultExpireTime));boolean isSuccess = result == null;if (isSuccess) {// 若成功,增加延时任务scheduleExpirationRenew(lockKey, uuid, reentrant);}return isSuccess;}/*** 解锁* @author: lixinyu 2023/4/25**/public void unlock(String lockKey){if (reentrant) {redisUtils.evalScript(unlockScript_reentrant, ReturnType.INTEGER, 1, lockKey, uuid, String.valueOf(defaultExpireTime));} else {redisUtils.evalScript(unlockScript, ReturnType.INTEGER, 1, lockKey, uuid);}}/*** 锁延时,定时任务队列,定时判断一次是否续期*/private void scheduleExpirationRenew(String lockKey, String lockValue, boolean reentrant) {Runnable renewTask = new Runnable(){@Overridepublic void run() {try {String script = renewScript;if (reentrant) {script = renewScript_reentrant;}// 将lua语法传给redis解析Object result = evalScript(script, ReturnType.INTEGER, 1, lockKey, lockValue, String.valueOf(defaultExpireTime));if (result != null && !result.equals(false) && result.equals(Long.valueOf(1))) {// 延时成功,再定时执行scheduleExpirationRenew(lockKey, lockValue, reentrant);log.info("redis锁【" + lockKey + "】延时成功!");}} catch (Exception e) {log.error("scheduleExpirationRenew run异常", e);}}};renewExpirationExecutor.schedule(renewTask, defaultExpireTime / 3, TimeUnit.MILLISECONDS);}
}
 /***  将lua语法传给redis*/ public Object evalScript(String script, ReturnType returnType, int numKeys,String... keysAndArgs){Object value = false;try{value = redisTemplate.execute((RedisCallback<Object>)conn -> {try{byte[][] keysAndArgsByte = new byte[keysAndArgs.length][];for (int i = 0; i < keysAndArgs.length; i++ ){keysAndArgsByte[i] = redisTemplate.getStringSerializer().serialize(keysAndArgs[i]);}return conn.eval(redisTemplate.getStringSerializer().serialize(script), returnType, numKeys,keysAndArgsByte);}catch (SerializationException e){log.error("异常", e);return false;}});}catch (Exception e){log.error("异常", e);}return value;}

使用锁

 private void demo() {RedisDistributeLock lock = new RedisDistributeLock(false);String lockKey = redisSeqPrefix + "lock:" + seqName;try {if (lock.tryLock(lockKey)) {String redisValue = redisUtils.get(redisSeqPrefix + seqName);// 加锁之后再次判断是否超出规定长度,防止并发时重置多次if (redisValue != null && redisValue.length() > seqLength) {redisUtils.set(redisSeqPrefix + seqName, "1");}}} catch (Exception e) {logger.error("resetSeqValue异常", e);} finally {lock.unlock(lockKey);}}

不仅仅是锁延期需要两步(判断锁是否存在、重置时间),删除锁也需要两步(判断锁是否存在、删除锁)这也需要保证原子性,所以建议使用lua脚本实现。

你干脆想到:要不然我获取锁、解锁、获取可重入锁、解可重入锁,锁延期等等都写成lua脚本吧,但是工作量好像就变多了。

Redisson 提供了高级的分布式对象和服务,使用起来非常简单,不需要手动编写复杂的 Lua 脚本。只需要引入Redisson 的依赖库
Redisson 提供了许多高级功能,如分布式集合、分布式锁、分布式队列等。这些功能是为了解决常见的分布式应用场景而设计的,使用 Redisson 可以更轻松地集成这些功能

如果你只是一些简单的功能,可以自定义lua脚本实现,毕竟引入新的依赖库,就需要维护这个库,看怎么考虑了。

相关文章:

对Redis锁延期的一些讨论与思考

上一篇文章提到使用针对不同的业务场景如何合理使用Redis分布式锁&#xff0c;并引入了一个新的问题 若定义锁的过期时间是10s&#xff0c;此时A线程获取了锁然后执行业务代码&#xff0c;但是业务代码消耗时间花费了15s。这就会导致A线程还没有执行完业务代码&#xff0c;A线程…...

【高德地图】Android高德地图初始化定位并显示小蓝点

&#x1f4d6;第3章 初始化定位并显示小蓝点 ✅第1步&#xff1a;配置AndroidManifest.xml✅第2步&#xff1a;设置定位蓝点✅第3步&#xff1a;初始化定位✅完整代码 ✅第1步&#xff1a;配置AndroidManifest.xml 在application标签下声明Service组件 <service android:n…...

继电器测试中需要注意的安全事项有哪些?

继电器广泛应用于电气控制系统中的开关元件&#xff0c;其主要功能是在输入信号的控制下实现输出电路的断开或闭合。在继电器测试过程中&#xff0c;为了确保测试的准确性和安全性&#xff0c;需要遵循一定的安全事项。以下是在进行继电器测试时需要注意的安全事项&#xff1a;…...

Java向ES库中插入数据报错:I/O reactor status: STOPPED

Java向ES库中插入数据报错&#xff1a;java.lang.IllegalStateException: Request cannot be executed; I/O reactor status: STO 一、问题问题原因 二、解决思路 一、问题 在使用Java向ES库中插入数据时&#xff0c;第一次成功插入&#xff0c;第二次出现以下错误&#xff1a…...

vue3实现页面跳转

有需求是在vue项目中实现点击按钮完成页面跳转。这里不适用a标签&#xff0c;而是用vue自带的vue-router。 首先看一下项目结构 src │ App.vue │ main.js │ ├─router │ index.js │ └─views index.vue content.vue 可以看到&…...

【Linux运维系列】vim操作

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

Centos服务器部署前后端项目

目录 准备工作1. 准备传输软件2. 连接服务器 部署Mysql1.下载Mysql(Linux版本)2. 解压3. 修改配置4. 启动服务另一种方法Docker 部署后端1. 在项目根目录中创建Dockerfile文件写入2. 启动 部署前端1. 在项目根目录中创建Dockerfile文件写入2. 启动 准备工作 1. 准备传输软件 …...

【初始RabbitMQ】延迟队列的实现

延迟队列概念 延迟队列中的元素是希望在指定时间到了之后或之前取出和处理消息&#xff0c;并且队列内部是有序的。简单来说&#xff0c;延时队列就是用来存放需要在指定时间被处理的元素的队列 延迟队列使用场景 延迟队列经常使用的场景有以下几点&#xff1a; 订单在十分…...

spark为什么比mapreduce快?

spark为什么比mapreduce快&#xff1f; 首先澄清几个误区&#xff1a; 1&#xff1a;两者都是基于内存计算的&#xff0c;任何计算框架都肯定是基于内存的&#xff0c;所以网上说的spark是基于内存计算所以快&#xff0c;显然是错误的 2;DAG计算模型减少的是磁盘I/O次数&…...

Unity通过XXpermission插件获取MANAGE_EXTERNAL_STORAGE权限

最近公司准备用Unity做一个安卓端的文件管理器功能&#xff0c;文件管理器已经做完了。刚开始的时候想要申请一下所有文件权限&#xff0c;发现在Unity里面申请所有文件权限(android.permission.MANAGE_EXTERNAL_STORAGE)相对来说比较麻烦。所以准备写一下文章记录一下如何申请…...

「连载」边缘计算(二十一)02-26:边缘部分源码(源码分析篇)

&#xff08;接上篇&#xff09; DeviceTwin struct组成剖析 该部分对DeviceTwin struct的组成进行剖析。接着devicetwin struct调用链剖析的实例化DeviceTwin struct&#xff08;dt : DeviceTwin{}&#xff09;往下剖析&#xff0c;进入DeviceTwin struct的定义&#xff0c;…...

Unity(第四部)新手组件

暴力解释就是官方给你的功能&#xff1b;作用的对象上面如&#xff1a; 创建一个球体&#xff0c;给这个球体加上重力 所有物体都是一个空物体&#xff0c;加上一些组件才形成了所需要的GameObject。 这是一个空物体&#xff0c;在Scene场景中没有任何外在表现&#xff0c;因为…...

【JS】【Vue3】【React】获取鼠标位置的方法:JavaScript、Vue 3和React示例

目录 使用JavaScript原生方法在Vue 3中获取鼠标位置在React中获取鼠标位置 随着Web应用程序的复杂性不断增加&#xff0c;获取用户交互信息变得越来越重要。其中&#xff0c;获取鼠标位置是一项常见的任务&#xff0c;可以用于实现各种交互效果&#xff0c;如拖拽、悬停提示等。…...

[Docker 教学] 常用的Docker 命令

Docker是一种流行的容器化技术。使用Docker可以将数据科学应用程序连同代码和所需的依赖关系打包成一个名为镜像的便携式工件。因此&#xff0c;Docker可以简化开发环境的复制&#xff0c;并使本地开发变得轻松。 以下是一些必备的Docker命令列表&#xff0c;这些命令将在你下一…...

小程序应用、页面、组件生命周期

引言 微信小程序生命周期是指在小程序运行过程中&#xff0c;不同阶段触发的一系列事件和函数。这一概念对于理解小程序的整体架构和开发流程非常重要。本文将介绍小程序生命周期的概念以及在不同阶段触发的关键事件&#xff0c;帮助开发者更好地理解和利用小程序的生命周期。 …...

Springboot中如何记录好日志

Springboot中如何记录日志 日志体系整体介绍 日志一直在系统中占据这十分重要的地位&#xff0c;他是我们在系统发生故障时用来排查问题的利器&#xff0c;也是我们做操作审计的重要依据。那么如何记录好日志呢&#xff1f;选择什么框架来记录日志&#xff0c;是不是日志打越…...

vm 虚拟机中ubuntu环境配置共享文件夹的方式

1. 在虚拟机设置中启用共享文件夹选项&#xff0c;映射到Windows中具体的目录。 2. 启动虚拟机。 3. 挂在cd #查看cd设备文件 sudo blkid#创建挂载点 sudo mkdir -p /media/cdrom#挂载cd sudo mount /dev/sr0 /media/cdrom#卸载cd sudo umount /media/cdrom 4. 执行完挂载后…...

EMQX Enterprise 5.5 发布:新增 Elasticsearch 数据集成

EMQX Enterprise 5.5.0 版本已正式发布&#xff01; 在这个版本中&#xff0c;我们引入了一系列新的功能和改进&#xff0c;包括对 Elasticsearch 的集成、Apache IoTDB 和 OpenTSDB 数据集成优化、授权缓存支持排除主题等功能。此外&#xff0c;新版本还进行了多项改进以及 B…...

安全架构设计理论与实践

一、考点分布 安全架构概述&#xff08;※※&#xff09;安全模型&#xff08;※※※&#xff09;信息安全整体架构设计网络安全体系架构设计区块链技术&#xff08;※※&#xff09; 二、安全架构概述 被动攻击&#xff1a;收集信息为主&#xff0c;破坏保密性 主动攻击&#…...

SQL注入漏洞解析--less-46

我们先看一下46关 他说让我们先输入一个数字作为sort,那我们就先输入数字看一下 当我们分别输入1&#xff0c;2&#xff0c;3可以看到按照字母顺序进行了排序&#xff0c;所以它便是一个使用了order by语句进行排序的查询的一种查询输出方式 当输入时出现报错提示&#xff0c;说…...

Arduino ESP32开发指南:从零开始构建物联网应用

Arduino ESP32开发指南&#xff1a;从零开始构建物联网应用 【免费下载链接】arduino-esp32 Arduino core for the ESP32 项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32 Arduino ESP32项目为乐鑫ESP32系列芯片提供了完整的Arduino核心支持&#xff0…...

避坑指南:rosbag合并时的时间戳问题处理(ROS Noetic版)

ROS Noetic下rosbag合并的时间戳陷阱与实战解决方案 在自动驾驶和机器人开发中&#xff0c;rosbag作为数据记录和回放的核心工具&#xff0c;其合并操作看似简单却暗藏玄机。特别是在多传感器数据融合场景下&#xff0c;时间戳处理不当会导致后续算法出现难以排查的时序错乱。本…...

为什么你的文章没人读?聊聊文章可读性

文章可读性不是“写得简单”就完事我以前以为&#xff0c;只要把字写短一点、句子弄直白点&#xff0c;别人就能轻松看懂我的文章。后来才发现&#xff0c;事情没那么简单。文章可读性其实不只是关于词汇难易或句子长短&#xff0c;它更像是一种“读者友好度”——你有没有站在…...

基于宾汉姆流体粘度空间衰减的COMSOL三维离散裂隙恒压注浆模型研究

COMSOL 三维离散裂隙注浆模型。 基于粘度空间衰减的宾汉姆流体注浆。 裂隙采用随机分布的圆盘模型&#xff0c;恒压注浆。 裂隙注浆数值仿真这活儿&#xff0c;说难不难&#xff0c;说简单也够折腾。最近在COMSOL里搭了个三维注浆模型&#xff0c;用宾汉姆流体模拟水泥浆液&am…...

Xftp 7不只是传文件:挖掘同步、直接编辑与图像预览这些被低估的高效功能

Xftp 7高阶技巧&#xff1a;解锁专业用户才知道的远程文件管理方案 当大多数用户还在用Xftp 7进行基础文件传输时&#xff0c;真正的效率高手已经将这套工具玩出了新花样。想象一下&#xff1a;前端设计师无需下载就能快速预览服务器上的图片素材&#xff0c;运维工程师直接在V…...

量子纠错技术:从比特到高维系统的演进与实践

1. 量子纠错基础&#xff1a;从比特到高维系统的范式演进量子计算的核心挑战在于量子态的脆弱性——环境噪声和操作误差会迅速破坏量子信息。我在IBM量子云平台上的实验数据显示&#xff0c;未经保护的量子比特在100次门操作后保真度就会降至50%以下。量子纠错码&#xff08;QE…...

为什么你的下一款小说阅读器必须是开源纯净的ReadCat?

为什么你的下一款小说阅读器必须是开源纯净的ReadCat&#xff1f; 【免费下载链接】read-cat 一款免费、开源、简洁、纯净、无广告的小说阅读器 项目地址: https://gitcode.com/gh_mirrors/re/read-cat 你是否曾经在深夜追更小说时&#xff0c;被突然弹出的广告打断了沉…...

eNSP模拟企业网:手把手教你配置DHCP服务器与中继(含三层交换机实战)

eNSP模拟企业网&#xff1a;手把手教你配置DHCP服务器与中继&#xff08;含三层交换机实战&#xff09; 当企业网络规模不断扩大&#xff0c;手动为每台设备分配IP地址不仅效率低下&#xff0c;还容易出错。DHCP&#xff08;动态主机配置协议&#xff09;作为网络自动化的基石&…...

三步打造个人AI记忆库:微信聊天记录永久保存与智能分析终极指南

三步打造个人AI记忆库&#xff1a;微信聊天记录永久保存与智能分析终极指南 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending…...

保姆级教程:在宝塔面板的PostgreSQL 14/15上,手动编译安装pgvector插件(含常见make错误解决)

从零到一&#xff1a;在宝塔面板的PostgreSQL中手动编译安装pgvector插件全指南 当你需要在PostgreSQL中实现高效的向量相似性搜索时&#xff0c;pgvector插件无疑是最佳选择之一。不同于简单的apt-get或yum安装&#xff0c;手动编译安装能让你更深入地理解插件与数据库的交互…...