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

2024-02-27 10:44
文章标签 思考 redis 讨论 延期

本文主要是介绍对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锁延期的一些讨论与思考的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/752135

相关文章

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

Redis的数据过期策略和数据淘汰策略

《Redis的数据过期策略和数据淘汰策略》本文主要介绍了Redis的数据过期策略和数据淘汰策略,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录一、数据过期策略1、惰性删除2、定期删除二、数据淘汰策略1、数据淘汰策略概念2、8种数据淘汰策略

Redis存储的列表分页和检索的实现方法

《Redis存储的列表分页和检索的实现方法》在Redis中,列表(List)是一种有序的数据结构,通常用于存储一系列元素,由于列表是有序的,可以通过索引来访问元素,因此可以很方便地实现分页和检索功能,... 目录一、Redis 列表的基本操作二、分页实现三、检索实现3.1 方法 1:客户端过滤3.2 方法

Python中操作Redis的常用方法小结

《Python中操作Redis的常用方法小结》这篇文章主要为大家详细介绍了Python中操作Redis的常用方法,文中的示例代码简洁易懂,具有一定的借鉴价值,有需要的小伙伴可以了解一下... 目录安装Redis开启、关闭Redisredis数据结构redis-cli操作安装redis-py数据库连接和释放增

redis防止短信恶意调用的实现

《redis防止短信恶意调用的实现》本文主要介绍了在场景登录或注册接口中使用短信验证码时遇到的恶意调用问题,并通过使用Redis分布式锁来解决,具有一定的参考价值,感兴趣的可以了解一下... 目录1.场景2.排查3.解决方案3.1 Redis锁实现3.2 方法调用1.场景登录或注册接口中,使用短信验证码场

Redis 多规则限流和防重复提交方案实现小结

《Redis多规则限流和防重复提交方案实现小结》本文主要介绍了Redis多规则限流和防重复提交方案实现小结,包括使用String结构和Zset结构来记录用户IP的访问次数,具有一定的参考价值,感兴趣... 目录一:使用 String 结构记录固定时间段内某用户 IP 访问某接口的次数二:使用 Zset 进行

解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)

《解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)》该文章介绍了使用Redis的阻塞队列和Stream流的消息队列来优化秒杀系统的方案,通过将秒杀流程拆分为两条流水线,使用Redi... 目录Redis秒杀优化方案(阻塞队列+Stream流的消息队列)什么是消息队列?消费者组的工作方式每

Redis如何使用zset处理排行榜和计数问题

《Redis如何使用zset处理排行榜和计数问题》Redis的ZSET数据结构非常适合处理排行榜和计数问题,它可以在高并发的点赞业务中高效地管理点赞的排名,并且由于ZSET的排序特性,可以轻松实现根据... 目录Redis使用zset处理排行榜和计数业务逻辑ZSET 数据结构优化高并发的点赞操作ZSET 结

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

关于rpc长连接与短连接的思考记录

《关于rpc长连接与短连接的思考记录》文章总结了RPC项目中长连接和短连接的处理方式,包括RPC和HTTP的长连接与短连接的区别、TCP的保活机制、客户端与服务器的连接模式及其利弊分析,文章强调了在实... 目录rpc项目中的长连接与短连接的思考什么是rpc项目中的长连接和短连接与tcp和http的长连接短