SWE · System / Product Design

Meta 任务调度与过期清理:租约、取消和后台线程

设计可并发访问的任务调度服务,区分执行租约与过期时间,并防止清理线程误删新任务。

题目重点:设计可并发访问的任务调度服务,区分执行租约与过期时间,并防止清理线程误删新任务。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 两个相关变体分别是分布式job scheduler和同进程job带expire+后台cleaner;一个是任务执行调度,一个是内存生命周期,本文分层说明而不混同。
  • 训练jobId唯一、version随替换增加,expire是不可使用时刻。执行重复是否允许要明确;对外副作用通常不能仅靠scheduler保证恰好一次。
  • clock选单机monotonic用于相对TTL,分布式绝对deadline由权威服务转换;不能把wall clock后拨忽略掉。

解题路线

  1. 单机map存job/version/expireAt,最小堆存(expireAt,jobId,version)。所有结构变更同锁;cleaner睡到最近deadline,新增更早任务用Condition通知,醒来重新检查当前时间而非相信sleep时长。
  2. pop到期堆项时只删除map中同version且真正过期的记录;替换后的旧堆项可丢弃。get同锁判断到期,因此清理线程延迟不会让过期job继续可见。
  3. 分布式scheduler把scheduledAt与expireAt分别持久化;到时worker通过CAS READY→RUNNING取得lease/epoch,heartbeat续约。租约过期允许重试,完成提交要验证attemptEpoch,旧worker不能覆盖新结果。
  4. 业务侧用operationId幂等或事务outbox把副作用与完成结果可恢复关联;取消是请求状态,不保证已经发生的外部动作回滚。运行中的取消需要协作检测,并提供终态CANCELLED/COMPLETED竞争规则。
  5. 重启扫描到期和可执行任务恢复,不依赖唯一内存heap。分片按jobId/租户并用公平队列,观察deadline miss、queue lag、retry与执行P99。

手动走一遍

jobA version1在t=10过期,t=8被替换为version2/expire20;t=10 cleaner弹旧项必须忽略,不删version2。分布式attempt1失联后attempt2执行成功,attempt1迟到提交必须拒绝旧epoch。

规模与取舍

单机heap插入O(log J),到期删除每项O(log J),map查询预期O(1);旧堆项会累积,需要按阈值重建。分布式容量由arrivalRate×平均runtime决定平均运行槽位,另加峰值与故障冗余。

易错点与追问

容易失分的边界

  • 过期时间与执行租约到期不是同一含义;重试已业务过期的任务可能造成无效副作用。
  • cleaner只按jobId删除,遇到替换会误删新任务。
  • 每job一个线程/timer会在大规模耗资源,应使用共享调度线程与堆/时间轮。

继续深挖

  • 时间轮适合海量粗粒度期限,但精度、长周期圈数和重启恢复需解释。
  • 如何证明单机cleaner无忙等?Condition loop处理伪唤醒,新deadline通知,并在持锁区只做短操作。
✓已复制微信号!