聚合操作不支持自动重试,因retrywrites仅适用于insert、update等写命令,aggregate属读操作(含$out/$merge也不触发重试),选举期间抛出notmastererror等错误时驱动默认不重试,须应用层识别可重试错误并实现指数退避重试。

聚合操作本身不支持可重试写入,也不能在故障转移期间被 MongoDB 驱动自动重试。 这是关键前提——别指望 aggregate() 像 insertOne() 或 updateOne() 那样触发自动重试逻辑。
为什么 aggregate() 不会被自动重试
MongoDB 的「可重试写入」(retryable writes)仅适用于明确的写操作命令,比如 insert、update、delete、findAndModify 等。而 aggregate() 是读操作(即使含 $out 或 $merge 阶段),其重试行为完全由驱动和客户端控制,服务端不介入。
常见误解是:看到日志里有「not master」或「node is recovering」就以为能靠重试扛过去——实际上,如果 aggregate() 在选举窗口内发到旧 Primary(已降级)或新 Primary 尚未就绪的节点,驱动会直接抛出错误,不会自动换节点重发。
- 错误典型如:
NotMasterError、InterruptedDueToReplStateChange、PrimarySteppedDown - 这些不是网络超时,而是明确的状态拒绝,驱动默认不重试
- 即使启用了
retryWrites=true连接字符串参数,对aggregate()也无效
带 $out 或 $merge 的聚合是否算“写”?
是写,但仍是「非原子写入命令」,不纳入可重试写入范畴。MongoDB 把 $out 和 $merge 视为聚合管道的副作用阶段,整个命令仍以 aggregate 协议发出,而非 insert 或 update 协议。
这意味着:
-
aggregate([{ $out: "target" }])失败时,不会像insertMany()那样自动重试一次 - 若目标集合正在被其他操作锁住(例如 oplog 追同步中),可能报
InterruptedAtShutdown或LockTimeout,驱动也不会重试 - 真正写入发生在管道执行末尾,此时若主节点切换,整个聚合会中断,且无回滚机制
手动实现安全重试的实用做法
必须由应用层兜底。重点不是“无限重试”,而是识别可重试条件 + 设置合理退避 + 避免重复写入。
- 只对明确因拓扑变更导致的错误重试:
NotMasterError、InterruptedDueToReplStateChange、PrimarySteppedDown - 跳过数据一致性错误:
DuplicateKeyError、WriteConflict、BadValue—— 这些重试没意义,甚至更糟 - 用指数退避(如 100ms → 200ms → 400ms),最多 2–3 次;超过说明集群异常,该告警而不是硬扛
- 若聚合含
$out,确保目标集合名稳定,且操作幂等(例如先drop再$out,或用$merge配合唯一索引+onConflict: "replace") - Spring Boot 用户注意:
mongoTemplate.aggregate()不自带重试,需配合@Retryable(Spring Retry)并自定义RetryPolicy判断异常类型
连接配置与读偏好如何影响重试效果
驱动能否快速感知故障并路由到新主节点,取决于连接设置和 readPreference。
- 务必启用
heartbeatFrequencyMS=5000(默认 10s),让驱动更快发现节点状态变化 - 避免使用
readPreference=primaryPreferred调用aggregate()—— 它可能把请求发到刚降级、还没来得及通知客户端的旧 Primary - 对纯读聚合,用
readPreference=secondaryPreferred可绕过主节点压力,但要注意数据延迟;对$out类聚合,必须用primary(否则写失败) - 连接字符串加
?retryWrites=true&maxIdleTimeMS=60000,虽不作用于 aggregate,但有助于整体连接池健康度
真正容易被忽略的是:聚合重试不是“再跑一遍就完事”,而是要结合业务语义判断是否允许重复执行。比如按天聚合订单,重试两次可能生成两份相同日期的结果——这时候与其重试,不如加分布式锁或用时间戳+唯一索引约束写入目标。











