分布式锁主动续期的入门级实现-自省 | 简约而不简单

简介: 分布式锁主动续期的入门级实现-自省 | 简约而不简单

一、背景

《# 分布式锁上-初探》中有提到一个分布式锁应具备的功能特点中有避免死锁这一条:

如果某个客户端获得锁之后处理时间超过最大约定时间,或者持锁期间内发生了故障导致无法主动释放锁,其持有的锁也能够被其他机制正确释放,并保证后续其它客户端也能加锁,整个处理流程继续正常执行。

简单解释一下:

  1. 客户端抢到分布式锁之后开始执行任务,执行完毕后再释放分布式锁。
  2. 持锁后因客户端异常未能把锁释放,会导致锁成为永恒锁。
  3. 为了避免这种情况,在创建锁的时候给锁指定一个过期时间。
  4. 到期之后锁会被自动删除掉,这个角度看是对锁资源的一种保护。

二、理还乱?

逻辑看很简单,也很清晰,但任何事情都有两面性,自动删除自然有理,但肯定也有弊端。如果要把锁的功能做的健壮,总要从不断地自我质疑、自我反思中,理顺思路,寻找答案,我认为这属于自省式学习,以后也想尝试这种模式,一起来试试吧:

  • 问题:锁过期了会被删掉,可是任务没结束怎么办?
    如果锁被释放的时候,任务尚未执行完毕,那就可能导致其它客户端又抢到锁,任务被重复执行。
  • 问题:把锁的过期时间定的长一点?
    逻辑听起来没错,如果你能确定任务的最大耗时,那没问题;大部分情况都很难确定任务的最大耗时该是多少。
  • 问题:锁的过期时间定多长合适?
    反正会被释放,过期时间定的足够长吧;如果锁使用的频率很高,加了锁程序有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;
}
复制代码
  • 问题:如果续期线程里在 sleep(),那就一直等 sleep() 结束?
    如果等到 sleep() 结束,就挺浪费资源的
  • 问题:能不能快速结束 sleep() 状态?
    可以,通过 interrupt(),需留意,被打断的时候会抛异常 InterruptedException
void cancel(){
    //控制线程退出
    this.isRunning = true;
    //中断线程
    this.interrupt();
}
复制代码

到这里,似乎都理顺了。

三、新的思考

  • 问题:如果同时有成百上千个锁呢?
    同时有成百上千个线程在工作,你若认为没问题,不存在,那ok,不用继续看下一篇。
  • 那怎么办呢?
    可以用 Executors.newScheduledThreadPool ,里边有 scheduleAtFixedRate
  • 阿里 Java 代码规范不允许用Execurots嘛?

2Zmh5D.gif

  • 不能用?风险是什么?你没看累嘛?

四、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。


相关文章
|
3月前
|
NoSQL 关系型数据库 MySQL
分布式锁:不同实现方式实践测评
分布式锁:不同实现方式实践测评
29 0
|
18天前
|
存储 NoSQL Java
分布式锁中的王者方案 - Redission
分布式锁中的王者方案 - Redission
28 1
|
6天前
|
存储 NoSQL 数据库
为什么要用 Tair 来服务低延时场景 - 从购物车升级说起
“购物车升级”是今年双十一期间提升用户体验的关键项目,展示了大淘宝技术团队致力于通过技术突破消费者和商家体验的天花板。低延迟是这些挑战中的核心,内存数据库Tair因其高吞吐、大连接数、热点请求处理、异常流量管理和复杂计算逻辑优化等特点,在低延迟场景下表现出色。Tair使用内存/SCM混合存储和各种索引来提供低延迟服务,并通过无锁并发、水平扩展分区等技术应对高并发。此外,Tair还通过热点策略、流控和执行流程优化等手段确保在大促时的稳定性和性能。Tair在双十一期间支持了购物车、销量统计、卖家优惠券召回和互动场景等多种业务,展现其低延迟和高并发的能力。
76548 10
|
24天前
|
存储 网络协议 数据可视化
【超强笔记软件】Obsidian如何实现免费无限流量无套路云同步?
【超强笔记软件】Obsidian如何实现免费无限流量无套路云同步?
|
3月前
|
弹性计算 数据安全/隐私保护
【零成本】【懒人版】阿里云上雾锁王国/Enshrouded服务搭建教程
【零成本】【懒人版】雾锁王国/Enshrouded服务搭建教程。随着游戏行业的不断发展,玩家们对于游戏体验的要求也越来越高。为了满足玩家们的需求,腾讯云提供了游戏联机服务器一键部署方案,本文将为大家分享基于阿里云服务器10秒钟完成雾锁王国游戏服务器搭建教程,让大家的游戏体验更加顺畅。
|
8月前
|
消息中间件 缓存 NoSQL
|
8月前
|
缓存 数据库
【有奖征文】高并发场景下的缓存穿透、失效和雪崩问题及解决方案
高并发场景下的缓存穿透、失效和雪崩问题及解决方案
41 0
|
12月前
|
消息中间件 安全 Java
实现高并发秒杀的 7 种方式,写的太好了,建议收藏!!
实现高并发秒杀的 7 种方式,写的太好了,建议收藏!!
实现高并发秒杀的 7 种方式,写的太好了,建议收藏!!
|
12月前
|
SQL Web App开发 BI
高并发-【抢红包案例】之二:使用悲观锁方式修复红包超发的bug
高并发-【抢红包案例】之二:使用悲观锁方式修复红包超发的bug
63 0
|
12月前
|
SQL 监控 NoSQL
高并发-【抢红包案例】之三:使用乐观锁方式修复红包超发的bug
高并发-【抢红包案例】之三:使用乐观锁方式修复红包超发的bug
91 0