TransientTransactionError,用户直接看到操作失败的错误提示。w:majority,主从切换后已确认成功的写入丢失。场景 | 推荐方案 | 说明 |
更新单个文档的多个字段 | 不需要事务 | 单文档操作天然原子 |
订单 + 订单项 | 不需要事务 | 嵌入式设计,单文档操作 |
转账(A 扣款 + B 入账) | 需要事务 | 必须跨文档原子 |
订单 + 库存扣减 | 需要事务 | 跨集合关联操作 |
批量插入独立文档 | 不需要事务 | 各文档独立,无需原子 |
// 错误设计:订单和订单项分两个集合,必须用事务保证一致性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 }]});
withTransaction 回调 API 编写事务逻辑,自动处理瞬态错误重试。TransientTransactionError。若应用层未实现重试逻辑,用户会直接看到操作失败的错误提示。withTransaction 回调 API 内置了瞬态错误自动重试机制,可在用户无感知的情况下自动恢复。TransientTransactionError。withTransaction 回调 API,默认整体重试时间约120秒,下单成功率提升至99.5%。withTransaction 实现自动重试的转账事务。from pymongo import MongoClientfrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernimport logginglogger = 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.banktry: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
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();}}
// 错误:事务中包含耗时的外部调用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 是否命中,而不是 modifiedCountif (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 };});
场景 | 推荐方案 | 说明 |
转账(资金相关) | 多文档事务 | 必须强一致性 |
订单 + 通知 | 最终一致性 | 通知失败不影响订单 |
订单 + 积分 | 最终一致性 | 积分可异步补偿 |
订单 + 库存 | 事务或乐观锁 | 库存必须准确 |
majority,事务内使用 snapshot。local 可能读到未同步到多数节点的数据,在主从切换后这些数据可能丢失。majority 确保读取的数据已被多数节点确认,不会因故障切换而丢失。Read Concern | 说明 | 适用场景 |
`local` | 读取本地最新数据(默认) | 一般读取 |
`majority` | 读取已被多数节点确认的数据 | 需要读已提交数据 |
`snapshot` | 事务中的快照隔离读取 | 多文档事务 |
`linearizable` | 线性一致性读取 | 强一致性要求(性能较低) |
local Read Concern。local 可能读到未同步到多数节点的数据,主从切换后这些数据丢失,导致"写入成功但数据消失"的困惑。majority,读取的数据一定已持久化,不会因故障切换而丢失。// 默认: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%需关注 |
while (retries < maxRetries) {try {session.startTransaction();// ... 业务逻辑 ...session.commitTransaction();break; // 成功退出} catch (error) {session.abortTransaction();if (error.hasErrorLabel("TransientTransactionError") && retries < maxRetries) {retries++;continue; // 重试}throw error; // 非瞬态错误,抛出}}
// 1. 查看当前活跃事务db.currentOp({ "transaction": { $exists: true } })// 2. 检查事务执行时间// 关注 transaction.timeActiveMicros 字段// 超过 1 秒的事务需排查原因// 3. 监控事务冲突// 关注 serverStatus 中的 transactions 指标db.serverStatus().transactions
文档反馈