题目重点:DoorDash 系统设计面经题:Cron 任务如何只在到期后入队,处理主从切换、重复投递、重试、时区与执行幂等。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 可见题目未明确出吞吐、准点误差或任务运行时长。以下训练规模假设 100 万个已登记 cron 任务、平均每分钟 1 万个到期实例、容许秒级触发误差;面试时先用真实 SLO 替换。
- Cron 用指定时区解释,创建时校验表达式;每次 occurrence 有稳定键 (job_id,scheduled_at)。任务可以至少一次执行,业务副作用要靠幂等键;不承诺跨外部系统 exactly once。
- 取消、暂停与更新 schedule 有版本号。已排队 occurrence 是否执行由提交时读取的版本/状态决定,避免旧配置在更新后继续触发。
解题路线
- 持久层保存 jobs(job_id, cron, timezone, next_run_at, version, state) 与 executions(occurrence_key, state, attempt, lease_until, error)。索引 (state,next_run_at) 让调度器按时间取到期任务。
- 多个调度器用数据库行锁或带 lease 的原子 claim 抢到期 job;在同一事务插入唯一 occurrence、计算下次运行时间、推进 next_run_at,并写 outbox。重复 claim 由唯一键消解。
- 调度器只把已经到期的 occurrence 发布到执行队列。若要提前放入队列,队列必须支持可靠的延迟可见/定时消息,worker 仍核对 due_at;不能让普通 FIFO worker 提前执行后再 sleep。
- worker 领取 execution lease,心跳续租,按 occurrence_key 调用业务处理;超时后重试且设置退避/死信。ack 只能在副作用确认后进行,外部副作用需幂等或补偿。
手动走一遍
某 job 计划 10:05 触发:若 10:03 更新 Cron,事务必须推进 next_run_at,因此旧计划不会生成;若更新恰好与 10:05 的 claim 竞争,则比较 occurrence 的配置版本与业务约定的取消边界。若有效 occurrence 在业务调用成功后、ack 前崩溃,下一 worker 会重试同一 occurrence_key,下游幂等保证只产生一次效果。
规模与取舍
按 next_run_at 有序索引取每批 B 个到期任务约 O(log N+B) 数据库扫描成本,具体锁与写放大由数据库决定;生成与执行吞吐随到期实例数扩展。若每分钟 1 万个到期,平均约 167/s,但整点尖峰可能远高于平均,需分片 claim 和队列扩容。
易错点与追问
容易失分的边界
- 把未来 job 直接塞进没有延迟语义的队列,会让 worker 提前执行;worker sleep 又占用消费槽且重启后难恢复。
- 只在内存维护 next_run_at 会在调度器故障后丢触发点;只用分布式锁而没有唯一 occurrence 键,锁过期/主从切换仍可能重复生成。
- Cron 的夏令时跳变会产生不存在或重复的本地时间;需定义跳过、顺延或运行两次的产品规则,并存时区标识与 UTC occurrence。
- 失败重试和业务定时重复是两种不同维度:同一 occurrence 重试保留同一键,下个 schedule tick 是新键。
继续深挖
- 如果要求严格不漏且误差 <1 秒,缩短扫描周期、分片时间索引并监控 due-to-start 延迟;数据库热点和批量整点任务会成为主要约束。
- 如果 job 执行数小时,lease 与 heartbeat 要独立于调度循环;支持取消时需要 cooperative cancellation,而非仅删除队列消息。
- 若使用工作流平台,应说明它如何持久化 timer、重放事件、给 activity 配重试与幂等,而非只报产品名。
