V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
gaobing
V2EX  ›  分享创造

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

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


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

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

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

    操作内存的场景会有问题。
    比如 定期读取数据库刷新规则、消费累计在内存里的待发送邮件信息 等。
    buster
        14
    buster  
       2022-09-07 14:30:25 +08:00
    咦,是增强版的 shedlock 么?
    gaobing
        15
    gaobing  
    OP
       2022-09-07 19:04:17 +08:00
    @wolfie 是会增强所有的,下个版本会更新下,可以取消增强
    siweipancc
        16
    siweipancc  
       2022-09-08 10:53:54 +08:00 via iPhone
    做过类似的代码,基本原理是替换掉默认的扫描后处理逻辑,redis 打执行时间戳避免时钟不同步问题。
    后来不方便运行时维护,又改回去调度框架了
    gaobing
        17
    gaobing  
    OP
       2022-09-09 09:40:53 +08:00
    @siweipancc 是的,分场景。这个项目也不会去对标调度框架,而是解决对分布式定时任务的需求不复杂的场景,能够使用熟悉的 @Scheduled 注解快速实现需求。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1426 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 17:30 · PVG 01:30 · LAX 09:30 · JFK 12:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.