本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
事务
Amazon DocumentDB(与 MongoDB 兼容)现在支持 MongoDB 4.0,包括事务性。您可以跨多个文档、语句、集合和数据库执行事务。事务使您能够对 Amazon DocumentDB 集群中的一个或多个文档执行原子、一致、隔离和持久 (ACID) 操作,从而简化了应用程序开发。交易的常见用例包括财务处理、履行和管理订单以及构建多人游戏。
事务不会产生额外费用。您只需为作为交易一部分使用的读写操作系统付费。
要求
要使用事务功能,您需要满足以下要求:
-
你必须使用Amazon DocumentDB 4.0 引擎。
-
您必须使用与 MongoDB 4.0 或更高版本兼容的驱动程序。
最佳实践
以下是一些最佳实践,可让您充分利用 Amazon DocumentDB 的交易。
-
务必在事务完成后提交或中止事务。让事务处于不完整状态会占用数据库资源,并可能导致写入冲突。
-
建议将事务保持在所需的最小数量的命令范围内。如果您的交易包含多个语句,这些语句可以分成多个较小的交易,则建议这样做以减少超时的可能性。始终以创建短期交易为目标,而不是长时间读取。
限制
-
Amazon DocumentDB 不支持事务中的游标。
-
Amazon DocumentDB 无法在交易中创建新馆藏,也无法对不存在的集合进行查询/更新。
-
文档级写入锁有 1 分钟的超时限制,用户无法配置该超时。
-
Amazon DocumentDB 不支持可重试写入、可重试提交和可重试中止命令。例外:如果您使用的是 mongo shell,请不要在任何代码字符串中包含该
retryWrites=false
命令。可重试写入数据。包括retryWrites=false
可能会导致正常读取命令失败。 -
每个 Amazon DocumentDB 实例对一次在该实例上打开的并发事务数量都有上限。有关限制,请参阅实例限制。
-
对于给定事务,事务日志大小必须小于 32MB。
-
Amazon DocumentDB 确实支持事务
count()
内部,但并非所有驱动程序都支持此功能。另一种方法是使用countDocuments()
API,它将计数查询转换为客户端的聚合查询。 -
交易的执行限制为一分钟,会话的超时时间为 30 分钟。如果事务超时,它将被中止,并且在会话中为现有事务发出的任何后续命令都将产生以下错误:
WriteCommandError({ "ok" : 0, "operationTime" : Timestamp(1603491424, 627726), "code" : 251, "errmsg" : "Given transaction number 0 does not match any in-progress transactions." })
监控和诊断
借助 Amazon DocumentDB 4.0 中对事务的支持,添加了其他 CloudWatch 指标来帮助您监控交易。
新 CloudWatch 指标
-
DatabaseTransactions
:一分钟内进行的未平仓交易数量。 -
DatabaseTransactionsAborted
:一分钟内中止的交易数量。 -
DatabaseTransactionsMax
:一分钟内的最大未平仓交易数。 -
TransactionsAborted
:一分钟内实例中止的事务数量。 -
TransactionsCommitted
:一分钟内在实例上提交的交易数量。 -
TransactionsOpen
:一分钟内在实例上打开的交易数量。 -
TransactionsOpenMax
:一分钟内在实例上打开的最大交易数量。 -
TransactionsStarted
:一分钟内在实例上启动的事务数量。
注意
有关 Amazon DocumentDB 的更多 CloudWatch 指标,请访问使用以下方式监视Amazon DocumentDB CloudWatch。
此外,在、和中都currentOp
lsid
添加了新字段transactionThreadId
,并为 “idle transaction
” 和serverStatus
交易添加了新状态:currentActive
currentInactive
、currentOpen
、totalAborted
、totalCommitted
、和totalStarted
。
事务隔离级别
启动事务时,您可以同时指定readConcern
和,writeConcern
如下例所示:
mySession.startTransaction({readConcern: {level: 'snapshot'}, writeConcern: {w: 'majority'}});
对于readConcern
,Amazon DocumentDB 默认支持快照隔离。如果指定readConcern
了本地、可用或多数,Amazon DocumentDB 会将该readConcern
级别升级为快照。Amazon DocumentDB 不支持线性化readConcern
参数,指定这样的读取问题会导致错误。
对于writeConcern
,Amazon DocumentDB 默认支持多数,当四个数据副本保存在三个可用区中时,即达到写入法定人数。如果指定了更低的值writeConcern
,Amazon DocumentDB 会将升级writeConcern
为多数。此外,所有 Amazon DocumentDB 写入操作都会被记录下来,并且无法禁用日记功能。
使用案例
在本节中,我们将介绍两个交易用例:多语句和多重收款。
多账单交易
Amazon DocumentDB 事务是多语句的,这意味着您可以通过显式提交或回滚来编写跨多个语句的交易。您可以将insert
、update
delete
、和findAndModify
操作分组为单个原子操作。
多语句交易的常见用例是借记信贷交易。例如:你欠朋友钱买衣服。因此,您需要从您的账户中扣除(提取)500美元,并将500美元(存款)存入朋友的账户。要执行该操作,您需要在单笔交易中同时执行债务和信贷操作,以确保原子性。这样做可以防止从您的账户中扣除500美元但未存入朋友账户的情况。以下是这个用例的样子:
// *** Transfer $500 from Alice to Bob inside a transaction: Success Scenario*** // Setup bank account for Alice and Bob. Each have $1000 in their account var databaseName = "bank"; var collectionName = "account"; var amountToTransfer = 500; var session = db.getMongo().startSession({causalConsistency: false}); var bankDB = session.getDatabase(databaseName); var accountColl = bankDB[collectionName]; accountColl.drop(); accountColl.insert({name: "Alice", balance: 1000}); accountColl.insert({name: "Bob", balance: 1000}); session.startTransaction(); // deduct $500 from Alice's account var aliceBalance = accountColl.find({"name": "Alice"}).next().balance; var newAliceBalance = aliceBalance - amountToTransfer; accountColl.update({"name": "Alice"},{"$set": {"balance": newAliceBalance}}); var findAliceBalance = accountColl.find({"name": "Alice"}).next().balance; // add $500 to Bob's account var bobBalance = accountColl.find({"name": "Bob"}).next().balance; var newBobBalance = bobBalance + amountToTransfer; accountColl.update({"name": "Bob"},{"$set": {"balance": newBobBalance}}); var findBobBalance = accountColl.find({"name": "Bob"}).next().balance; session.commitTransaction(); accountColl.find(); // *** Transfer $500 from Alice to Bob inside a transaction: Failure Scenario*** // Setup bank account for Alice and Bob. Each have $1000 in their account var databaseName = "bank"; var collectionName = "account"; var amountToTransfer = 500; var session = db.getMongo().startSession({causalConsistency: false}); var bankDB = session.getDatabase(databaseName); var accountColl = bankDB[collectionName]; accountColl.drop(); accountColl.insert({name: "Alice", balance: 1000}); accountColl.insert({name: "Bob", balance: 1000}); session.startTransaction(); // deduct $500 from Alice's account var aliceBalance = accountColl.find({"name": "Alice"}).next().balance; var newAliceBalance = aliceBalance - amountToTransfer; accountColl.update({"name": "Alice"},{"$set": {"balance": newAliceBalance}}); var findAliceBalance = accountColl.find({"name": "Alice"}).next().balance; session.abortTransaction();
多重集合交易
我们的交易也是多重收款,这意味着它们可用于在单笔交易和多个集合中执行多项操作。这提供了一致的数据视图并维护了数据的完整性。当你将命令作为一个单一提交时<>
,事务就是 all-or-nothing 执行——也就是说,它们要么全部成功,要么全部失败。
以下是多重集合交易的示例,使用了多语句交易示例中的相同场景和数据。
// *** Transfer $500 from Alice to Bob inside a transaction: Success Scenario*** // Setup bank account for Alice and Bob. Each have $1000 in their account var amountToTransfer = 500; var collectionName = "account"; var session = db.getMongo().startSession({causalConsistency: false}); var accountCollInBankA = session.getDatabase("bankA")[collectionName]; var accountCollInBankB = session.getDatabase("bankB")[collectionName]; accountCollInBankA.drop(); accountCollInBankB.drop(); accountCollInBankA.insert({name: "Alice", balance: 1000}); accountCollInBankB.insert({name: "Bob", balance: 1000}); session.startTransaction(); // deduct $500 from Alice's account var aliceBalance = accountCollInBankA.find({"name": "Alice"}).next().balance; var newAliceBalance = aliceBalance - amountToTransfer; accountCollInBankA.update({"name": "Alice"},{"$set": {"balance": newAliceBalance}}); var findAliceBalance = accountCollInBankA.find({"name": "Alice"}).next().balance; // add $500 to Bob's account var bobBalance = accountCollInBankB.find({"name": "Bob"}).next().balance; var newBobBalance = bobBalance + amountToTransfer; accountCollInBankB.update({"name": "Bob"},{"$set": {"balance": newBobBalance}}); var findBobBalance = accountCollInBankB.find({"name": "Bob"}).next().balance; session.commitTransaction(); accountCollInBankA.find(); // Alice holds $500 in bankA accountCollInBankB.find(); // Bob holds $1500 in bankB // *** Transfer $500 from Alice to Bob inside a transaction: Failure Scenario*** // Setup bank account for Alice and Bob. Each have $1000 in their account var collectionName = "account"; var amountToTransfer = 500; var session = db.getMongo().startSession({causalConsistency: false}); var accountCollInBankA = session.getDatabase("bankA")[collectionName]; var accountCollInBankB = session.getDatabase("bankB")[collectionName]; accountCollInBankA.drop(); accountCollInBankB.drop(); accountCollInBankA.insert({name: "Alice", balance: 1000}); accountCollInBankB.insert({name: "Bob", balance: 1000}); session.startTransaction(); // deduct $500 from Alice's account var aliceBalance = accountCollInBankA.find({"name": "Alice"}).next().balance; var newAliceBalance = aliceBalance - amountToTransfer; accountCollInBankA.update({"name": "Alice"},{"$set": {"balance": newAliceBalance}}); var findAliceBalance = accountCollInBankA.find({"name": "Alice"}).next().balance; // add $500 to Bob's account var bobBalance = accountCollInBankB.find({"name": "Bob"}).next().balance; var newBobBalance = bobBalance + amountToTransfer; accountCollInBankB.update({"name": "Bob"},{"$set": {"balance": newBobBalance}}); var findBobBalance = accountCollInBankB.find({"name": "Bob"}).next().balance; session.abortTransaction(); accountCollInBankA.find(); // Alice holds $1000 in bankA accountCollInBankB.find(); // Bob holds $1000 in bankB
回调 API 的交易 API 示例
回调 API 仅适用于 4.2 以上的驱动程序。
核心 API 的交易 API 示例
支持的 命令
命令 | 支持 |
---|---|
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
否 |
|
否 |
|
是 |
不支持的功能
方法 | 阶段或命令 |
---|---|
|
|
|
|
|
|
会话
MongoDB 会话是一个框架,用于支持可重试写入、因果一致性、事务和管理跨数据库的操作。创建会话时,客户端会生成逻辑会话标识符 (lsid),用于在向服务器发送命令时标记该会话中的所有操作。
Amazon DocumentDB 支持使用会话来启用事务,但不支持因果一致性或可重试写入。
在 Amazon DocumentDB 中使用事务时,将使用session.startTransaction()
API 从会话中启动交易,并且会话一次支持单个事务。同样,事务是使用 commit (session.commitTransaction()
) 或 abort (session.abortTransaction()
) API 完成的。
因果一致性
因果一致性保证了在单个客户端会话中,客户端将遵守 read-after-write 一致性,单原子读取/写入将遵循读取,这些保证适用于集群中的所有实例,而不仅仅是主实例。Amazon DocumentDB 不支持因果一致性,以下语句将导致错误。
var mySession = db.getMongo().startSession(); var mySessionObject = mySession.getDatabase('test').getCollection('account'); mySessionObject.updateOne({"_id": 2}, {"$inc": {"balance": 400}}); //Result:{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 } mySessionObject.find() //Error: error: { // "ok" : 0, // "code" : 303, // "errmsg" : "Feature not supported: 'causal consistency'", // "operationTime" : Timestamp(1603461817, 493214) //} mySession.endSession()
您可以在会话中禁用因果一致性。请注意,这样做将使您能够使用会话框架,但不能为读取提供因果一致性保证。使用 Amazon DocumentDB 时,来自主实例的读取将 read-after-write 保持一致,而来自副本实例的读取最终将保持一致。事务是利用会话的主要用例。
var mySession = db.getMongo().startSession({causalConsistency: false}); var mySessionObject = mySession.getDatabase('test').getCollection('account'); mySessionObject.updateOne({"_id": 2}, {"$inc": {"balance": 400}}); //Result:{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 } mySessionObject.find() //{ "_id" : 1, "name" : "Bob", "balance" : 100 } //{ "_id" : 2, "name" : "Alice", "balance" : 1700 }
可重试写入
可重试写入是一种功能,当出现网络错误或客户端找不到主写入操作时,客户端将尝试重试一次写入操作。在 Amazon DocumentDB 中,不支持可重试写入,必须将其禁用。您可以使用连接字符串中的命令 (retryWrites=false
) 将其禁用。
例外:如果您使用的是 mongo shell,请不要在任何代码字符串中包含该retryWrites=false
命令。可重试写入数据。包括retryWrites=false
可能会导致正常读取命令失败。
事务性错误
在使用交易时,在某些情况下可能会出现错误,指出交易号与任何正在进行的交易都不匹配。
该错误可能至少在两种不同的场景中产生:
- After the one-minute transaction timeout.
- After an instance restart (due to patching, crash recovery, etc.), it is possible to receive this error even in cases where the transaction successfully committed. During an instance restart, the database can't tell the difference between a transaction that successfully completed versus a transaction that aborted. In other words, the transaction completion state is ambiguous.
处理此错误的最佳方法是使事务更新具有幂等性,例如,使用$set
变异器而不是递增/递减运算。见下文:
{ "ok" : 0, "operationTime" : Timestamp(1603938167, 1), "code" : 251, "errmsg" : "Given transaction number 1 does not match any in-progress transactions." }