writeconflict 错误表示 mongodb 事务中同一文档被多事务并发修改,wiredtiger 检测到页面版本号变更而拒绝提交;它仅发生于多文档事务,伴随 transienttransactionerror 抛出。

WriteConflict 错误到底在报什么
WriteConflict 不是网络或权限问题,而是 MongoDB 在事务执行过程中检测到同一文档被多个事务同时修改——底层 WiredTiger 引擎发现某页(page)的版本号在事务读取后已被其他写入更新,导致当前事务无法安全提交。它通常伴随 TransientTransactionError 抛出,且只发生在多文档事务中(单文档写入天然原子,不会触发此错误)。
为什么指数退避比简单重试更有效
直接 retry 3 次,间隔 100ms 在高并发下会加剧冲突:所有失败事务几乎同时醒来、再次争抢相同文档,形成“重试风暴”。指数退避通过错开重试时间窗口,显著降低碰撞概率。关键点在于:
- 基础延迟必须带随机抖动(如
Math.random() * baseDelay),否则仍可能同步重试 - 退避上限建议设为 1–2 秒,避免长尾延迟拖垮整体响应时间
- MongoDB 驱动(如 Node.js 的
mongodbv4.13+)已内置maxCommitTimeMS和自动重试逻辑,但仅限于TransientTransactionError,不覆盖自定义业务逻辑中的冲突处理
Node.js 中手动实现带抖动的指数退避
以下是在 session.withTransaction() 外层封装的重试逻辑,适用于需要精细控制重试行为的场景(比如写入前需校验业务状态):
async function writeWithExponentialBackoff(operation, maxRetries = 5) {
let lastError;
for (let i = 0; i setTimeout(r, baseDelay + jitter));
}
}
throw lastError;
}
<p>// 使用示例
await writeWithExponentialBackoff(async () => {
await session.withTransaction(async () => {
const doc = await collection.findOne({ _id: id }, { session });
await collection.updateOne(
{ _id: id },
{ $set: { balance: doc.balance - amount } },
{ session }
);
});
});</p>
真正容易被忽略的优化点
退避只是缓解手段,治本要从事务设计入手:
- 缩小事务作用域:把「查余额 → 扣款 → 记日志」拆成两个事务(扣款用事务,日志异步写),避免日志写入拖长锁持有时间
- 避免在事务中做非数据库操作(如 HTTP 调用、文件读写),它们延长事务生命周期,放大冲突窗口
- 确认索引覆盖:如果事务内
findOne()没走索引,会锁住整个集合范围,让冲突从“单文档”升级为“全集合”级别 - 注意分片键设计:跨分片事务强制两阶段提交,冲突概率和延迟都更高;尽量让高频更新的文档落在同一分片上
退避算法再优雅,也救不了一个锁了 3 秒的事务。











