将读操作尽量移到事务外面执行完全指南|Duuu笔记
事务内读操作拖慢MongoDB性能,因其强制快照读导致锁范围扩大、快照开销上升、WiredTiger缓存压力增大;仅两类读必须留在事务内:依赖一致性的读和用于写冲突判断的读。
为什么事务里做读操作会拖慢 MongoDB 性能
MongoDB 事务本质是加锁 + 日志 + 一致性快照,只要读操作在
session.startTransaction()
和
session.commitTransaction()
之间,就会强制走事务快照读(snapshot read),哪怕你只是
collection.findOne()
查一条不相关的文档。这会导致:锁范围扩大、快照维护开销上升、WiredTiger cache 压力变大——尤其在高并发写场景下,读操作反而成了事务瓶颈。
哪些读操作必须留在事务内
只有两类读操作不能移出去:
依赖事务一致性的读
(比如“先查余额,再扣款”)和
用于写冲突判断的读
(如基于旧值做条件更新)。其他所有读都该挪走。常见误留场景包括:
collection.countDocuments()
统计用于日志或监控(与业务逻辑无关)
collection.find().toArray()
加载配置或字典数据(这些数据本身不参与事务逻辑)
为日志拼接用户昵称而查
users.findOne()
(昵称不参与扣款/状态变更)
怎么安全地把读移到事务外
核心原则是:**读操作必须在事务开始前完成,且结果传入事务函数作为不可变输入**。注意三点:
不要在事务内调用异步读(如
await collection.findOne()
),哪怕它看起来“只是读”
如果读结果用于写条件(例如
updateOne({ _id, status: "pending" })
),确保该读发生在事务快照中——此时必须保留在事务内,不能简单“提前读”
对多集合关联读,若只有一部分参与事务逻辑,拆成“事务外预读 + 事务内局部验证”
示例(Node.js):
独响
一个轻笔记+角色扮演的app
下载
// ✅ 正确:配置类读提前完成
const config = await configs.findOne({ key: "fee_rate" });
const user = await users.findOne({ _id: userId }); // 用户基础信息,不参与状态变更
// 事务只做原子写
await session.withTransaction(async () => {
const order = await orders.findOne({ _id: orderId }, { session });
if (order.status !== "created") throw new Error("invalid status");
await orders.updateOne(
{ _id: orderId },
{ $set: { status: "paid", fee: config.rate * order.amount } },
{ session }
);
});
容易被忽略的隐式读操作
有些读不是显式
find
,但一样进事务快照:
collection.bulkWrite()
中的
upsert: true
会隐式执行一次查询判断是否存在
collection.replaceOne()
默认带
upsert: false
,但如果设为
true
,就等价于“读+写”
使用
$expr
的更新条件(如
{ $gt: ["$updatedAt", "$createdAt"] }
)需要读取文档字段,也走快照
这类操作一旦出现在事务里,又没实际业务必要,就是性能黑洞。检查
mongod
日志里的
transaction
段落,留意
numReads
字段是否异常高。
事务不是万能隔离罩,它只解决“写-写冲突”和有限的“读-写一致性”,别把它当读操作的保险柜。
