[074][示例]基于Redisson的分布式锁在定时任务中的实践与异常模拟

074示例基于Redisson的分布式锁在定时任务中的实践与异常模拟

本文章代码: gitee , gitcode , github

在分布式系统中,定时任务往往部署在多台服务器上,若不加控制,同一任务可能被多个节点同时执行,引发数据重复、资源竞争甚至系统崩溃。分布式锁 是解决这一问题的标准方案。本文围绕一个基于 Redisson 的分布式锁框架,通过四个具体示例,深入剖析自动续期锁固定租期锁阻塞锁可重入锁的差异,并展示如何通过定时任务模拟锁竞争、超时等异常场景,帮助读者在实际项目中做出正确的选型和调优。


一、框架整体设计

框架提供四个接口,均继承自 TaskRunnerLockable,分别对应四种锁模式:

接口 锁类型 租期策略 可重入 等待时间
AutoRenewalBlockLockTaskRunner 阻塞 自动续期 无(立即失败)
AutoRenewalReentrantLockTaskRunner 可重入 自动续期 可配置(默认1s)
FixedLeaseBlockLockTaskRunner 阻塞 固定租期
FixedLeaseReentrantLockTaskRunner 可重入 固定租期 可配置

每个接口的 run() 方法由框架实现,内部调用 RedissonBlockLockServiceRedissonReentrantLockService 执行加锁逻辑,并将业务逻辑委托给子类的 doRun()。异常统一由 handleException() 处理,但仅捕获 CacheErrorCode 类型异常,其他异常继续向上抛出,保证调度层能感知任务失败。


二、四种锁模式详解

1. 自动续期阻塞锁(Demo1

java 复制代码
public class Demo1AutoRenewalBlockTaskRunner implements AutoRenewalBlockLockTaskRunner {
    @Override
    public String key() { return "auto-renewal-block:demo1"; }
    @Override
    public void doRun(Map<String, String> params) {
        // 模拟业务耗时 0~10 秒
        Thread.sleep(ThreadLocalRandom.current().nextInt(10000));
    }
}
  • 特点 :基于 Redisson 的 看门狗(Watchdog) 机制,锁默认租期 30 秒,业务未完成则自动续期,直至任务结束。
  • 适用场景任务执行时间不可预估,如数据清洗、报表生成等长耗时操作。
  • 注意 :阻塞锁意味着获取锁失败时立即抛出异常,不等待。适合对实时性要求高、宁可失败也不愿排队等待的场景。

2. 自动续期可重入锁(Demo2

java 复制代码
public class Demo2AutoRenewalReentrantTaskRunner implements AutoRenewalReentrantLockTaskRunner {
    @Override
    public Duration waitTime() { return Duration.ofSeconds(1); }
    // doRun 同样随机睡眠 0~10 秒
}
  • 特点 :可重入锁允许同一线程多次获取锁;设置 waitTime=1s,获取锁失败时会等待最多 1 秒,超时抛出异常。
  • 适用场景任务内部存在嵌套锁调用,或希望给锁获取一定的缓冲时间,减少因瞬时竞争导致的失败。
  • 对比:与阻塞锁相比,更"宽容",适合对任务成功率有一定要求的场景。

3. 固定租期阻塞锁(Demo3

java 复制代码
public class Demo3FixedLeaseBlockTaskRunner implements FixedLeaseBlockLockTaskRunner {
    @Override
    public Duration expireTime() { return Duration.ofSeconds(5); }
    // doRun 随机睡眠 0~10 秒
}
  • 特点 :锁租期固定为 5 秒,不会自动续期。若业务执行超过 5 秒,锁会被 Redisson 自动释放,其他节点可获取锁,导致任务并发执行。
  • 适用场景任务执行时间严格可控,且要求锁尽快释放,避免因异常导致锁长期占用。
  • 风险 :必须确保 expireTime 大于任务最大执行时间,否则会出现锁提前释放的"假锁"问题。

4. 固定租期可重入锁(Demo4

java 复制代码
public class Demo4FixedLeaseReentrantTaskRunner implements FixedLeaseReentrantLockTaskRunner {
    @Override
    public Duration waitTime() { return Duration.ofSeconds(1); }
    @Override
    public Duration expireTime() { return Duration.ofSeconds(5); }
}
  • 特点:结合固定租期与可重入,既限制锁存活时间,又支持重入。
  • 注意 :框架调用为 doInLock(key, waitTime(), waitTime(), ...) ------ 这里第二个参数是等待时间,第三个也是等待时间(可能是笔误,预期应为租期)。实际使用需核对框架源码,确保正确传入租期。

三、定时任务制造锁异常

ScheduleConfig 中为每个 Demo 配置了两个定时任务,初始延迟错开,固定延迟 6 秒,模拟多节点同时触发:

java 复制代码
@Scheduled(initialDelay = 3000, fixedDelay = 6000)
public void deom1() { demo1.run(null); }

@Scheduled(initialDelay = 5000, fixedDelay = 6000)
public void deom1_1() { demo1.run(null); }
  • 竞争场景:两个任务间隔 2 秒启动,后续每 6 秒重复。当任务耗时超过 2 秒时,第二次触发时前一次可能尚未释放锁,从而产生锁竞争。
  • 超时场景Demo3 租期 5 秒,业务随机睡眠 0~10 秒,若睡眠超过 5 秒,锁自动失效,第二个任务即可获取锁,造成两个任务同时运行(日志中会出现两个 >>><<< 交叉)。

这些精心设计的触发时序,使得我们在日志中能清晰观察到:

  • 获取锁成功/失败
  • 锁自动续期(通过 Redisson 日志或监控)
  • 锁提前释放导致的并发执行
  • 可重入锁的等待超时

四、异常处理与日志

所有 Demo 均实现了 handleException(),仅记录错误日志:

java 复制代码
@Override
public void handleException(BaseRuntimeException exception) {
    log.error("{}: {}", key(), exception.getMessage());
}

框架在 run() 中捕获 CacheErrorCode 异常后回调该方法,然后 继续抛出异常 ,这样调度层(如 Spring @Scheduled)能感知任务失败,便于触发重试或告警。

注意 :固定租期锁若因执行超时而导致锁被自动释放,业务线程并不感知,仍会继续执行,此时可能出现数据不一致。因此,使用固定租期锁时必须确保 expireTime 宽松,或配合业务逻辑中的"中断"机制(如通过 Thread.interrupt())。


五、选型建议与实践要点

维度 自动续期锁 固定租期锁
执行时长 不确定或很长 确定且较短
锁安全性 高(不会提前释放) 低(可能提前释放)
资源占用 续期带来额外网络开销 无续期,轻量
异常处理 任务崩溃锁也会在租期后释放 需保证租期足够长
  • 可重入性:仅当业务逻辑中需要重复获取同一把锁时再选择可重入锁,否则使用非重入锁更轻量。
  • 等待时间:建议根据任务对延迟的容忍度设置,太短易失败,太长可能堆积请求。
  • 监控与告警:需监控锁获取失败率、持有时间、续期次数等指标,及时发现异常。

六、总结

本文通过一个具体的 Redisson 分布式锁框架,演示了四种锁模式在定时任务中的应用,并利用定时任务触发时序模拟了锁竞争、超时等真实场景。理解自动续期与固定租期的本质区别,结合业务执行时间的特征,是正确选型的关键。同时,异常处理的合理设计能帮助运维人员快速定位问题。

实际生产环境中,还可结合 Spring 的 @Async分布式调度平台(如 XXL-JOB) 进行更精细的控制,但分布式锁依然是保障数据一致性的基石。希望本文能为您在微服务架构中的任务调度设计提供有价值的参考。

相关推荐
小刘在重生~43 分钟前
Java Lock 显式锁案例|ReentrantLock 解决多线程安全问题(超详细实战)
java·笔记·面试
步行cgn1 小时前
Spring 中完整的 Scope 选项详解
java·后端·spring
hai_android1 小时前
Android MeasureSpec 详解
android·java·kotlin
LEE1 小时前
前端转型全栈 03:接口失败也返回 200,OpenAPI 契约与错误码怎么定
前端·后端·全栈
打工仔折腾 AI2 小时前
用 Docker 部署 Excalidraw 手绘白板并配置固定公网访问的完整实践
人工智能·后端·python
山岚的运维笔记2 小时前
mysql 专业笔记 -- 第 36 章:MySQL 管理
运维·数据库·笔记·后端·mysql·oracle·dba
掘金者阿豪2 小时前
Oracle 迁移金仓,50 个存储过程到底要改多少?聊聊实际改造中的几个问题
后端
梅梅绵绵冰2 小时前
大模型应用开发-SpringAI框架
spring·ai
大白802 小时前
iOS 内存深度解析:ARC 到底是怎么管理对象生命周期的
后端