文档中心>云数据库 MongoDB>开发规范>事务与数据的一致性

事务与数据的一致性

最近更新时间:2026-05-25 09:51:30

我的收藏

操作场景

MongoDB 从4.0版本开始支持多文档事务,为需要跨文档保证原子性的场景提供了解决方案。但在实际生产环境中,由于事务使用不当,往往会引发严重的可用性问题,例如:
事务过度使用导致性能骤降:为单文档操作添加不必要的事务包装,WiredTiger Cache 被快照数据占满,系统吞吐量下降40% ~ 50%。
长事务引发死锁与超时:事务中包含外部 API 调用等耗时操作,锁资源长时间占用,导致后续事务排队等待甚至超时回滚。
高并发事务冲突剧增:缺乏冲突处理机制,写冲突导致大量 TransientTransactionError,用户直接看到操作失败的错误提示。
Write Concern 配置不当导致数据丢失:核心业务未使用 w:majority,主从切换后已确认成功的写入丢失。
本文档旨在:提供标准的事务使用与数据一致性配置规范,帮助开发者正确使用事务,避免过度依赖事务带来的性能问题。

事务使用决策

规范一:优先利用单文档原子性

核心动作:通过合理的文档模型设计,将需要原子操作的数据放在同一文档中,而非依赖多文档事务。
MongoDB 对单个文档的所有操作(包括多字段更新、嵌入文档修改、数组操作)天然具备原子性,无需显式事务。通过嵌入式文档设计,可将原本需要跨集合事务保证一致性的操作,转化为单文档的原子操作,避免事务的性能开销。
场景
推荐方案
说明
更新单个文档的多个字段
不需要事务
单文档操作天然原子
订单 + 订单项
不需要事务
嵌入式设计,单文档操作
转账(A 扣款 + B 入账)
需要事务
必须跨文档原子
订单 + 库存扣减
需要事务
跨集合关联操作
批量插入独立文档
不需要事务
各文档独立,无需原子
业务应用:某电商系统将订单和订单项分为两个集合存储(关系型思维),创建订单时需要事务保证两个集合的一致性。
问题表现:高峰期每秒数千笔订单同时开启事务,WiredTiger Cache 被快照数据占满,触发 eviction 风暴,系统吞吐量骤降80%。
优化操作:重新设计为嵌入式文档(订单项嵌入订单),单文档操作天然原子,无需事务,性能恢复正常。
单文档原子性示例:以下示例演示如何通过嵌入式文档设计避免使用多文档事务。
// 错误设计:订单和订单项分两个集合,必须用事务保证一致性
const session = client.startSession();
session.startTransaction();
try {
await db.t_orders.insertOne(
{ orderId: "ORD001", userId: "U001", total: 299 },
{ session }
);
await db.t_order_items.insertMany([
{ orderId: "ORD001", productId: "P001", quantity: 1, price: 199 },
{ orderId: "ORD001", productId: "P002", quantity: 1, price: 100 }
], { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}

// 正确设计:嵌入式文档,单文档操作天然原子,无需事务
await db.t_orders.insertOne({
orderId: "ORD001",
userId: "U001",
total: 299,
items: [
{ productId: "P001", quantity: 1, price: 199 },
{ productId: "P002", quantity: 1, price: 100 }
]
});

规范二:仅在必要时使用事务

核心动作:只有在跨文档、跨集合且必须保证原子性时才使用事务,禁止为单文档操作添加事务包装。
事务的性能开销来自三个方面:快照维护占用 WiredTiger Cache、锁等待增加延迟、事务协调消耗 CPU。不必要的事务包装会使系统整体吞吐量下降,且增加死锁风险。
业务应用:某系统开发人员为了"保险起见",给所有数据库操作都加上事务包装,即使是单文档更新也使用事务。
问题表现:系统整体延迟增加50%,并发能力下降40%,高峰期频繁出现事务超时错误。
优化操作:清理不必要的事务包装,仅保留跨文档操作的事务,性能恢复正常。

事务编写规范

规范三:使用官方推荐的事务回调 API

核心动作:使用 withTransaction 回调 API 编写事务逻辑,自动处理瞬态错误重试。
MongoDB 事务在高并发场景下可能因写冲突触发 TransientTransactionError。若应用层未实现重试逻辑,用户会直接看到操作失败的错误提示。withTransaction 回调 API 内置了瞬态错误自动重试机制,可在用户无感知的情况下自动恢复。
业务应用:库存扣减场景中,多个请求同时争抢同一商品库存,频繁触发写冲突导致 TransientTransactionError
问题表现:应用层未实现重试逻辑,用户看到"下单失败"错误提示,下单成功率仅85%。
优化操作:使用 withTransaction 回调 API,默认整体重试时间约120秒,下单成功率提升至99.5%。
Python 事务回调示例:以下示例演示如何使用 withTransaction 实现自动重试的转账事务。
from pymongo import MongoClient
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern
import logging

logger = logging.getLogger(__name__)


def transfer_funds(db, session, from_account, to_account, amount):
"""转账业务逻辑"""
accounts = db.accounts

# 扣款(CAS 校验余额)
result = accounts.update_one(
{"_id": from_account, "balance": {"$gte": amount}},
{"$inc": {"balance": -amount}},
session=session
)
if result.matched_count != 1:
# 区分账户不存在与余额不足
account = accounts.find_one({"_id": from_account}, session=session)
if not account:
raise Exception(f"扣款账户不存在: {from_account}")
raise Exception(
f"余额不足: {from_account}, 当前 {account['balance']}, 需扣 {amount}"
)

# 入账(必须校验目标账户存在)
result = accounts.update_one(
{"_id": to_account},
{"$inc": {"balance": amount}},
session=session
)
if result.matched_count != 1:
raise Exception(f"入账账户不存在: {to_account}")


# 使用 with_transaction 回调 API(自动重试 TransientTransactionError)
client = MongoClient("mongodb://...")
db = client.bank

try:
with client.start_session() as session:
session.with_transaction(
lambda s: transfer_funds(db, s, "A001", "A002", 100),
read_concern=ReadConcern(level="snapshot"),
write_concern=WriteConcern(w="majority", j=True)
)
logger.info("转账成功: A001 -> A002, 金额 100")
except Exception as e:
logger.error(f"转账失败: {e}")
raise
Node.js 事务回调示例:以下示例演示 Node.js 驱动中的事务回调用法。
const { MongoClient } = require('mongodb');

async function transferFunds(client, fromAccount, toAccount, amount) {
const session = client.startSession();

try {
// 使用 withTransaction 回调 API(自动重试 TransientTransactionError)
await session.withTransaction(async () => {
const accounts = client.db('bank').collection('accounts');

// 扣款(CAS 校验余额)
const debitResult = await accounts.updateOne(
{ _id: fromAccount, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);

if (debitResult.matchedCount !== 1) {
// 区分"账户不存在"与"余额不足"
const account = await accounts.findOne(
{ _id: fromAccount },
{ session }
);
if (!account) {
throw new Error(`扣款账户不存在: ${fromAccount}`);
}
throw new Error(
`余额不足: ${fromAccount}, 当前 ${account.balance}, 需扣 ${amount}`
);
}

// 入账(必须校验目标账户存在,否则钱会消失)
const creditResult = await accounts.updateOne(
{ _id: toAccount },
{ $inc: { balance: amount } },
{ session }
);

if (creditResult.matchedCount !== 1) {
throw new Error(`入账账户不存在: ${toAccount}`);
}

}, {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority', j: true }
});

console.log(`转账成功: ${fromAccount} -> ${toAccount}, 金额 ${amount}`);
} finally {
await session.endSession();
}
}

规范四:事务执行时间控制在秒级以内

核心动作:事务执行时间控制在秒级以内,禁止在事务中包含外部 API 调用、文件读写等耗时操作。
长事务会持续占用锁和快照资源。事务持续时间越长,与其他事务冲突的概率越高,且占用的 WiredTiger Cache 快照无法释放,导致后续事务排队等待。MongoDB 默认事务超时时间为60秒,超时后事务自动回滚。
业务应用:某支付系统在事务中调用外部风控 API 验证交易合法性,网络延迟导致事务持续数秒。
问题表现:长事务占用锁和快照资源,其他事务排队等待,系统吞吐量骤降。
优化操作:将风控调用移到事务外部,事务时间从3秒降至100ms,系统恢复稳定。
事务精简示例:以下示例演示如何将耗时操作移出事务,缩短事务执行时间。
// 错误:事务中包含耗时的外部调用
session.startTransaction();
await collection.updateOne({...}, {...}, { session });
const result = await callExternalAPI(); // 外部调用,延迟不可控
await collection.updateOne({...}, {...}, { session });
session.commitTransaction();
// 正确:外部调用放在事务外部
const externalResult = await callExternalAPI(); // 先完成外部调用
session.startTransaction();
await collection.updateOne({...}, {...}, { session });
await collection.updateOne({...}, {...}, { session });
session.commitTransaction();

事务替代方案

规范五:使用乐观锁替代事务

核心动作:对于"读取 → 修改 → 写入"场景,可使用版本号实现乐观锁,替代多文档事务。
乐观锁的核心思路是:读取时记录文档版本号,更新时将版本号作为查询条件,若版本号已变化则说明数据被其他请求修改,更新失败后重试。相比事务,乐观锁不占用快照和锁资源,数据库压力更低,吞吐量更高。
业务应用:库存扣减场景中,使用事务保证原子性但冲突率高。
问题表现:高并发下事务频繁冲突,重试成本高,数据库快照压力大。
优化操作:改用乐观锁,事务冲突变为应用层重试,数据库压力大幅降低。
乐观锁实现示例:以下示例演示基于版本号的乐观锁实现方法。
// 乐观锁实现
async function updateWithOptimisticLock(collection, id, updateFn, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
// 读取当前文档和版本号
const doc = await collection.findOne({ _id: id });
if (!doc) {
throw new Error(`文档不存在: ${id}`);
}
const currentVersion = doc.version || 0;

// 应用业务逻辑(业务异常直接外抛,不重试)
const updates = updateFn(doc);

// 带版本号条件更新
const result = await collection.updateOne(
{ _id: id, version: currentVersion },
{
$set: updates,
$inc: { version: 1 }
}
);

// 用 matchedCount 判断 CAS 是否命中,而不是 modifiedCount
if (result.matchedCount === 1) {
return true; // 更新成功
}

// 版本冲突,指数退避 + 随机抖动后重试
const backoff = Math.random() * Math.pow(2, i) * 10;
await new Promise(resolve => setTimeout(resolve, backoff));
}
throw new Error(`乐观锁更新失败,超过重试次数上限: ${maxRetries}`);
}

// 使用示例:库存扣减
await updateWithOptimisticLock(
db.t_products,
"P001",
(product) => {
if (product.stock < 1) throw new Error('库存不足');
return { stock: product.stock - 1 };
}
);

规范六:非核心场景使用最终一致性方案

核心动作:非核心场景可使用消息队列 + 补偿机制实现最终一致性,避免强一致事务的性能开销。
强一致事务要求所有操作要么全部成功、要么全部回滚。当事务包含多个非核心操作(如发送通知、记录日志、更新统计)时,任一环节失败都会导致整个事务回滚,降低核心操作的成功率。最终一致性方案将核心操作与非核心操作解耦,核心操作成功后异步处理其他操作,失败时重试或补偿。
场景
推荐方案
说明
转账(资金相关)
多文档事务
必须强一致性
订单 + 通知
最终一致性
通知失败不影响订单
订单 + 积分
最终一致性
积分可异步补偿
订单 + 库存
事务或乐观锁
库存必须准确
业务应用:订单创建后需要发送通知、记录日志、更新统计等操作。
问题表现:若用事务保证强一致性,任一环节失败都导致订单创建失败,订单创建成功率仅95%。
优化操作:改为最终一致性方案,订单创建成功后发送消息,消费者异步处理其他操作,失败时重试或补偿。订单创建成功率提升至99.9%。

Read Concern 配置

规范七:根据场景选择 Read Concern

核心动作:需要读已提交数据时使用 majority,事务内使用 snapshot
MongoDB 的 Read Concern 决定读取数据的一致性级别。默认的 local 可能读到未同步到多数节点的数据,在主从切换后这些数据可能丢失。majority 确保读取的数据已被多数节点确认,不会因故障切换而丢失。
Read Concern
说明
适用场景
`local`
读取本地最新数据(默认)
一般读取
`majority`
读取已被多数节点确认的数据
需要读已提交数据
`snapshot`
事务中的快照隔离读取
多文档事务
`linearizable`
线性一致性读取
强一致性要求(性能较低)
业务应用:某系统在写入后立即读取验证,使用默认的 local Read Concern。
问题表现local 可能读到未同步到多数节点的数据,主从切换后这些数据丢失,导致"写入成功但数据消失"的困惑。
优化操作:改用 majority,读取的数据一定已持久化,不会因故障切换而丢失。
Read Concern 配置示例:以下示例演示不同 Read Concern 级别的配置方法。
// 默认:local(可能读到未持久化的数据)
db.t_orders.find({ orderId: "ORD001" })
// 推荐:majority(读取已持久化的数据)
db.t_orders.find({ orderId: "ORD001" }).readConcern("majority")
// 事务内:snapshot(快照隔离)
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
// 连接串配置方式(全局生效)
// mongodb://user:pwd@host1,host2,host3/db?readConcernLevel=majority

事务检查清单

上线前必查

检查项
验证方法
通过标准
是否真的需要事务
分析业务逻辑
单文档能解决的不用事务
事务时间是否足够短
监控事务执行时间
< 1秒
是否有外部调用
代码审查
事务中无网络调用
是否使用回调 API
代码审查
使用 `withTransaction`
是否配置了重试
代码审查
处理 `TransientTransactionError`
Read Concern 是否合理
检查配置
事务内使用 `snapshot`
Write Concern 是否合理
检查配置
核心业务使用 `majority`

定期检查

检查项
验证方法
操作建议
事务执行时间
监控面板
P99超过1秒的事务需排查
事务冲突率
监控 `TransientTransactionError` 频次
冲突率 > 5%需优化
WiredTiger Cache 使用率
`serverStatus().wiredTiger.cache`
使用率 > 80%需关注

常见问题

Q1:什么时候必须用事务?

:仅在以下场景使用多文档事务:
1. 跨文档的资金操作(如转账的扣款和入账)。
2. 跨集合的关联操作(如创建订单同时扣减库存)。
3. 需要原子读写多个文档的批量操作。
单文档内的多字段更新、嵌入文档修改、数组操作等均无需事务。

Q2:事务中遇到 TransientTransactionError 怎么办?

:使用 withTransaction 回调 API,或者监测 TransientTransactionError 错误码,并自动重试。 若仍需手动处理:
while (retries < maxRetries) {
try {
session.startTransaction();
// ... 业务逻辑 ...
session.commitTransaction();
break; // 成功退出
} catch (error) {
session.abortTransaction();
if (error.hasErrorLabel("TransientTransactionError") && retries < maxRetries) {
retries++;
continue; // 重试
}
throw error; // 非瞬态错误,抛出
}
}

Q3:如何监控事务的健康状况?

// 1. 查看当前活跃事务
db.currentOp({ "transaction": { $exists: true } })

// 2. 检查事务执行时间
// 关注 transaction.timeActiveMicros 字段
// 超过 1 秒的事务需排查原因

// 3. 监控事务冲突
// 关注 serverStatus 中的 transactions 指标
db.serverStatus().transactions