跳转至

分布式任务调度

面试追问地图

主问题 必讲关键点 下一层追问
方案演进 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 模型成熟、应用内集成 控制台和运维治理直观 动态分片和弹性扩缩
适合 中等规模复杂定时规则 大量业务任务统一管理 大数据量并行分片任务
关注点 数据库竞争、集群配置 调度中心可用性、回调语义 注册中心、重分片和数据倾斜

选型先看任务规模、是否分片、运维能力和既有基础设施,不按功能清单盲选。


四、分片与可靠性

任务分片如何设计?

频次 ★★★★ · 难度 🔴

稳定做法是把业务数据映射到逻辑分片,再把逻辑分片分配给执行节点:

shard = hash(businessKey) mod logicalShardCount

逻辑分片数通常大于节点数,扩容时只重新分配分片,不改变业务数据的分片算法。分片键要分布均匀且支持独立处理;热点租户可单独拆分。任务必须记录游标或检查点,否则节点故障后只能从头扫描。

常见追问:分片数是否等于机器数?→ 不建议。机器数变化频繁,逻辑分片应相对稳定,再由调度系统把多个分片分给机器。

调度平台能保证任务恰好执行一次吗?

频次 ★★★★★ · 难度 🔴 · 高频:全厂

通常不能。网络超时、节点宕机和结果回调丢失使调度器无法区分“未执行”和“执行成功但响应丢失”。工程上按至少一次触发 + 业务幂等设计:

  1. 用 任务类型 + 业务日期/对象 ID 生成幂等键。
  2. 数据库唯一约束或状态机抢占执行权。
  3. 外部副作用带业务幂等号,或用本地消息表/Outbox 保证最终一致。
  4. 记录开始、心跳、完成和失败状态,超时任务通过租约重新领取。
INSERT INTO job_execution(job_key, status)
VALUES (?, 'RUNNING')
ON DUPLICATE KEY UPDATE job_key = job_key;

插入成功者执行;已存在者读取状态决定跳过、接管或报警。仅用 Redis 短锁不能覆盖进程长停顿和锁过期,正确性要求高时需要 fencing token 或数据库状态约束,见分布式系统。

重试如何避免把故障放大?

  • 只重试明确的瞬时错误,不重试参数错误和业务拒绝。
  • 指数退避并加随机抖动,限制最大次数和总时长。
  • 重试任务与正常任务隔离线程池/队列,防止积压挤占新任务。
  • 超过阈值进入人工处理或死信,不无限重试。

五、监控与排障

任务调度应该监控什么?

  • 调度延迟:计划时间到实际开始时间。
  • 成功率、失败类型、重试次数。
  • 执行耗时及 P95/P99,是否超过调度周期。
  • 待执行、运行中、超时和死信数量。
  • 执行器在线数、心跳和资源水位。
  • 分片处理量和倾斜程度。

告警要带任务名、计划时间、执行实例、幂等键和最后错误。任务失败率暴增时先暂停自动重试或降并发,避免故障下游被恢复流量再次打垮。


参考资料