聚合管道中$lookup(无索引时)、$out、$merge等阶段会持锁;$lookup对被查集合加读锁,$out/$merge对目标集合加写锁,锁持续至管道结束。

聚合管道里哪些阶段会持锁、等锁
不是所有 $lookup、$group 都触发写锁,但只要管道中出现写操作(如 $out、$merge)或隐式修改(如带 $set + $expr 的 update),WiredTiger 就会按文档粒度加写锁。更隐蔽的是:即使只读聚合,若用到未命中索引的 $lookup(目标集合无对应字段索引),它会在被查集合上执行全表扫描——这会持有该集合的读锁(r),阻塞后续对该集合的写入。
常见错误现象包括:db.currentOp() 返回大量 "waitingForLock": true 且 ns 指向被 $lookup 查询的集合;serverStatus().locks.Collection.acquireWaitCount.w 突增;日志里频繁出现 WT_ROLLBACK。
-
$out和$merge必然触发目标集合的写锁,且锁持续到整个管道结束 -
$lookup对“被查集合”加读锁,若没索引,锁持有时间 = 扫描耗时 -
$facet或多分支聚合本身不加锁,但各分支内若含写操作,锁是并行申请的,容易错序 - 带
allowDiskUse: true的管道,临时文件落盘期间不释放锁,I/O 慢会拉长锁时间
怎么快速定位是哪个聚合在拖慢别人
别翻日志猜,直接查活操作。在主节点运行:
db.currentOp({
"secs_running": {"$gt": 5},
"active": true,
"command.pipeline": {"$exists": true}
})
重点关注返回里的 secs_running、ns(源集合)、command.pipeline(前几个 stage)、client(来源 IP/服务名)。如果多个操作卡在同一个 ns 上,且都含 $lookup 或 $out,基本就是它了。
- 若
secs_running小但waitingForLock为 true → 它正被另一个长聚合阻塞,顺藤摸瓜查那个持锁者的opid - 若
command.pipeline里有$lookup: { from: "users" },而ns是orders,说明锁冲突点在users集合 - 配合
db.serverStatus().metrics.operation.write看写操作突增是否与聚合时间吻合
聚合管道锁问题的实操修复路径
调大 maxTransactionLockRequestTimeoutMillis 对纯聚合无效——它只影响事务内锁等待,而聚合默认不进事务。真正要做的,是拆解锁依赖链。
- 给所有
$lookup.from集合的目标字段建索引,例如db.users.createIndex({ email: 1 }),避免扫描 - 把
$out替成应用层分批bulkWrite(),让锁分散、可控 - 禁用
allowDiskUse,改用内存足够大的实例,或提前用$limit缩小数据集 - 若必须用
$merge,确保on字段有唯一索引,否则 WiredTiger 会升级为集合级锁 - 业务侧加限流:同一时间只允许一个关键聚合运行,用 Redis 锁或 DB 表协调
为什么聚合锁问题容易被误判为“数据库卡了”
因为聚合本身不报错、不超时,它只是慢——而慢的根源常不在 MongoDB 内部,而在外部:比如 $lookup 查的服务响应延迟高,或磁盘 I/O 在刷 journal 时被压满。此时 currentOp 显示它“还在跑”,但 iostat -x 1 的 %util 已达 99%,mongostat 的 faults 每秒几百次。这种情况下,优化聚合语句不如先检查 IO 负载和网络延迟。
最容易被忽略的一点:聚合管道里混用 readConcern: "majority" 和长耗时 stage,会导致 oplog slot 卡住,间接拖慢整个副本集的写入提交——这不是锁问题,但表现一模一样。

















