增强 Spring @Scheduled 注解,支持分布式定时任务

2022-09-04 19:50:15 +08:00
 gaobing
微服务项目中,@Scheduled 在有多个实例的情况下无法使用。
引入 xxl job 等又比较重,需要耗费一定的时间,如果项目对定时任务的需求比较简单,完全划不来。
所以对 @Scheduled 进行了增强,使其支持分布式的定时任务。
原理如下:
Spring 的 @Scheduled 使用 cron 的情况下,是按照周期执行的,也就是根据表达式计算的下次执行的时间是固定的。 使用 CronSequenceGenerator 可以计算出下次执行时间,所以就可以依据这个时间对当前周期进行加锁,防止定时任务的重复执行,即在一个周期内只有一个定时任务会被执行,同时相比直接加锁,也不会影响下个周期的定时任务执行。


项目地址: https://github.com/gaoice/distributed-scheduling-spring-boot-starter
2655 次点击
所在节点    分享创造
17 条回复
Asimov01
2022-09-04 20:48:14 +08:00
已 star ,希望能继续完善下去,感谢分享。
Jooooooooo
2022-09-04 21:16:42 +08:00
分布式是咋做到的...
wolfie
2022-09-04 21:50:25 +08:00
正好周一看看
iluolSNS
2022-09-04 23:14:23 +08:00
@Jooooooooo 引入了 redis
Kipp
2022-09-05 00:17:42 +08:00
可以,学习一下
potatowish
2022-09-05 08:04:06 +08:00
感谢分享,这个我也写过,不过分布式锁是在定时任务开始前获取,结束后自动释放。你这种方式是通过 cron 表达式来计算需要加锁的时间。

我有个疑问,key 后面拼接了 nextTime ,如果有多个实例的启动时间不一致,那么都会获取到对应的分布式锁(计算得到的 nextTime 不一样),这样在一个实例的定时任务执行期间,其他实例也有可能启动定时任务。
dqzcwxb
2022-09-05 09:13:19 +08:00
@Jooooooooo #2 加了个分布式锁
gaobing
2022-09-05 10:19:09 +08:00
感谢以上各位的 star 。
@potatowish 多个实例的启动时间虽然不一样,但在一个定时任务的周期内,计算出来的 nextTime 其实是一样的。之所以加这种带 nextTime 周期的锁,是因为用不带 nextTime 分布式锁的话,一个实例执行完当前周期的定时任务后就会释放锁,别的实例因为线程池队列已满等一些原因的话,导致定时任务运行稍晚,此时分布式锁已经被释放,就会重复执行当前周期的定时任务。
pkwenda
2022-09-05 10:31:20 +08:00
理解 @potatowish 说的

https://github.com/gaoice/distributed-scheduling-spring-boot-starter/blob/a25406c6e291a3cf37fc9de9b8a125c2c79e41cb/src/main/java/com/gaoice/distributed/scheduling/aspect/ScheduledAspect.java#L44

这行代码的 key 充满了随机性,大概率都会获取到锁,6L 提到的启动时间,UTC 时间等等因素,如果该定时任务是 10s 一次甚至是 5s 一次,刚启动后和稳定后肯能可能比较好复现吧
gaobing
2022-09-05 10:57:16 +08:00
@pkwenda key 不是随机的,同一个周期计算得到的是固定的值,这样通过 key 就保证了加的锁只锁定当前周期,不会因为时间的误差而影响到下个周期定时任务的执行,你可以执行下这段代码看下 nextTime 的计算结果:
```java
@Test
public void testNextTime() throws Exception {
CronSequenceGenerator c = new CronSequenceGenerator("0/5 * * * * ?");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss");
for (int i = 0; i < 100; i++) {
Date now = new Date();
long nextTime = c.next(now).getTime();
System.out.println(format.format(now) + " 的 nextTime 为:" + nextTime);
Thread.sleep(1000);
}
}
```
我执行的结果:
2022-09-05 10:52:11.011 的 nextTime 为:1662346335000
2022-09-05 10:52:12.012 的 nextTime 为:1662346335000
2022-09-05 10:52:13.013 的 nextTime 为:1662346335000
2022-09-05 10:52:14.014 的 nextTime 为:1662346335000
2022-09-05 10:52:15.015 的 nextTime 为:1662346340000
2022-09-05 10:52:16.016 的 nextTime 为:1662346340000
2022-09-05 10:52:17.017 的 nextTime 为:1662346340000
2022-09-05 10:52:18.018 的 nextTime 为:1662346340000
2022-09-05 10:52:19.019 的 nextTime 为:1662346340000
2022-09-05 10:52:20.020 的 nextTime 为:1662346345000
2022-09-05 10:52:21.021 的 nextTime 为:1662346345000
2022-09-05 10:52:22.022 的 nextTime 为:1662346345000
2022-09-05 10:52:23.023 的 nextTime 为:1662346345000
2022-09-05 10:52:24.024 的 nextTime 为:1662346345000
2022-09-05 10:52:25.025 的 nextTime 为:1662346350000
2022-09-05 10:52:26.026 的 nextTime 为:1662346350000
2022-09-05 10:52:27.027 的 nextTime 为:1662346350000
heroconan
2022-09-05 11:43:00 +08:00
即使是在多个实例的系统时间存在误差的情况下,因为一个实例的定时任务从开始执行起到下次执行前锁都是被占据的,同一个定时任务的执行时间间隔一致,所以可以保证在一个执行周期内同一个定时任务是不会被执行多次。
一个简单示例:
X 表示任务执行,-表示间隔

没有时间误差的情况,两个实例:
X-----X-----X-----X-----X-----X-----X.........
X-----X-----X-----X-----X-----X-----X........
此时两个实例要去竞争锁

存在时间误差,两个实例
X-----X-----X-----X-----X-----X-----X.........
X-----X-----X-----X-----X-----X-----X........
此时第二个实例开始执行任务的时候发现锁已被占用,所以不会执行
heroconan
2022-09-05 11:44:22 +08:00
上面第二个示例被格式化了,所以补充一下

存在时间误差,两个实例
X-----X-----X-----X-----X-----X-----X.........
... [时间误差] X-----X-----X-----X-----X-----X-----X........
此时第二个实例开始执行任务的时候发现锁已被占用,所以不会执行
wolfie
2022-09-07 10:13:27 +08:00
有个小问题,会增强所有的 @Scheduled

操作内存的场景会有问题。
比如 定期读取数据库刷新规则、消费累计在内存里的待发送邮件信息 等。
buster
2022-09-07 14:30:25 +08:00
咦,是增强版的 shedlock 么?
gaobing
2022-09-07 19:04:17 +08:00
@wolfie 是会增强所有的,下个版本会更新下,可以取消增强
siweipancc
2022-09-08 10:53:54 +08:00
做过类似的代码,基本原理是替换掉默认的扫描后处理逻辑,redis 打执行时间戳避免时钟不同步问题。
后来不方便运行时维护,又改回去调度框架了
gaobing
2022-09-09 09:40:53 +08:00
@siweipancc 是的,分场景。这个项目也不会去对标调度框架,而是解决对分布式定时任务的需求不复杂的场景,能够使用熟悉的 @Scheduled 注解快速实现需求。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/877658

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX