mongodb事务必须在单个coroutinescope内完成,所有操作需包裹在同一withcontext(dispatchers.io)块中,避免协程切换导致clientsession失效;需显式设置超时、禁止跨协程传递session、分离本地数据库操作。

事务必须在单个CoroutineScope内完成
协程切换线程时,MongoDB驱动的ClientSession会丢失上下文绑定,导致IllegalStateException: Session is not active。这不是驱动 bug,而是事务语义要求:从startTransaction()到commitTransaction()或abortTransaction(),必须在同一个 session 实例、同一线程(或协程调度器)中执行。
常见错误是把withContext(Dispatchers.IO)拆成多次调用,比如先开事务、再切一次调度器查数据、再切回来提交——这会创建新线程上下文,session 已失效。
- 所有事务操作(包括查询、更新、提交/回滚)必须包裹在同一个
withContext(Dispatchers.IO)块内 - 不要在事务块内调用其他 suspend 函数并隐式切换调度器(如未显式指定 dispatcher 的网络请求)
- 避免将
ClientSession作为参数跨协程传递;它不是线程安全的,也不支持跨协程复用
正确使用 mongodb-driver-kotlin-coroutine 的 transaction API
MongoDB Kotlin 协程驱动本身不提供顶层事务挂起函数,需手动封装。官方推荐模式是用withSession + withTransaction组合,它们内部已处理 session 生命周期,但注意:这两个函数本身不是 suspend 的,必须在 suspend 上下文中调用。
示例写法:
suspend fun transferMoney(fromId: String, toId: String, amount: Double) = withContext(Dispatchers.IO) {
client.withSession { session ->
session.withTransaction {
val from = collection.findOneAndUpdate(
Filters.eq("_id", fromId),
Updates.inc("balance", -amount)
)
val to = collection.findOneAndUpdate(
Filters.eq("_id", toId),
Updates.inc("balance", amount)
)
// 若任一操作失败,事务自动 abort
}
}
}
-
client.withSession是阻塞式调用,必须包在withContext(Dispatchers.IO)里 -
session.withTransaction会自动重试 transient 错误(如主节点切换),但不会捕获业务逻辑异常;你仍需在外层 try/catch 处理校验失败等场景 - 事务内所有操作共享同一
collection实例,且必须传入session参数(如collection.findOneAndUpdate(..., session)),否则操作脱离事务上下文
避免在事务中混用 Realm 或 Room 等本地数据库
如果你的业务逻辑需要“先查本地缓存,再决定是否写 MongoDB”,请勿在同一个事务块里调用 Realm/Room 的 suspend 方法。Realm 要求实例与线程强绑定,而 MongoDB 事务强制运行在 IO 线程;两者调度器冲突会导致IllegalStateException: Realm access from incorrect thread或 session 提交失败。
- 本地数据库读取应放在事务外完成(如提前用
withContext(Dispatchers.IO)查 Realm,再把结果传入事务函数) - 禁止在
session.withTransaction块内调用realm.where(...).findAllAsync()或roomDao.getSomething()等 suspend DAO 方法 - 若必须联动,用状态机方式分阶段:先本地决策 → 启动事务 → 执行 MongoDB 操作 → 事务成功后再异步更新本地缓存(用独立协程)
超时与取消必须显式处理
MongoDB 事务默认无协程取消感知能力。withTransaction底层依赖 Java 驱动的timeout配置,但不会响应 Kotlin 的job.cancel()。用户点击返回或页面销毁时,若事务仍在等待 writeConcern 响应,协程会卡住直到超时(默认 60 秒)。
- 务必为
withTransaction设置显式超时:session.withTransaction(TransactionOptions().timeout(5, TimeUnit.SECONDS)) - 在 suspend 函数开头检查
coroutineContext.isActive,非活跃时直接 throwCancellationException,避免进入事务流程 - 不要依赖
ensureActive()来中断正在执行的事务调用——它只能中断挂起点,对阻塞的 driver 调用无效











