分布式任务调度¶
面试追问地图¶
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 方案演进 | ScheduledExecutor、Quartz、调度中心 | 什么情况下不需要平台 |
| Cron | 时区、夏令时、重复/跳过、错过执行 | 系统时间回拨怎么办 |
| Quartz | JobDetail、Trigger、JobStore、集群共享库 | misfire、集群唯一执行 |
| XXL-JOB | 中心调度、执行器注册、路由策略 | 调度中心故障、回调失败 |
| ElasticJob | 去中心化、注册中心、动态分片 | 扩容重分片、故障转移 |
| 分片 | 稳定分片键、并行度、重平衡 | 数据倾斜、重复处理 |
| 可靠性 | 至少一次、幂等、租约、状态机 | 执行成功但回调失败 |
| 错过执行 | 业务语义、补跑窗口、限速 | 大量积压如何防雪崩 |
| 监控 | 延迟、成功率、耗时、积压、心跳 | 告警风暴和自动止损 |
调度系统负责“何时、在哪执行”,业务负责“重复执行是否安全”。把两者混为一谈,是定时任务事故的主要来源。
一、方案与时间语义¶
定时任务方案如何从单机演进到分布式?¶
频次 ★★★★ · 难度 🟡 · 高频:美团/阿里
| 方案 | 能力 | 适用场景 | 主要限制 |
|---|---|---|---|
ScheduledThreadPoolExecutor |
单进程延迟/周期执行 | 本地缓存刷新、心跳 | 重启丢调度状态、无法集群唯一执行 |
Spring @Scheduled |
声明式单应用调度 | 简单后台任务 | 多副本会重复执行 |
| Quartz | Trigger、持久化、集群 | 应用内复杂调度 | 运维和分片治理能力有限 |
| XXL-JOB | 中心化调度、控制台、路由和日志 | 业务任务统一治理 | 依赖调度中心,执行幂等仍由业务保证 |
| ElasticJob | 去中心化协调、弹性分片 | 大数据量并行处理 | 注册中心和分片语义增加复杂度 |
若任务允许每个实例各跑一次,或可由数据库唯一约束轻松兜底,不必为“看起来高级”引入平台。跨实例唯一执行、动态分片、失败重试、审计和人工补跑出现后,才需要分布式调度。
Cron 表达式有哪些生产陷阱?¶
频次 ★★★ · 难度 🟡
- 时区:部署环境默认时区变化会改变触发时刻,应显式配置业务时区。
- 夏令时:时钟前跳可能跳过一次,回拨可能出现两次;账务日切应基于业务日期和幂等键,而非相信触发次数。
- 执行时长:Cron 只表达触发时间,不保证上一次已完成;要明确允许并发、跳过还是排队。
- 系统时间:节点时间漂移会影响判断,应使用时间同步,并让业务数据时间以权威源为准。
Cron 适合“时间表”,不适合表达任务依赖。复杂 DAG 应由工作流/编排系统管理,不能靠多个 Cron 猜测上游何时完成。
二、Quartz¶
Quartz 的 Job、Trigger、Scheduler、JobStore 如何协作?¶
频次 ★★★★ · 难度 🟡
Job:业务执行逻辑。JobDetail:Job 的定义、身份和参数。Trigger:何时触发,包括 SimpleTrigger 和 CronTrigger。Scheduler:注册、暂停、恢复和触发任务的入口。JobStore:保存调度状态;RAMJobStore 不持久化,JDBCJobStore 可持久化并支持集群。
Quartz 集群中的节点共享同一数据库,竞争到 Trigger 的节点执行任务;实例要配置唯一 instanceId,并保持时钟同步。数据库协调只能避免同一 Trigger 被正常并发获取,不能替业务承诺“绝不重复”:节点执行成功但提交触发状态前宕机,仍可能恢复执行。
常见追问:
- 如何禁止同一个 JobDetail 并发?→ 使用 @DisallowConcurrentExecution;它按 JobKey 生效,不是全局互斥。
- JobDataMap 能放任意对象吗?→ 持久化模式应优先保存简单类型和业务 ID,避免类版本变化导致反序列化失败。
Misfire 应该补跑还是跳过?¶
Misfire 指任务错过了原定触发时间,例如服务停机或线程池耗尽。策略必须由业务语义决定:
- 对账、结算:通常补跑,但要按业务日期幂等,并限制追赶速度。
- 缓存刷新、心跳:通常立即执行一次或直接跳到下一次,逐次补跑没有价值。
- 通知:需要去重和过期判断,不能停机 1 小时后把过期提醒全部发出。
恢复时同时放出大量补跑任务会形成“恢复雪崩”。应设置补跑窗口、批次、并发上限和依赖资源水位。
三、分布式调度平台¶
XXL-JOB 的调度中心和执行器如何协作?¶
频次 ★★★★ · 难度 🟡 · 高频:美团/字节
XXL-JOB 采用中心化调度:调度中心管理任务和触发时间,执行器注册可用地址并暴露任务 Handler;触发时调度中心按路由策略选择执行器,发送执行请求,执行器运行后回调结果。
这种架构把任务治理集中到控制台,便于告警、人工触发、失败重试和日志查询。风险也集中在两条链路:
- 调度请求超时,不代表执行器没收到,盲目重试可能重复执行。
- 任务执行成功但结果回调失败,调度中心可能仍显示失败。
因此业务状态应以业务库为准,调度记录是控制面证据,不能替代交易事实。调度中心应多实例部署并共享持久化存储;执行器注册和心跳只能说明进程可达,不能证明下游依赖健康。
ElasticJob 为什么适合分片任务?¶
频次 ★★★ · 难度 🟡
ElasticJob 是轻量级、去中心化的分布式调度方案,节点通过注册中心协调任务分片。新增或下线执行节点时,分片可重新分配,从而把数据处理能力随节点数扩缩。
它适合“一个大任务可拆为多个独立数据片”的场景,如按用户 ID 或分区处理数据;不适合强依赖顺序、无法划分稳定边界的任务。重分片只决定新任务归属,已经执行中的业务动作仍需幂等和断点记录。
Quartz、XXL-JOB、ElasticJob 如何选择?¶
| 维度 | Quartz | XXL-JOB | ElasticJob |
|---|---|---|---|
| 架构 | 嵌入应用,可共享数据库集群 | 中心化调度中心 + 执行器 | 去中心化执行节点 + 注册中心 |
| 优势 | Trigger 模型成熟、应用内集成 | 控制台和运维治理直观 | 动态分片和弹性扩缩 |
| 适合 | 中等规模复杂定时规则 | 大量业务任务统一管理 | 大数据量并行分片任务 |
| 关注点 | 数据库竞争、集群配置 | 调度中心可用性、回调语义 | 注册中心、重分片和数据倾斜 |
选型先看任务规模、是否分片、运维能力和既有基础设施,不按功能清单盲选。
四、分片与可靠性¶
任务分片如何设计?¶
频次 ★★★★ · 难度 🔴
稳定做法是把业务数据映射到逻辑分片,再把逻辑分片分配给执行节点:
逻辑分片数通常大于节点数,扩容时只重新分配分片,不改变业务数据的分片算法。分片键要分布均匀且支持独立处理;热点租户可单独拆分。任务必须记录游标或检查点,否则节点故障后只能从头扫描。
常见追问:分片数是否等于机器数?→ 不建议。机器数变化频繁,逻辑分片应相对稳定,再由调度系统把多个分片分给机器。
调度平台能保证任务恰好执行一次吗?¶
频次 ★★★★★ · 难度 🔴 · 高频:全厂
通常不能。网络超时、节点宕机和结果回调丢失使调度器无法区分“未执行”和“执行成功但响应丢失”。工程上按至少一次触发 + 业务幂等设计:
- 用
任务类型 + 业务日期/对象 ID生成幂等键。 - 数据库唯一约束或状态机抢占执行权。
- 外部副作用带业务幂等号,或用本地消息表/Outbox 保证最终一致。
- 记录开始、心跳、完成和失败状态,超时任务通过租约重新领取。
INSERT INTO job_execution(job_key, status)
VALUES (?, 'RUNNING')
ON DUPLICATE KEY UPDATE job_key = job_key;
插入成功者执行;已存在者读取状态决定跳过、接管或报警。仅用 Redis 短锁不能覆盖进程长停顿和锁过期,正确性要求高时需要 fencing token 或数据库状态约束,见分布式系统。
重试如何避免把故障放大?¶
- 只重试明确的瞬时错误,不重试参数错误和业务拒绝。
- 指数退避并加随机抖动,限制最大次数和总时长。
- 重试任务与正常任务隔离线程池/队列,防止积压挤占新任务。
- 超过阈值进入人工处理或死信,不无限重试。
五、监控与排障¶
任务调度应该监控什么?¶
- 调度延迟:计划时间到实际开始时间。
- 成功率、失败类型、重试次数。
- 执行耗时及 P95/P99,是否超过调度周期。
- 待执行、运行中、超时和死信数量。
- 执行器在线数、心跳和资源水位。
- 分片处理量和倾斜程度。
告警要带任务名、计划时间、执行实例、幂等键和最后错误。任务失败率暴增时先暂停自动重试或降并发,避免故障下游被恢复流量再次打垮。