事务
Overview
阅读本指南,了解如何使用 Node.js 驱动程序在 MongoDB 中执行事务。 事务是一个工作单元,由一系列操作组成,您希望这些操作一起成功,或者在一个或多个操作失败时一起失败。 这种行为称为原子性。 原子性是一种属性,在该属性中,由一个或多个操作组成的事务同时发生,因此其他客户端无法将它们视为单独的操作,并且如果其中一个操作失败,则不会留下任何更改。
由于 MongoDB 中单个文档上的所有写入操作都是原子性的,因此当您必须进行修改多个文档的原子更改(称为多文档事务)时,您可能会从事务中受益最多。 与对单个文档的写入操作类似,多文档事务是ACID 兼容的,这意味着 MongoDB 可以保证您的事务操作中涉及的数据保持一致,即使遇到意外错误。 阅读这篇关于ACID 事务的 MongoDB 文章,了解更多信息。
您可以使用驱动程序执行多文档事务。
注意
要运行多文档事务,必须使用 MongoDB 4.0 或更高版本。
有关限制的详细列表,请参阅服务器手册中的事务和操作部分。
在 MongoDB 中,多文档事务在客户端会话中运行。 客户端会话是指要确保按顺序运行的一组相关读取或写入操作。 我们建议您将客户端重复用于多个会话和事务,而不是每次都实例化一个新客户端。
与多数读关注和写关注结合使用时,驱动程序可以保证操作之间的因果一致性。 有关详细信息,请参阅有关客户端会话和因果一致性保证的服务器手册指南。
有关如何使用驱动程序在 MongoDB 上运行多文档事务的更多信息,请参阅本指南的以下部分:
事务 API
使用Core API与驱动程序实现事务。 要使用 Core API,您需要声明事务的开始点和提交点。
Core API
Core API 提供启动、取消或提交事务的方法。 提交事务时,您将向服务器发送请求,以自动对操作进行更改。 使用此 API 时,您必须手动处理服务器返回的某些事务错误。
有关这些错误的更多信息,请参阅TransientTransactionError和UnknownTransactionCommitResult 。
要启动、取消或提交事务,可以对 Session
对象调用相应的方法:
startTransaction()
commitTransaction()
abortTransaction()
有关示例ACID 事务实施,请参阅Core API示例。
事务设置
实例化事务时,可以指定以下选项来设置该事务的默认行为:
设置 | 说明 |
---|---|
readConcern | 指定如何检查读取操作从副本集中检索的数据的一致性。 有关更多信息,请参阅服务器手册中的“读关注”。 |
writeConcern | 指定确认写入的条件。 有关更多信息,请参阅服务器手册中的写关注。 |
readPreference | 有关更多信息,请参阅服务器手册中的读取偏好。 |
maxCommitTimeMS | 指定允许对事务执行提交操作的最长时间(以毫秒为单位)。 |
如果不提供值,驱动程序将使用客户端设置。
您可以使用类似于以下内容的代码在 Core API 中指定事务选项:
const transactionOptions = { readPreference: 'primary', readConcern: { level: 'local' }, writeConcern: { w: 'majority' }, maxCommitTimeMS: 1000 }; session.startTransaction(transactionOptions);
示例
考虑客户从您的在线商店购买商品的场景。 要记录购买,您的应用程序需要更新与库存、客户订单相关的信息,并注册订单详细信息。 假设您按如下方式组织数据更新:
Collection | 操作 | 变更说明 |
---|---|---|
orders | insert | 记录购买信息 |
customers | update | 附加订单 ID 以将其与客户关联 |
inventory | update | 减去订购商品的数量 |
购买失败可能有多种原因,例如库存中的商品数量不足、订单无法完成或您的支付系统离线。
如果付款失败,您可以使用事务来确保避免暴露任何部分更新,这些更新可能会导致依赖于该数据的其他操作出现数据一致性问题。
样本数据
代码示例需要testdb
数据库中的以下样本数据来运行多文档付款事务:
customers
集合中的文档,描述客户及其订单。在
inventory
collection中的文档,每个都追踪商品的数量和描述。
本示例中customers
collection中的文档包含以下内容:
{ _id: 98765, orders: [] }
本示例中inventory
集合中的文档包含以下内容:
[ { name: "sunblock", sku: 5432, qty: 85 }, { name: "beach towel", sku: 7865, qty: 41 } ]
orders
代码示例还会对collection执行操作,但不需要任何事先的示例文档。
代码示例使用cart
和payment
变量来表示所购商品的样本列表和订单付款详情,如下所示:
const cart = [ { name: 'sunblock', sku: 5432, qty: 1, price: 5.19 }, { name: 'beach towel', sku: 7865, qty: 2, price: 15.99 } ]; const payment = { customer: 98765, total: 37.17 };
重要
以下部分中的示例要求您在ACID 事务之外创建集合,或者使用MongoDB 4.4或更高版本。 有关在ACID 事务中创建集合的更多信息,请参阅“在事务中创建集合和索引”服务器指南。
Core API 实施
本节中的代码示例演示了如何使用 Core API 在会话中运行多文档付款事务。 此函数向您展示如何执行以下操作:
启动会话
启动事务,指定事务选项
在同一会话中执行数据操作
提交事务,或在驱动程序遇到错误时取消事务
结束会话
1 async function placeOrder(client, cart, payment) { 2 const transactionOptions = { 3 readConcern: { level: 'snapshot' }, 4 writeConcern: { w: 'majority' }, 5 readPreference: 'primary' 6 }; 7 8 const session = client.startSession(); 9 try { 10 session.startTransaction(transactionOptions); 11 12 const ordersCollection = client.db('testdb').collection('orders'); 13 const orderResult = await ordersCollection.insertOne( 14 { 15 customer: payment.customer, 16 items: cart, 17 total: payment.total, 18 }, 19 { session } 20 ); 21 22 const inventoryCollection = client.db('testdb').collection('inventory'); 23 for (let i=0; i<cart.length; i++) { 24 const item = cart[i]; 25 26 // Cancel the transaction when you have insufficient inventory 27 const checkInventory = await inventoryCollection.findOne( 28 { 29 sku: item.sku, 30 qty: { $gte: item.qty } 31 }, 32 { session } 33 ) 34 if (checkInventory === null) { 35 throw new Error('Insufficient quantity or SKU not found.'); 36 } 37 38 await inventoryCollection.updateOne( 39 { sku: item.sku }, 40 { $inc: { 'qty': -item.qty }}, 41 { session } 42 ); 43 } 44 45 const customerCollection = client.db('testdb').collection('customers'); 46 await customerCollection.updateOne( 47 { _id: payment.customer }, 48 { $push: { orders: orderResult.insertedId }}, 49 { session } 50 ); 51 await session.commitTransaction(); 52 console.log('Transaction successfully committed.'); 53 54 } catch (error) { 55 if (error instanceof MongoError && error.hasErrorLabel('UnknownTransactionCommitResult')) { 56 // add your logic to retry or handle the error 57 } 58 else if (error instanceof MongoError && error.hasErrorLabel('TransientTransactionError')) { 59 // add your logic to retry or handle the error 60 } else { 61 console.log('An error occured in the transaction, performing a data rollback:' + error); 62 } 63 await session.abortTransaction(); 64 } finally { 65 await session.endSession(); 66 } 67 }
请注意,您必须将会话对象传递给要在该会话上运行的每个 CRUD 操作。
catch
区块中的代码和注释演示了如何识别服务器事务错误以及在何处放置处理这些错误的逻辑。 确保在代码中包含驱动程序中的MongoError
类型,如以下示例导入声明所示:
const { MongoError, MongoClient } = require('mongodb');
请参阅“付款事务结果”部分,了解运行ACID 事务后集合应包含的内容。
支付事务结果
如果应用程序完成了支付事务,则数据库应包含所有更新;如果异常中断了事务,则数据库中不应存在任何更改。
customers
collection应包含客户文档,其字段附加有订单 ID:
{ "_id": 98765, "orders": [ "61dc..." ] }
inventory
集合应包含“sunblock”和“beach towel”商品的更新数量:
[ { "_id": ..., "name": "sunblock", "sku": 5432, "qty": 84 }, { "_id": ..., "name": "beach towel", "sku": 7865, "qty": 39 } ]
orders
collection 应包含订单和付款信息:
[ { "_id": "...", "customer": 98765, "items": [ { "name": "sunblock", "sku": 5432, "qty": 1, "price": 5.19 }, { "name": "beach towel", "sku": 7865, "qty": 2, "price": 15.99 } ], "total": 37.17 } ]