Skip to content

DDK Job Starter ​

让定时任务在多实例下同一时刻只在一个实例上执行。它开启 Spring 的 @Scheduled,并接入 ShedLock,锁放在 Redis 里。

text
实例 A  @Scheduled 触发 ──► 抢锁 ddk:job-lock:<应用名>:<任务名>  抢到   ──► 执行任务 ──► 释放(不早于 at-least-for)
实例 B  @Scheduled 触发 ──► 抢锁 ddk:job-lock:<应用名>:<任务名>  被占用 ──► 跳过这一轮

能做什么 ​

能力说明
开启调度不需要再写 @EnableScheduling 和 @EnableSchedulerLock
任务互斥标了 @SchedulerLock 的任务,多个实例里只有抢到锁的那个执行
锁存储默认用应用的 Spring Data Redis 连接;应用声明自己的 ShedLock LockProvider 即可换成数据库等存储
架构规则任务只能写在适配层;可选规则要求每个 @Scheduled 都加锁

引入方式 ​

xml
<dependency>
    <groupId>com.domaindrivenkit</groupId>
    <artifactId>ddk-job-starter</artifactId>
</dependency>
java
package com.acme.order.adapter.job;

@Component
public class OrderJobs {

    private final OrderService orders;

    @Scheduled(cron = "0 */5 * * * *")
    @SchedulerLock(name = "closeExpiredOrders")
    public void closeExpiredOrders() {
        orders.closeExpired();
    }
}

任务类放在 adapter.job,方法里只调用应用服务。定时任务和控制器、MCP 工具一样是用例的入口,只是触发它的是时钟:事务、业务校验和领域事件都应该走同一条路径,不能在任务里直接通过 Mapper 改表。ArchGuard 的 SCHEDULED_JOBS_MUST_RESIDE_IN_ADAPTER 规则在生成的项目里默认检查这一点。

多租户 ​

定时任务开始执行时,OperatorContext 里没有操作者。开启 ddk-mybatis-starter 的多租户后,它的第一条查询就会失败,这是应该的:任务必须说明自己在为哪个租户工作。在任务里逐个租户、以系统操作者的身份执行:

java
@Scheduled(cron = "0 */5 * * * *")
@SchedulerLock(name = "closeExpiredOrders")
public void closeExpiredOrders() {
    for (String tenant : tenants.activeTenantIds()) {          // 租户列表由应用自己提供
        OperatorContext.runAs(new Operator("job:closeExpiredOrders", null, tenant), orders::closeExpired);
    }
}
  • runAs 结束后恢复调度线程原来的上下文,一个租户不会带到下一轮或下一个任务。
  • 操作者标识写成任务名,它会出现在任务写入数据的审计字段里。
  • 不希望一个租户失败就中断其他租户时,按租户分别捕获异常。
  • 不要为了让任务跑通而关掉租户拦截器或豁免 Mapper,那样会读到所有租户的数据。

锁的两个时间 ​

设置含义
lockAtMostFor持锁的最长时间。执行任务的实例宕机后,锁到期才会释放,其他实例才能接手。要比任务的正常耗时长,但不要长太多
lockAtLeastFor持锁的最短时间。任务很快结束时锁仍保留这么久,防止另一个时钟略有偏差的实例把同一轮再执行一次

两者的默认值在 ddk.job.lock.* 下配置,也可以在每个任务的 @SchedulerLock 上单独指定。任务的实际耗时超过 lockAtMostFor 时,下一轮可能在另一个实例上与它重叠,所以任务本身仍然要能安全地重复执行。

漏写 @SchedulerLock 不会报错。 没有这个注解的 @Scheduled 方法会在每个实例上各执行一遍。刷新本地缓存这类任务本来就该如此,但更多时候是忘了。想让漏写变成构建失败,把可选规则加进架构测试:

java
ArchGuard.check(classes,
        CommonArchRules.LAYERED_ARCHITECTURE_RULE,
        CommonArchRules.SCHEDULED_JOBS_MUST_RESIDE_IN_ADAPTER,
        CommonArchRules.SCHEDULED_JOBS_MUST_BE_LOCKED);

没有 Redis 的环境 ​

本地开发环境常常没有 Redis。默认配置下没有 Redis 连接就没有 LockProvider,带 @SchedulerLock 的任务触发时会失败。0.7.0 起,可以为这样的环境选择进程内的锁:

yaml
ddk:
  job:
    lock:
      store: local

锁放在当前进程的内存里:进程内同名任务仍然不会重叠,lockAtMostFor 与 lockAtLeastFor 的含义不变。它不跨实例,只能用在单实例上,启用时启动日志里有一条警告。它必须显式选择,缺少 Redis 连接时不会自动切换过去:一把悄悄变成不共享的锁,比任务直接失败更糟。

配置项 ​

配置项默认值说明
ddk.job.enabledtrue关闭调度,例如本地只想跑接口时
ddk.job.lock.at-most-for10m默认的 lockAtMostFor
ddk.job.lock.at-least-for0s默认的 lockAtLeastFor
ddk.job.lock.storeredis锁放在哪里。local 放在当前进程的内存里,不需要 Redis,只能用于单实例(0.7.0 起)
ddk.job.lock.key-prefixddk:job-lockRedis key 的前缀,完整的 key 是 <前缀>:<spring.application.name>:<任务名>

不适用的场景 / 已知问题 ​

  1. 它不是分布式调度:没有分片、失败重试、控制台和手动触发。需要这些能力时用调度平台。
  2. 没有 XXL-Job 的 starter。XXL-Job 的许可证是 GPL-3.0,DDK 是 Apache-2.0,DDK 的模块不链接它。项目里照常按 XXL-Job 的文档自己配置 XxlJobSpringExecutor 即可,SCHEDULED_JOBS_MUST_RESIDE_IN_ADAPTER 规则同样覆盖 @XxlJob 方法。以后会考虑集成许可证兼容的调度平台。
  3. 锁依赖单个 Redis:主从切换时锁可能丢失,任务有小概率被执行两次。这是任务要能安全重复执行的另一个原因。

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权