事务提交失败需按错误码和标签分类处理:code 112(transienttransactionerror)可重试,code 251和50不可重试;unknowntransactioncommitresult须重试整个事务体并新建session;notwritableprimary需重建session;底层配置如enablemajorityreadconcern、版本一致、writeconcern等必须严格匹配。

事务提交失败不是单一错误,而是多种底层状态的聚合表现;必须按错误码和标签分类处理,不能统一重试或忽略。
识别 commitTransaction 抛出的真实错误类型
驱动不会统一抛 MongoServerError,关键要看 error.code 和 error.errorLabels:
-
error.code === 112:名义是NoSuchTransaction,实际可能是会话丢失、事务过期或写冲突回滚——需结合error.errorLabels?.includes('TransientTransactionError')判断是否可重试 -
error.code === 251:InvalidOptions,比如在非 majority 模式下用了linearizable读关注,属于配置错误,重试无意义 -
error.code === 50(MaxTimeMSExpired):事务内某操作超时,通常因maxTimeMS设得太小,或数据模型导致单次操作耗时过高 - 出现
UnknownTransactionCommitResult标签时,服务端无法确认是否提交成功——这不是网络超时,不能只重试commitTransaction()
UnknownTransactionCommitResult 必须重试整个事务体
客户端发出 commit 后没收到明确响应,可能因主节点切换、网络分区或连接中断。此时服务端状态未知:事务可能已提交,也可能被丢弃。
- 直接重试
commitTransaction()会触发NoSuchTransaction或TransactionTooOld,因为txnNumber已失效 - 每次重试都必须新建
ClientSession,显式调用session.startTransaction() - 读操作必须指定
readConcern: "snapshot",否则两次读可能看到不一致快照 - 所有写操作必须幂等:
updateOne({ _id: orderId }, { $setOnInsert: { status: "pending" } }, { upsert: true })替代insertOne();用条件更新替代无保护写入 - 重试前立即调用
session.endSession(),防止连接池中 session 泄漏
NotWritablePrimary 错误要快速失败 + 主动重建
这不是超时或抖动,而是副本集当前无主节点的确定性反馈——驱动在本地拓扑缓存中确认“无主可写”,毫秒级返回,根本不会发请求到服务器。
-
retryWrites=true对事务完全无效,别指望它兜底 - 捕获
NotWritablePrimary或InterruptedDueToReplStateChange后,必须手动重建ClientSession并重放业务逻辑 - 重放前建议先
sleep(200 + jitter),再通过db.runCommand({isMaster: 1})确认主节点是否就位 - 驱动
timeoutMS建议设为 1500(1.5 秒),足够覆盖多数选举窗口,避免过长等待
底层配置不匹配会让代码再正确也高频失败
即使重试逻辑写得再严谨,以下配置项不匹配就会反复触发 UnknownTransactionCommitResult 或静默失败:
- 所有分片/副本集成员必须启用
enableMajorityReadConcern=true,否则 prepare 阶段静默失败 - 各分片 MongoDB 小版本号(含补丁号,如 6.0.12 vs 6.0.15)必须完全一致,
gitVersion也须相同 -
writeConcern必须设为{ w: "majority", j: true };w: 1几乎必然导致提交失败 - 副本集投票节点数必须为奇数且 ≥3,否则
"majority"语义失效 -
heartbeatFrequencyMS应设为 2000(跨 AZ 场景),并同步调整electionTimeoutMillis至 10000,保持 1:5 比例
最容易被忽略的是分片间版本不一致和 enableMajorityReadConcern 未开启——它们不会报错,但会让事务在 prepare 阶段无声失败。











