分布式调度中单字段status失效,因其无法应对并发更新、状态模糊(如分不清“调度中”与“执行中”)及崩溃后任务卡死;应改用state+assignedat+executedat三字段原子建模,并配合ttl索引与带state约束的复合查询保障可靠性。

别用单字段 status 存任务状态,它扛不住并发更新、查不准、也分不清“调度中”和“执行中”。
为什么 status: "running" 字段在分布式调度里会失效
多个 Worker 同时拉取同个任务,靠
updateOne({ _id: id, status: "pending" }, { $set: { status: "running" } })
- 竞态条件:两个 Worker 几乎同时读到
"pending",都成功写入 <code>"running",任务被重复执行 - 状态模糊:
"running"无法区分是“刚被调度器选中但还没发给 Worker”,还是“Worker 已收到并正在跑”,导致监控误判 - 超时难处理:如果 Worker 崩溃,没人主动把
status改回"pending"或标为"failed",任务就卡死
推荐用三字段组合建模:state + assignedAt + executedAt
把“任务生命周期”拆成可验证的原子阶段,而不是靠一个字符串猜意图:
-
state是主状态机字段,只取有限值:"pending"/"scheduled"/"executing"/"succeeded"/"failed"/"timeout" -
assignedAt记录调度器把任务分配给某个 Worker 的时间(带ISODate),为空表示没被调度过 -
executedAt记录 Worker 实际开始执行的时间,为空表示没真正跑起来
例如判断是否“疑似卡死”:查 { state: "scheduled", assignedAt: { $lt: { $subtract: [ "$$NOW", 30000" ] } } } —— 分配超 30 秒还没执行,就该触发重分配。
用 TTL 索引自动清理失败残留,别靠定时 job 扫
任务失败后,如果长期不清理,pending 和 scheduled 文档会越积越多,拖慢查询。直接在集合上建 TTL 索引更可靠:
db.tasks.createIndex({ "updatedAt": 1 }, { expireAfterSeconds: 86400 })
配合应用层每次状态变更都 $currentDate: { updatedAt: true }。这样:
- 失败超过 24 小时的任务自动消失,不污染主查询路径
- 避免后台 job 全表扫描
find({ state: "failed" }),尤其当任务量达百万级时 - 注意:TTL 删除是异步的,不能用于强一致性场景(比如财务类任务),仅适用于可重试的调度元数据
查询时必须带上 state + 时间范围,否则索引失效
哪怕你建了 { state: 1, scheduledAt: -1 } 复合索引,只要查询里漏掉 state,MongoDB 就可能走全表扫描。常见翻车点:
- 监控页想看“最近 1 小时所有执行记录”,写了
find({ scheduledAt: { $gt: ... } })—— 没state条件,索引白建 - 重试逻辑查
{ executedAt: null },但没加state: "scheduled",结果把一堆"pending"也扫进来了 - 正确姿势:
find({ state: "executing", executedAt: { $exists: false } }),再配合{ state: 1, executedAt: 1 }索引
状态模型本身不复杂,真正容易被忽略的是「每个查询都要有明确的状态边界」——没有这个约束,再好的字段设计也会在高并发下崩出各种边缘 case。











