MongoDB Go驱动跨集合事务完全可行,但必须满足:所有集合同属一个副本集、所有操作共用同一ClientSession及其派生的SessionContext、显式配置读写关注与超时,且禁止跨分片或混用上下文。

Go中mongo-go-driver事务必须在同一个副本集内
跨集合事务在mongo-go-driver里完全可行,但有个硬性前提:所有参与事务的集合必须属于同一个MongoDB副本集(replica set)。分片集群(sharded cluster)不支持跨分片事务,哪怕集合逻辑上“同属一个数据库”——如果它们被分配到不同shard,session.StartTransaction() 会直接报错 TransactionNotSupportedOnShardedCluster。确认部署模式比写代码更关键。
开启事务前必须获取有效ClientSession
事务生命周期绑定在 ClientSession 上,不能复用全局 mongo.Client 直接操作。漏掉 client.StartSession() 或忘记 defer session.EndSession(ctx) 是最常见崩溃点:前者导致 ErrNoTransactionInContext,后者造成连接泄漏和会话堆积。
正确姿势:
session, err := client.StartSession()
if err != nil {
return err
}
defer session.EndSession(ctx)
err = session.WithTransaction(ctx, func(sessCtx mongo.SessionContext) (interface{}, error) {
// 在这里调用 collection.InsertOne、UpdateOne 等,全部传入 sessCtx
_, err := colA.InsertOne(sessCtx, docA)
if err != nil {
return nil, err
}
_, err = colB.UpdateOne(sessCtx, filter, update)
return nil, err
})
跨集合操作必须共用同一个session.Context
事务内所有数据库操作(无论哪个collection)都必须使用由该session派生的 SessionContext,而不是原始 context.Context。混用会导致操作脱离事务上下文,变成独立提交的“幽灵写入”。
立即学习“go语言免费学习笔记(深入)”;
典型错误写法:
- 用
ctx调用colA.Find(),再用sessCtx调用colB.DeleteOne()—— 前者不参与事务 - 在事务函数内新建子goroutine,并把
sessCtx传进去 ——sessCtx不是线程安全的,可能panic - 对不同集合使用不同
Database实例(比如client.Database("db1")和client.Database("db2"))—— 只要它们连的是同一副本集,完全合法;但必须确保所有Collection都从这些Database获取,且全部操作用同一个sessCtx
事务超时与锁等待需主动控制
MongoDB默认事务最大运行时间是60秒,超过即自动abort。但实际瓶颈常是写锁竞争:如果另一个客户端正长时间持有某文档锁,你的事务会在 UpdateOne 处卡住,直到超时或对方释放锁。这容易被误判为网络问题。
建议显式设置:
- 用
options.Session().SetDefaultReadConcern()和SetDefaultWriteConcern()控制一致性级别 - 给事务函数内的每个操作加短超时,例如
col.FindOne(context.WithTimeout(sessCtx, 5*time.Second), filter) - 避免在事务中做HTTP调用、文件IO等不可控延迟操作——事务越长,冲突概率越高
真正棘手的不是语法,而是设计时就要想清楚:哪些业务逻辑必须强一致(适合放事务),哪些可以接受最终一致(用消息队列补偿)。


















