想用 MongoDB 做一个任务队列,怎么保证每个任务只被一个 worker 领走?这篇对比原子领取、两步领取、带条件更新三种写法,再验证优先级、索引和租约。
耗时 0.27 s(7,462 个/秒);领取总数 2,000,不同的任务 2,000(应为 2,000,没有重复领取);各 worker 领到: [259, 240, 239, 253, 246, 264, 257, 242]
数据库里 tries>1 的任务数 0;status 分布 [{'_id': 'running', 'n': 2000}]findOneAndUpdate 把“找一个 pending 的”和“改成 running”合成一个原子操作,2,000 个任务 8 个 worker 零重复。
耗时 2.26 s;领取总数 8,377,不同的任务 2,000(应为 2,000);被多个 worker 同时领到(重复处理)的任务有 6,377 个
先 find 再 update 中间没有保护,多个 worker 找到同一个任务:8,377 次领取对应只有 2,000 个不同任务,6,377 个任务被重复处理。
耗时 2.27 s;领取 2,000,不同 2,000(没有重复);抢占失败(白做的 find + update)共 6,223 次
update 加上 status: "pending" 条件并检查 modified_count 可以避免重复,但抢占失败 6,223 次,白做了很多工作,慢约 8 倍。
300 个任务的领取顺序里 prio 序列是否单调不增: True;前 10 个 prio [4, 4, 4, 4, 4, 4, 4, 4, 4, 4],第 100 / 200 / 300 个 3, 1, 0
用 sort 指定 {prio: -1, _id: 1} 领取,验证了领取顺序是 prio 从高到低。
有索引:0.1 ms(中位数) 无索引:17 ms(中位数)
队列必须有 (status, prio, _id) 索引,否则每次领取要扫描(17 ms 对 0.1 ms)。
4 个任务被领走(租约 1 秒)后 worker 崩溃: [{'_id': 'pending', 'n': 6}, {'_id': 'running', 'n': 4}]
1.2 秒后清扫:把 4 个过期租约的任务放回 pending;[{'_id': 'pending', 'n': 10}];这 4 个任务的 tries = [1, 1, 1, 1](领取过 1 次)领取时写入租约时间,清扫线程把过期的 running 放回 pending,配合 tries 限制重试次数。