bigywater.tech
Spring · 146

多实例下的 @Scheduled 重复执行与 ShedLock

部署两个实例,定时任务为什么执行了两遍?ShedLock 是怎么保证只跑一次的?lockAtMostFor 和 lockAtLeastFor 各有什么用?任务比锁时间长会怎样?实例崩了多久能恢复?这篇用两个进程和真实的 MySQL 锁表逐项实测。

18 次
不加锁,两个实例在前 10 秒里合计执行的次数,单实例约 10 次
2.9 秒
任务 4 秒、lockAtMostFor 2 秒时,两次执行重叠的最长时间
5.9 秒
持有锁的实例被 kill -9 后,另一个实例第一次执行的等待时间,lockAtMostFor 为 6 秒

不加锁

实测 · 本机执行输出
  前 10 秒:A 执行 9 次,B 执行 9 次,合计 18 次(单实例应该是 10 次)

两个实例各跑一遍,翻倍。

ShedLock

实测 · 本机执行输出
  运行中 shedlock 表里的行(name, locked_by):(('locked', 'ws-MacBook-Pro.local'),);A、B 是同一台机器上的两个进程,locked_by 都是这台机器的主机名
  20 秒里 A 执行 5 次,B 执行 2 次,合计 7 次;相邻两次执行的间隔(秒)[2.9, 2.0, 3.0, 2.0, 3.0, 3.0];执行者依次是 AAABAAB

先到先得;lockAtLeastFor 拉大最短间隔;锁表一行;locked_by 默认主机名。

任务超时

实测 · 本机执行输出
  一共执行了 5 次,执行者依次是 BABAB;其中有重叠的相邻两次(前者, 后者, 重叠秒数)[('B', 'A', 1.1), ('A', 'B', 2.9), ('B', 'A', 1.1), ('A', 'B', 2.9)]

lockAtMostFor 太短导致并发重叠。

实例崩溃

实测 · 本机执行输出
  A 先拿到锁开始执行,1 秒后被 kill -9;另一个实例 B 在被杀后 5.9 秒才第一次执行(距离那次加锁 6.9 秒,约等于 lockAtMostFor)

锁不释放,要等 lockAtMostFor 到期。

分步演示

ShedLock
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。MySQL 是自己起的独立实例;JdbcTemplateLockProvider 用 usingDbTime();两个实例是同一台机器上的两个进程,所以 locked_by 相同;任务用 Thread.sleep 模拟;次数是一次运行的观察值,抢锁的先后会变
    • 没有验证:Redis / ZooKeeper / MongoDB 等其他 LockProvider;Quartz 集群模式作为替代;ShedLock 的 keepAliveForTask / 锁续期;数据库与应用时钟不一致时不使用 usingDbTime 的风险;事务内锁的行为;@SchedulerLock 与 @Async 的组合;Kubernetes CronJob 作为替代方案
    京ICP备2026067502号-1京公网安备11010502063487号