锁重试和续约? Redisson: 不错, 正是在下 (源码解读)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 这是最详细的Redisson锁重入, 锁重试, Watchdog看门狗, 锁续约机制源码讲解Redisson分布式锁原理:可重入:利用hash结构记录线程id和重入次数可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制超时续约:利用watchDog,每隔一段时间(releaseTime), 重置超时时间

♨️本篇文章记录的为Redisson 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛
👨‍🔧 个人主页 : 阿千弟
⚡> 点击这里👉👉👉 : Redis专栏学习

@[toc]

🍓为什么使用Redisson

应用场景

请求 a 的锁过期,但其业务还未执行完毕;请求 b 申请到了锁,其也正在处理业务。如果此时两个请求都同时修改了共享的库存数据,那么就又会出现数据不一致的问题,即仍然存在并发问题。在高并发场景下,问题会被无限大。
### 问题解决

对于该问题,可以采用“锁续约”方式解决

  1. 在当前业务进程开始执行时,fork 出一个子进程,用于启动一个定时任务。
  2. 该定时任务的定时时间小于锁的过期时间,其会定时查看处理当前请求的业务进程的锁是否已被删除。
  3. 如果已被删除,则子进程结束;如果未被删除,说明当前请求的业务还未处理完毕,则将锁的时间重新设置为“原过期时间”。
  4. 这种方式称为锁续约,也称为锁续命。

使用 Redisson 的可重入锁可以解决上述问题。

Redisson 内部使用 Lua 脚本实现了对可重入锁的添加、重入、续约(命)、释放。Redisson需要用户为锁指定一个 key,但无需为锁指定过期时间,因为它有默认过期时间(当然,也可指定)。由于该锁具有“可重入”功能,所以 Redisson 会为该锁生成一个计数器,记录一个线程重入锁的次数。

在这里插入图片描述


## 🍉了解Redisson

### Redisson 简介
在生产中,对于 Redisson 使用最多的场景就是其分布式锁 RLock。当然,RLock 仅仅是Redisson 的线程同步方案之一。Redisson 提供了 8 种线程同步方案,用户可针对不同场景选用不同方案。

需要注意的是,为了避免锁到期但业务逻辑没有执行完毕而引发的多个线程同时访问共享资源的情况发生,Redisson 内部为锁提供了一个监控锁的看门狗 watch dog,其会在锁到期前不断延长锁的到期时间,直到锁被主动释放。即会自动完成“锁续命”。

### 可重入锁
Redisson 的分布式锁 RLock 是一种可重入锁。当一个线程获取到锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
- JDK 中的 ReentrantLock 是可重入锁,其是通过 AQS(抽象队列同步器)实现的锁机制
- synchronized 也是可重入锁,其是通过监视器模式(本质是 OS 的互斥锁)实现的锁机制

## 🍅Redisson的锁重入源码解读
### 废话不多说, 我们直接进入源码

在这里插入图片描述

  • waitTime : 获取锁的最大等待时长, 第一次获取锁失败不会立即返回, 而是在等待时间内不断的尝试, 如果这个时间结束了都还没获取成功, 才返回false
  • lease : 锁自动失效释放的时间
  • unit : 时间单位

点进去查看 tryAcquire 发现内部调用的是 tryAcquireAsync

在这里插入图片描述
查看 tryAcquireAsync 内部方法

在这里插入图片描述

查看 tryLockInnerAsync 内部方法

在这里插入图片描述

lua脚本部分执行成功返回的是nil (类似于我们java中的null), 执行失败了反而返回一个结果 : redis.call ( 'pttl', KEYS[1] ) 也就是锁的剩余的有效期

执行了pttl命令, KEYS[1]是锁的名称, pttl和ttl效果是类似的, 都是获取key的剩余有效期, 只不过ttl返回的是s为单位, pttl返回的是ms为单位

现在已经拿到了锁的有效期, 我们现在往回倒一步

在这里插入图片描述

把RFuture返回以后, 这里就有回到了这里 get(tryAcquireAsync((wait, leaseTime, threadId))
get方法就是获取阻塞等待RFuther结果, 等待得到的剩余有效期

在这里插入图片描述

这时就回到了这里

在这里插入图片描述

在这里插入图片描述

这里的subscribe就是订阅释放锁的lua脚本中的publish
如果等待结束还没有收到通知就取消订阅, 并返回获取锁失败

在这里插入图片描述
在这里插入图片描述

java if (ttl>=0 && ttl < time)
- ttl小于time(等待时间), 代表在等待之间锁就已经释放了
- ttl大于time(等待时间), 如果等了time的时间, 经过time的时间,锁还没有被释放, 也就没必要等了

在这里插入图片描述
如果时间还很充足, 就继续while(true)执行上面的代码, 不停的尝试等待,不断的进行这样的循环

这里设计的巧妙之处就在于利用了消息订阅, 信号量的机制, 它不是无休止的这种盲等机制, 也避免了不断的重试, 而是检测到锁被释放才去尝试重新获取, 这对CPU十分的友好

在这里插入图片描述

## 🍊WatchDog续约(续命)源码解读
### 场景
Redisson锁重试的问题是解决了, 但是总会发生一些问题, 如果我们的业务阻塞超时了ttl到期了, 别的线程看见我们的ttl到期了, 他重试他就会拿到本该属于我们的锁, 这时候就有安全问题了, 所以该怎么解决?

我们必须确保锁是业务执行完释放的, 而不是因为阻塞而释放的
### 进入源码

我们再次来看这段代码
在这里插入图片描述

当我们没有设置leaseTime的时候, 也就是leaseTime=-1的时候就用看门狗过期时间来获取锁
watchTimeout默认时间是30s

在这里插入图片描述

当ttlRemainingFuture的异步尝试获取锁完成以后, 先判断执行过程中是否有异常, 如果有异常就直接返回了结束执行.
如果没有发生异常, 则判断ttlRemaining(剩余有效期)是否为空, 为空的话就代表获取锁成功, 执行锁到期续约的核心方法scheduleExpectationRenew

进入scheduleExpectationRenew方法中查看

在这里插入图片描述

这里面的EXPIRATION_RENEWAL_MAP中的key很有意思, 我们进去看一下

在这里插入图片描述
清楚的发现 entryName由 id 和 name 两部分组成
id就是当前的这个连接的id, name 就是 当前锁的名称

在这里插入图片描述

这就好办了, 我们可以这样理解getEntryName获得的就是锁的名称, 而这个EXPIRATION_RENEWAL_MAP是静态的, 那么RedissonLock类的所有实例就都可以看到这个map
而一个RedissonLock类可以创建出很多锁的实例, 每一个锁都会有自己的名字, 那么在这个map中就会有唯一的key也就是getEntryName()与唯一的entry相对应

  • 如果是第一次创建entrymap里放的时候, 这个entry肯定不存在, 所以调用的是putIfAbsent, 这时候往map中放入的就是一个全新的entry, 返回值就是null
  • 如果不是第一次放入,放入的是重入的entry的话, putIfAbsent返回的就是旧的oldEntry

这样做是为了保证同一个锁拿到的永远是同一个entry

下面是更新有效期的方法renewExpectation

internalLockLeaseTime是这样来的
在这里插入图片描述
在这里插入图片描述
这个方法主要开启一段定时任务, 不断的去更新有效期, 定时任务的的时间就是 看门狗时间/3, 也就是10s后刷新有效期

10s后做这样一件事
在这里插入图片描述

刷新有效期
在这里插入图片描述
这段lua脚本重置有效期, 满血复活
在这里插入图片描述
这里实现了递归, 一直调用自己, 这就是锁永不过期的原因

那么问题来了,什么时候释放锁呢?

当然是在释放锁的时候

在这里插入图片描述
在这里插入图片描述

先从map中取出任务, 先移除任务的线程Id, 再取消这个任务, 最后再移除entry
到这里看门狗的流程就已经结束了

总结

执行流程

在这里插入图片描述

Redisson分布式锁原理:

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime), 重置超时时间

在这里插入图片描述

如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对redis感兴趣的朋友,请多多关注💖💖💖
👨‍🔧 个人主页 : 阿千弟

目录
相关文章
|
缓存 NoSQL Java
面试官:如何保证本地缓存的一致性?
面试官:如何保证本地缓存的一致性?
2325 1
|
缓存 Java 应用服务中间件
Tomcat是如何打破"双亲委派"机制的?
上文我们详细了解了类加载以及什么是双亲委派机制,相信很多童鞋都了解Tomcat打破了双亲委派机制,本文将对Tomcat为什么要打破双亲委派机制,以及Tomcat是如何打破双亲委派机制的,进行完整性的复盘与解析。
3646 0
Tomcat是如何打破"双亲委派"机制的?
|
存储 缓存 NoSQL
Redis常见面试题(二):redis分布式锁、redisson、主从一致性、Redlock红锁;Redis集群、主从复制,哨兵模式,分片集群;Redis为什么这么快,I/O多路复用模型
redis分布式锁、redisson、可重入、主从一致性、WatchDog、Redlock红锁、zookeeper;Redis集群、主从复制,全量同步、增量同步;哨兵,分片集群,Redis为什么这么快,I/O多路复用模型——用户空间和内核空间、阻塞IO、非阻塞IO、IO多路复用,Redis网络模型
Redis常见面试题(二):redis分布式锁、redisson、主从一致性、Redlock红锁;Redis集群、主从复制,哨兵模式,分片集群;Redis为什么这么快,I/O多路复用模型
|
10月前
|
存储 缓存 安全
ConcurrentHashMap的实现原理,非常详细,一文吃透!
本文详细解析了ConcurrentHashMap的实现原理,深入探讨了分段锁、CAS操作和红黑树等关键技术,帮助全面理解ConcurrentHashMap的并发机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
ConcurrentHashMap的实现原理,非常详细,一文吃透!
|
9月前
|
供应链 NoSQL Java
关于Redisson分布式锁的用法
Redisson分布式锁是实现分布式系统中资源同步的有效工具。通过合理配置和使用Redisson的各种锁机制,可以确保系统的高可用性和数据一致性。本文详细介绍了Redisson分布式锁的配置、基本用法和高级用法,并提供了实际应用示例,希望对您在实际项目中使用Redisson分布式锁有所帮助。c
1448 10
|
10月前
|
消息中间件 存储 Java
吃透 RocketMQ 消息中间件,看这篇就够了!
本文详细介绍 RocketMQ 的五大要点、核心特性及应用场景,涵盖高并发业务场景下的消息中间件关键知识点。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
吃透 RocketMQ 消息中间件,看这篇就够了!
|
11月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
存储 NoSQL 算法
周四埋下的坑,周五来恶补!! Redisson 加锁、锁自动续期、解锁源码分析
周四埋下的坑,周五来恶补!! Redisson 加锁、锁自动续期、解锁源码分析
620 2
|
11月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
591 1
|
缓存 监控 安全
Spring AOP 详细深入讲解+代码示例
Spring AOP(Aspect-Oriented Programming)是Spring框架提供的一种面向切面编程的技术。它通过将横切关注点(例如日志记录、事务管理、安全性检查等)从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。 在Spring AOP中,切面(Aspect)是一个模块化的关注点,它可以跨越多个对象,例如日志记录、事务管理等。切面通过定义切点(Pointcut)和增强(Advice)来介入目标对象的方法执行过程。 切点是一个表达式,用于匹配目标对象的一组方法,在这些方法执行时切面会被触发。增强则定义了切面在目标对象方法执行前、执行后或抛出异常时所
16225 4