一、背景在《??# 分布式锁上-初探??》中有提到一个分布式锁应具备的功能特点中有避免死锁这一条:
如果某个客户端获得锁之后处理时间超过最大约定时间,或者持锁期间内发生了故障导致无法主动释放锁,其持有的锁也能够被其他机制正确释放,并保证后续其它客户端也能加锁,整个处理流程继续正常执行 。
简单解释一下:
- 客户端抢到分布式锁之后开始执行任务,执行完毕后再释放分布式锁 。
- 持锁后因客户端异常未能把锁释放,会导致锁成为永恒锁 。
- 为了避免这种情况,在创建锁的时候给锁指定一个过期时间 。
- 到期之后锁会被自动删除掉,这个角度看是对锁资源的一种保护 。
- 问题:锁过期了会被删掉,可是任务没结束怎么办?如果锁被释放的时候,任务尚未执行完毕,那就可能导致其它客户端又抢到锁,任务被重复执行 。
- 问题:把锁的过期时间定得长一点?逻辑听起来没错,如果你能确定任务的最大耗时,那没问题;大部分情况都很难确定任务的最大耗时该是多少 。
- 问题:锁的过期时间定多长合适?反正会被释放,过期时间定的足够长吧;如果锁使用的频率很高,加了锁程序有bug释放不掉,服务端岂不是要出现大量的垃圾数据?思来想去,对一个健壮的分布式锁来说,过期时间设置太长了不合适,设置太短了也不合适 。
- 问题:怎么平衡?不长不短,主动延期!持锁期间,酌情推后锁的过期时间,以基于redis的分布式锁来说,就需要调用 API 重置锁 key 的过期时间 。当前线程持锁后在执行任务期间不能再调用 API 重试锁 key 的过期时间 。
- 问题:谁来调用API呢?需要使用其他的线程来执行续期 。
- 问题:给每个锁配一个线程?可以,如果使用分布式锁的场景中没有什么并发,一个客户端也就那么三两个锁同时存在,那就没问题 。每个锁抢锁成功后,开启一个线程,在线程中通过循环给锁续期 。
public void run() {while (true) {// 续租action.run();}}
- 问题:多久执行一次续期?有一些常规处理是续租间隔默认采用过期时间的1/3 。若把锁的过期时间设定为与实际耗时相差不大,这样通过一两次续租基本就满足了大部分的情况 。
- 问题:为什么要触发一次续期操作呢,这不浪费资源吗?采用过期时间1/3间隔,若用户定义锁3秒过期,那每秒钟都有一个续期指令,有没有觉得也不太合适 。
- 问题:要不要避免续期指令太频繁?避免续期指令太频繁调用是有必要的,也可以增加一个续期的最小间隔时间,比如最少是5秒 。可由用户自己控制续期周期,没必要一定要发起续期调用 。比如任务执行大多在5秒钟,那么就把锁定为7秒,续期时间定在6秒,那么6秒内任务结束了就不用续期,即不必把过期时间定的太长,也不必执行一两次续期操作 。
- 问题:续租的间隔怎么实现?线程内间隔控制通常是通过 sleep() 方法,稍微精准一点的话,单位使用毫秒 。
public void run() {while (true) {// 1、间隔TimeUnit.MILLISECONDS.sleep(sleepTime);// 2、续租action.run();}}
- 问题:线程要关闭吧?释放锁的时候要主动关闭负责续期的线程,所以线程的循环里要有一个变量来控制退出 while 循环
public void run() {while (isRunning) {// 1、间隔TimeUnit.MILLISECONDS.sleep(sleepTime);// 2、续租action.run();}}
- 问题:变量是跨线程访问,如何保证跨线程的可见性呢?在变量上增加 volatile 关键字 。
private volatile boolean isRunning = true;void cancel(){//控制线程退出this.isRunning = true;}