定时任务生产实践
框架选好了,真正的考验在生产环境:任务依赖怎么编排、配置怎么动态调整、重复执行怎么防、失败怎么告警。本文围绕定时任务的工程实践给出完整方法论。
一、任务设计原则
任务拆分的三个问题
设计任务前先回答:
├─ 边界:这个任务做什么?数据范围是什么?
├─ 粒度:能拆成小任务并行吗?(分片 / 分批)
└─ 频率:多久跑一次合适?(不是越快越好)任务幂等是底线
定时任务天然会重复执行:
├─ 调度中心重试、网络超时重发
├─ 手动补跑、故障转移后重跑
├─ 分片边界重叠
└─ 所以任务必须幂等
幂等手段:
├─ 唯一约束:处理记录表加唯一键(日期 + 业务键)
├─ 状态机校验:只处理"待处理"状态的数据
├─ 分布式锁:同一时间只允许一个实例处理(Redisson)
└─ 业务幂等键:处理前先查"是否已处理"任务示例:每日订单对账
java
@XxlJob("orderDailyCheckJob")
public void orderDailyCheckJob() {
// 1. 获取昨日日期
String bizDate = LocalDate.now().minusDays(1).toString();
// 2. 幂等:检查该日期是否已处理(数据库唯一键)
if (dailyCheckDao.existsByBizDate(bizDate)) {
log.info("{} 已对账,跳过", bizDate);
return;
}
// 3. 分片处理(多个执行器并行)
ShardingUtil.ShardingVO sharding = ShardingUtil.getShardingVo();
List<Order> orders = orderDao.queryByDateAndShard(bizDate,
sharding.getIndex(), sharding.getTotal());
for (Order order : orders) {
checkAndRepair(order); // 幂等修复
}
// 4. 记录处理状态
dailyCheckDao.insert(bizDate);
}二、任务依赖编排
依赖的几种形式
形式一:定时触发 + 轮询等待
├─ 任务B 启动后轮询任务A 的结果表
├─ 简单但有延迟,适合轻量依赖
形式二:消息驱动
├─ 任务A 完成 → 发消息 → 任务B 消费消息后执行
├─ 解耦、可靠,适合跨服务依赖
形式三:工作流 DAG(PowerJob)
├─ 显式声明依赖图,节点完成后自动触发后继
└─ 适合复杂编排
形式四:父任务触发子任务
├─ XXL-Job 中父任务完成后手动触发子任务
└─ 适合简单链路编排注意点
├─ 依赖要有超时:任务A 一直不完成,任务B 不能无限等
├─ 依赖要有重试:A 失败重试策略,避免 B 等到天荒地老
├─ 避免环依赖:任务间不能互相等待
└─ 幂等传递:依赖触发本身可能重复,后继任务要幂等三、动态配置与 CRON 管理
CRON 表达式避坑
常用表达式示例:
0 0 2 * * ? 每天凌晨 2 点
0 */30 * * * ? 每 30 分钟
0 0 0/2 * * ? 每 2 小时
0 0 8-18 * * MON-FRI 工作日 8 点到 18 点每小时
注意:
├─ 避开整点高峰(如 0 0 2 比 0 0 0 稳妥)
├─ 秒级/分钟级任务避免与业务高峰重叠
├─ 夏令时 / 时区问题(统一使用服务器时区)
└─ 表达式用 6 段还是 7 段(秒 分 时 日 月 周 年)要分清动态调整
生产环境动态能力:
├─ 在线修改 CRON:调度中心改配置即时生效(无需重启)
├─ 动态启停:随时暂停/恢复任务
├─ 手动触发:补跑历史数据
└─ 参数化任务:通过任务参数传配置(如业务日期)
实践建议:
├─ 配置变化要有审计(谁改了什么)
├─ 变更前先小范围验证(灰度执行器)
└─ 关键任务改配置后关注首轮执行情况四、任务执行隔离
执行器隔离
隔离维度:
├─ 按业务域拆执行器(订单任务 / 报表任务分开)
├─ 重任务与轻任务分开执行器(避免互相拖累)
└─ 紧急任务单独执行器(保证资源)
举例:
├─ executor-order:订单处理任务
├─ executor-report:报表生成任务(重,慢)
└─ executor-urgent:紧急补偿任务(低延迟要求)线程池隔离
同一执行器内任务间也要隔离:
├─ 任务 A 卡死 → 不能阻塞任务 B
├─ 每个任务默认单线程(XXL-Job JobThread)
└─ 慢任务配置独立 JobHandler + 独立线程池
超时控制:
├─ 任务内设置超时(如 30 分钟)
├─ 超时任务主动终止(XXL-Job 支持 kill)
└─ 防止僵尸任务占着线程五、告警与监控
告警触发场景
需要告警的事件:
├─ 任务执行失败(业务异常)
├─ 任务超时未完成
├─ 任务执行结果为 0(数据异常信号)
├─ 任务连续失败 N 次
└─ 调度中心 / 执行器失联告警通道
告警渠道:
├─ 钉钉 / 企业微信 / 飞书机器人(webhook)
├─ 邮件
├─ 短信(关键任务)
└─ 对接监控平台(Prometheus AlertManager)
告警分级:
├─ WARN:重试后可恢复 → 记录即可
├─ ERROR:影响业务 → 立即通知
└─ FATAL:数据错乱 → 电话 / 值班监控指标
监控维度:
├─ 执行成功率:任务成功率下降 → 告警
├─ 执行耗时:P95 耗时不正常上涨 → 排查
├─ 调度延迟:调度时间与执行时间差过大
├─ 失败重试率:重试过多说明系统不稳定
└─ 任务堆积:等待执行的任务数激增
指标采集:
├─ 调度中心自身监控(Prometheus 暴露指标)
└─ 业务任务内埋点(Micrometer 上报)六、常见生产问题与排查
问题清单
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 任务重复执行 | 网络超时重发、故障转移重跑 | 检查幂等、日志触发记录 |
| 任务不执行 | CRON 错误、执行器失联、任务被暂停 | 检查调度日志、心跳 |
| 任务执行很慢 | 数据量大、SQL 慢、竞争锁 | 看执行耗时、慢 SQL |
| 任务偶发失败 | 数据库连接池耗尽、依赖服务超时 | 看错误日志、重试配置 |
| 多个实例都在跑 | 路由策略配置不当 | 确认单机/分片/广播策略 |
排查方法论
排查流程:
1. 看调度日志:是否触发、路由到哪台
2. 看执行日志:异常栈、执行到哪一步
3. 看业务数据:处理了多少、是否符合预期
4. 看监控:耗时、成功率趋势
5. 复现验证:手动触发小范围验证补偿机制
定时任务也出错,要有补偿手段:
├─ 自动重试:失败任务自动重跑(幂等保证安全)
├─ 手动补跑:通过控制台手动触发
├─ 补偿任务:定期扫描"处理失败"的数据重新处理
└─ 对账兜底:任务处理结果与业务数据对账七、最佳实践清单
定时任务生产实践清单:
├─ 1. 所有任务幂等(唯一约束 / 状态机 / 分布式锁)
├─ 2. 任务独立命名、独立日志目录
├─ 3. 重任务分片并行,避免单机长跑
├─ 4. 依赖要有超时与重试,禁止环依赖
├─ 5. CRON 避开高峰,配置可动态调整
├─ 6. 执行器按业务域隔离,任务间互不拖累
├─ 7. 告警分级,失败/超时/数据异常都要通知
├─ 8. 监控成功率、耗时、调度延迟
└─ 9. 保留补偿与对账手段,允许人工介入总结
定时任务生产实践的核心是四件事:设计上保证幂等,编排上管理依赖,配置上支持动态,运维上监控告警。框架解决"怎么调度",工程实践解决"调得稳不稳"。把幂等、隔离、告警、补偿做扎实,定时任务才能从"能跑"变成"可靠"。