MongoDB 4.4 不支持跨分片多文档事务,仅限单一分片内操作;必须运行在副本集模式下,且所有集合须预先创建、非 system.* 集合,驱动需启用 retryWrites=true。

MongoDB 4.4 的多文档事务不能跨分片集群使用,且必须运行在副本集(Replica Set)模式下——如果你的部署是单节点或分片集群,session.startTransaction() 会直接报错或静默失败。
确认你的部署满足事务前提
MongoDB 事务不是“开箱即用”的功能,它依赖底层架构约束:
- 必须是副本集(哪怕只有 1 个主节点 + 2 个延迟从节点),
mongod启动时需带--replSet rs0参数,并执行过rs.initiate() - 分片集群(sharded cluster)中,事务仅支持单一分片内的集合操作;跨分片事务在 4.4 中不被支持,尝试会抛出
MultiNamespaceTransactionNotSupportedOnShardedCluster - 所有参与事务的集合必须已存在(不能在事务中建集合),且不能是
system.*或admin下的特殊集合 - 连接驱动必须启用
retryWrites=true(Node.js 驱动默认开启,但 Java 驱动需显式配置)
扣款事务的标准写法:startTransaction → update → update → commitTransaction
典型场景:从用户 A 账户扣 100 元,向用户 B 账户加 100 元,要求原子性。关键不是“能不能写”,而是“怎么写才不会因异常导致资金不一致”:
- 必须用同一个
session对象贯穿全部操作,不能每个updateOne都新建 session - 两个
updateOne都要检查result.matchedCount === 1,否则说明账户不存在或余额不足(需在事务内查余额并做条件更新) - 务必包裹
try/catch,并在catch中调用session.abortTransaction()—— 不手动 abort,事务可能卡在 pending 状态,阻塞后续操作 - commit 失败(如超时、主节点切换)时,驱动会自动 abort,但业务层仍应捕获
TransientTransactionError并重试(最多 1 次)
示例(Node.js):
const session = client.startSession();
try {
await session.withTransaction(async () => {
const accounts = db.collection('accounts');
// 扣款:只在余额 >= 100 时才修改
const deduct = await accounts.updateOne(
{ _id: 'A', balance: { $gte: 100 } },
{ $inc: { balance: -100 } },
{ session }
);
if (deduct.matchedCount !== 1) throw new Error('insufficient balance');
// 入账
const credit = await accounts.updateOne(
{ _id: 'B' },
{ $inc: { balance: 100 } },
{ session }
);
});
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
余额校验必须和更新在同一原子操作里
很多人把“查余额”和“扣余额”拆成两步,这是最危险的坑——即使在事务中,findOne() + updateOne() 之间仍有竞态窗口。MongoDB 不支持“先读后写”锁,必须用 $cond 或原子条件更新:
- 错误写法:
const bal = await accounts.findOne({ _id: 'A' }, { session }); if (bal.balance - 正确写法:用
$gte在 update 条件里硬约束,如上例;或用findAndModify返回旧值再判断(但 4.4 中仍推荐条件 update) - 如果需精确知道扣款前余额,可用
findOneAndUpdate的returnDocument: 'before'选项,但它仍依赖条件匹配,不能替代$gte
事务性能与日志开销比想象中高
事务不是免费的:每个事务会在 oplog 中生成一条 applyOps 记录,包含全部变更操作;长时间运行的事务(> 60 秒)会被 server 自动终止(maxTransactionLockRequestTimeoutMS 默认值),且阻塞其他会话对相同文档的写入。
- 避免在事务中做耗时操作(如 HTTP 请求、文件读写、复杂计算)
- 不要用事务替代应用层幂等设计——比如重复提交扣款请求,应靠唯一业务流水号 +
upsert控制,而不是靠事务重试兜底 - 监控
currentOp中secs_running > 30的事务,它们往往是锁瓶颈源头
跨集合扣款能做,但每一步都踩在 MongoDB 事务能力的边界上:副本集是硬门槛,条件更新是安全底线,session 生命周期管理是易漏点。漏掉任意一环,表面成功,实则埋雷。


















