♨️本篇文章记录的为Redisson 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️
个人主页 : 阿千弟
⚡> 点击这里 : Redis专栏学习
请求 a 的锁过期,但其业务还未执行完毕;请求 b 申请到了锁,其也正在处理业务。如果此时两个请求都同时修改了共享的库存数据,那么就又会出现数据不一致的问题,即仍然存在并发问题。在高并发场景下,问题会被无限大。
对于该问题,可以采用“锁续约”方式解决。
Redisson 内部使用 Lua 脚本实现了对可重入锁的添加、重入、续约(命)、释放。Redisson需要用户为锁指定一个 key,但无需为锁指定过期时间,因为它有默认过期时间(当然,也可指定)。由于该锁具有“可重入”功能,所以 Redisson 会为该锁生成一个计数器,记录一个线程重入锁的次数。
在生产中,对于 Redisson 使用最多的场景就是其分布式锁 RLock。当然,RLock 仅仅是Redisson 的线程同步方案之一。Redisson 提供了 8 种线程同步方案,用户可针对不同场景选用不同方案。
需要注意的是,为了避免锁到期但业务逻辑没有执行完毕而引发的多个线程同时访问共享资源的情况发生,Redisson 内部为锁提供了一个监控锁的看门狗 watch dog,其会在锁到期前不断延长锁的到期时间,直到锁被主动释放。即会自动完成“锁续命”。
Redisson 的分布式锁 RLock 是一种可重入锁。当一个线程获取到锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
点进去查看 tryAcquire 发现内部调用的是 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
如果等待结束还没有收到通知就取消订阅, 并返回获取锁失败
if (ttl>=0 && ttl < time)
如果时间还很充足, 就继续while(true)执行上面的代码, 不停的尝试等待,不断的进行这样的循环
这里设计的巧妙之处就在于利用了消息订阅, 信号量的机制, 它不是无休止的这种盲等机制, 也避免了不断的重试, 而是检测到锁被释放才去尝试重新获取, 这对CPU十分的友好
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相对应
entry
往map
里放的时候, 这个entry
肯定不存在, 所以调用的是putIfAbsent
, 这时候往map
中放入的就是一个全新的entry
, 返回值就是null
entry
的话, putIfAbsent
返回的就是旧的oldEntry
这样做是为了保证同一个锁拿到的永远是同一个entry
下面是更新有效期的方法renewExpectation
internalLockLeaseTime是这样来的
这个方法主要开启一段定时任务, 不断的去更新有效期, 定时任务的的时间就是 看门狗时间/3, 也就是10s后刷新有效期
刷新有效期
这段lua脚本重置有效期, 满血复活
这里实现了递归, 一直调用自己, 这就是锁永不过期的原因
那么问题来了,什么时候释放锁呢?
当然是在释放锁的时候
先从map中取出任务, 先移除任务的线程Id, 再取消这个任务, 最后再移除entry
到这里看门狗的流程就已经结束了
如果这篇【文章】有帮助到你,希望可以给我点个赞,创作不易,如果有对Java后端或者对
redis
感兴趣的朋友,请多多关注
个人主页 : 阿千弟