用shardcollection按task_id哈希分片,配合mongos路由和findoneandupdate原子操作,可实现多worker并发安全抢任务;无需独立调度中心,因mongodb分片集群已内置数据分发、元数据管理、请求路由与原子更新能力。

直接回答:用 shardCollection 对任务集合按 task_id 或哈希分片,配合 mongos 路由和 findAndModify 原子操作,就能让多个工作节点并发安全地“抢”任务,无需额外协调服务。
为什么不用单独建调度中心?
传统方案常依赖 Redis + Lua 或 ZooKeeper 实现分布式锁来抢任务,但引入新组件意味着运维复杂度翻倍、网络跳转增多、故障点变多。MongoDB 分片集群本身已具备:
- 多节点数据分发能力(shard)
- 元数据统一视图(config server)
- 请求自动路由(mongos)
- 原子更新支持(findAndModify / updateOne with $setOnInsert)
这些能力足够支撑“无状态 worker 直连 mongos 拉取并标记任务”的轻量调度模型。
任务集合怎么分片才不卡死?
分片键选错是最大坑——选 created_at 会导致新任务全写进同一个分片(热点),选 status 会导致所有“pending”任务挤在一块(低基数)。必须满足三点:
- 高基数:优先用
task_id(UUID 或 Snowflake)、或哈希字段,例如{task_id: "hashed"} - 查询高频:worker 查询时必带该字段,比如
db.tasks.find({task_id: "abc123"})→ 定向查询,不广播 - 不可变:任务一旦插入,
task_id绝对不能更新,否则触发跨分片迁移(代价极高)
错误示例:sh.shardCollection("mydb.tasks", {created_at: 1}) —— 时间单调递增,所有新任务涌入最后一个分片,其他分片闲着。
worker 怎么安全“抢”任务?
关键不是“查再改”,而是单次原子操作。靠 findOneAndUpdate 配合过滤条件和更新动作,确保只有一个 worker 拿到任务:
db.tasks.findOneAndUpdate(
{ status: "pending", assignee: null },
{
$set: {
status: "running",
assignee: "worker-03",
started_at: new Date()
}
},
{ returnDocument: "after" }
)
注意几点:
- 这个查询必须包含分片键(如
task_id),否则变成广播查询,性能断崖下跌 - 索引要覆盖查询字段:
db.tasks.createIndex({task_id: 1, status: 1, assignee: 1}) - 不要用
find().limit(1)+update()两步,中间可能被其他 worker 插入
状态更新和失败兜底怎么做?
任务执行完或超时,需更新状态。但要注意:
- 成功完成:用
updateOne更新status和result,同样必须带分片键 - 超时未完成:定期跑一个清理 job,查
status: "running"且started_at超过阈值的任务,重置为"pending"—— 这个 job 也必须走定向查询,否则扫描全集群 - 避免大范围
updateMany:比如“把所有失败任务重试”,若没加分片键,会广播到每个分片,IO 和网络压力爆炸
真正难的不是写代码,而是让所有读写操作都“贴着分片键走”。一旦漏掉,看似功能正常,实则性能随数据量增长直线恶化,排查时才发现是某条日志查询没带 task_id,拖垮了整个集群。











