异步与定时任务
ECShopX Java 有两套互补的后台机制:
| 机制 | 模块 | 典型场景 |
|---|---|---|
| Dispatch 异步总线 | ecshopx-dispatch | 业务触发的 Job、跨模块异步事件消费 |
| XXL-JOB 定时任务 | 各 Bundle 的 cron 包 | 按 Cron 扫表、对账、状态轮询 |
RabbitMQ:
DispatchDriverType枚举含RABBITMQ,但当前生产装配以 Sync + Redis 为主(见DispatchBusConfig);Rabbit 驱动存在但未作为默认路径,二次开发无需优先接入。
A. ecshopx-dispatch
架构概览
业务 Service / Publisher
│
▼
DispatchFacade ← 统一入口(publishEvent / dispatchJob)
│
▼
DispatchCore ← 按 DispatchMode 路由
├─ SYNC → SyncDispatchDriver(当前线程执行)
└─ ASYNC → RedisDispatchDriver(入队)
│
▼
DispatchConsumerRuntime(出队消费、重试、失败记录)
│
▼
DispatchRegistry → DispatchListener / DispatchHandlerSpring 装配入口:ecshopx-bootstrap/.../config/DispatchBusConfig.java。
入口:DispatchFacade
| 方法 | 消息类型 | 用途 |
|---|---|---|
publishEvent(messageName, payload, options) | EVENT | 领域事件 fan-out |
dispatchJob(messageName, payload, options) | JOB | 后台任务(导出、同步分页等) |
二者均构造 DispatchMessage,经 DispatchCore#dispatch 路由。
驱动类型:DispatchDriverType
| 值 | 行为 |
|---|---|
SYNC | 同步执行;SyncDispatchDriver 直接查 DispatchRegistry 并调用 handler / listener |
REDIS | 异步入队;RedisDispatchDriver 写入 Redis List(就绪队列)或 ZSet(延迟队列) |
RABBITMQ | 预留;非当前主路径 |
DispatchOptions 常用工厂:
DispatchOptions.eventDefaults(); // 同步事件
DispatchOptions.jobDefaults(); // 异步 Job,默认重试
DispatchOptions.oplQueuedAfterCommit(); // 订单流程日志:异步 + Redis发布 Job(异步任务)
Job 与 Event 共用总线,区别在于 messageType == JOB 且注册的是 DispatchHandler。
注册 handler(bootstrap):
// DrawCashJobRegistrationConfig
dispatchRegistry.registerJob(AdaPayDispatchJobNames.DRAW_CASH_JOB, drawCashJobHandler);实现 handler(业务模块 dispatch 包):
@Component
public class GetItemsSpecFromOmeJobHandler implements DispatchHandler {
@Override
public void handle(Map<String, Object> payload) {
pagedSyncRunner.consumeQueuedPage(payload);
}
}投递 Job(bootstrap Publisher 或 Service):
dispatchFacade.dispatchJob(jobName, payload, DispatchOptions.jobDefaults());延迟投递
异步消息可在 DispatchOptions 或 ListenerDispatchOptions 中设置 Duration delay:
RedisDispatchDriver#enqueue:若配置了dispatch.redis.delayed-zset-key(默认dispatch:delayed)且delay > 0,消息写入 ZSet,score 为到期时间戳。RedisDispatchDelayedPollScheduler定时调用RedisDelayedDispatchMover#moveReadyMessages,将到期消息移入就绪队列dispatch:{queue}。
重试
| 组件 | 职责 |
|---|---|
RetryPolicy | maxAttempts(默认 3)+ nextDelay(默认 3s);RetryPolicy.platformDefault() |
DispatchRetryDecider | currentAttempt < maxAttempts 则重试 |
RedisDispatchDriver#retry | 重新入队 |
DispatchConsumerRuntime#consume | 捕获异常 → 重试或失败 |
重试次数耗尽后:
FailedJobRecorder#recordFailure写入失败任务表(FailedJobEntity/FailedJobMapper);默认 bootstrap 中DispatchBusConfig#failedJobMapper为 no-op 桩实现(return 1),生产若需落库须替换为真实 Mapper 实现RedisDispatchDriver#fail将消息推入 死信队列dispatch:dead:{queue}
消费运行时
DispatchConsumerRuntime 根据 messageType 分发:
- JOB →
registry.jobHandler(messageName).handle(payload) - EVENT → 按
listenerName精确匹配单个DispatchListener#onEvent
成功:stateRecorder.recordAck;失败且不重试:failedJobRecorder + stateRecorder.recordFail。
二次开发:加异步 Job 步骤
- 在
ecshopx-common/.../*DispatchJobNames定义 job 名(或与已有常量对齐的字符串)。 - 业务模块
dispatch包实现DispatchHandler。 ecshopx-bootstrap/config/*JobRegistrationConfig中registerJob。- 需要投递处调用
dispatchFacade.dispatchJob(...),按需指定queue、delay、RetryPolicy。 - 确保 Redis 可用(
dispatchBusStringRedisTemplate);本地见 快速入门 Compose 或localprofile。
二次开发:加异步事件监听
见 事件系统 注册 DispatchListener 一节;异步 listener 的队列名在 ListenerDispatchOptions.async(queue, delay) 中指定。
B. XXL-JOB 定时任务
角色分工
| 组件 | 部署 | 职责 |
|---|---|---|
| XXL-JOB Admin | 独立进程 / Compose 服务 | Cron 调度、任务配置、执行日志 |
| Executor | 随 ecshopx-java 启动 | 注册到 Admin,接收触发并执行 @XxlJob 方法 |
Admin 地址(注意 Compose vs 本地 profile)
| 环境 | Admin URL |
|---|---|
Docker Compose(docker-compose.yml) | http://localhost:8080/xxl-job-admin |
非 Compose / local profile(application.properties) | http://127.0.0.1:9080/xxl-job-admin |
Compose 内应用通过 --xxl.job.admin.addresses=http://xxl-job-admin:8080/xxl-job-admin 连接 Admin;与宿主机浏览器访问的 8080 映射一致。
Executor 端口
应用侧执行器默认端口 19999(application.properties 中 xxl.job.executor.port=19999)。Admin 调度时需能访问该端口(Compose 网络内通常自动可达)。
关键配置(ecshopx-bootstrap/src/main/resources/application.properties):
xxl.job.admin.addresses=http://127.0.0.1:9080/xxl-job-admin
xxl.job.accessToken=ecshopx-cron-dev
xxl.job.executor.appname=ecshopx-executor
xxl.job.executor.port=19999accessToken 须与 Admin 侧一致(Compose 中 xxl-job-admin 服务同样使用 ecshopx-cron-dev)。
编写 Handler
在业务模块 cron 包声明 Spring @Component,方法标注 @XxlJob("handler-name")。handler 名必须与 Admin 中 JobHandler 完全一致。
真实示例(ecshopx-aliyunsms):
@Slf4j
@Component
@RequiredArgsConstructor
public class ScheduleRunTaskHandler {
private final TaskService taskService;
private final ApplicationEventPublisher eventPublisher;
@XxlJob("aliyunsms-run-task")
public void execute() {
long start = System.currentTimeMillis();
try {
int n = taskService.scheduleRunTask();
log.info("[cron][aliyunsms-run-task] done, cost={}ms, taskRowsIsSendSet={}",
System.currentTimeMillis() - start, n);
} catch (Exception e) {
log.error("[cron][aliyunsms-run-task] failed, cost={}ms",
System.currentTimeMillis() - start, e);
eventPublisher.publishEvent(new CronAlertEvent("aliyunsms-run-task", e));
throw e;
}
}
}订单域另有 @XxlJob("finish-orders") 等 handler,模式相同:委托 *CronService,记录耗时,失败时发布 CronAlertEvent 并 rethrow 以便 Admin 标记失败。
Admin 配置步骤(联调)
- 启动 MySQL / Redis / XXL-JOB Admin /
ecshopx-java(推荐docker compose up -d)。 - 浏览器打开 Admin(Compose:
http://localhost:8080/xxl-job-admin)。 - 执行器管理:确认
ecshopx-executor已注册(应用日志无 token 错误)。 - 任务管理:新增任务,JobHandler 填
@XxlJob注解值(如aliyunsms-run-task),配置 Cron,路由策略选「第一个」或指定执行器。 - 手动触发一次,在 Admin「调度日志」与业务日志中验证。
Dispatch Job vs XXL-JOB:如何选型
| 问题 | 选 Dispatch Job | 选 XXL-JOB |
|---|---|---|
| 由用户操作 / API 触发? | ✅ | ❌ |
| 固定 Cron 扫表? | ❌ | ✅ |
| 需要 Admin 可视化 Cron / 手工补跑? | ❌ | ✅ |
| 需要与 dispatch 事件总线共用 Redis 队列? | ✅ | ❌ |
相关章节
- 事件系统 —
publishEvent、listener 注册、同步边界 - Bundle —
dispatch/cron目录 - 技术说明 — Compose 端口与中间件
- XXL-JOB 官方文档
