如何在 MongoDB 中使用事务

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

transaction 是数据库操作的序列,只有在交易中的每一个操作都正确执行时才会成功。 交易一直是关系数据库的重要特征多年,但直到最近一直不在文档导向数据库中。 文档导向数据库的性质 - 一个单一的文档可以是一个坚固的,嵌入式的结构,包含嵌入式文档和数组,而不是简单的值 - 简化了在单一的文档中存储相关数据。

然而,有些应用程序即使在以文档为导向的数据库中,也需要在一个操作中访问和修改多个文档,以保证完整性。MongoDB在数据库引擎版本 4.0 中引入了多文档ACID交易,以满足此类用例的需求。

前提条件

由于它们在 MongoDB 中实现的方式,交易只能在运行作为一个较大的集群的一部分的 MongoDB 实例上执行. 它可能是一个 sharded 数据库集群或复制集合. 如果您有一个现有的 sharded MongoDB 集群或 replica 集运行,可以用于测试目的,那么您可以转到下一节来了解 ACID 交易。

然而,设置一个适当的功能性复制集或分裂的 MongoDB 集群需要你至少有三个运行 MongoDB 实例,理想情况下在单独的服务器上运行。此外,本指南的示例都涉及使用一个单一节点作为复制集的一员运行。而不是你要通过提供多个服务器的工作并在每个服务器上配置 MongoDB 仅供你在本指南中使用其中一个,你可以将一个独立的 MongoDB 实例转换成一个单一节点的复制集,你可以使用它来练习运行交易。

本指南的第一步概述了如何做到这一点,因此为了完成本教程,您只需要以下几点:

  • 一个拥有sudo特权的常规非 root 用户和与 UFW 配置的防火墙的服务器,本教程是通过使用运行 Ubuntu 20.04 的服务器进行验证的,您可以通过遵循此(Ubuntu 20.04 的初始服务器安装教程)来准备您的服务器。
  • MongoDB 安装在您的服务器上。

了解 ACID 交易

一个 transaction 是一个数据库操作的集合(如阅读和写入)以序列顺序执行,以一切或没有的方式执行,这意味着运行这些操作的结果要存储在数据库中,并在交易之外可见于其他数据库客户端,所有单独的操作必须成功。

为了说明为什么交易对数据库系统至关重要,想象一下你在银行工作,你需要将资金从客户A转移到客户B。

如果这两项操作单独失败,而另一项失败,银行记录就会变得不一致. 要么客户B会从无处获得资金(如果客户A的账户余额没有减少),要么客户A会无缘无故失去资金(如果他们的余额减少,但客户B没有被认可)。

数据库交易的四个属性,确保这样复杂的操作可以安全可靠地执行,保证数据的有效性,尽管有错误或中断,被缩写为 _ACID:atomicity, consistency, isolation,和 durability. 如果数据库系统可以保证所有四个操作组合在一个交易中,它也可以保证数据库将保留在一个有效的状态,即使在执行中出现意外的错误。

  • ** 空想** 是指交易中的所有行动都作为单一的工作单位处理,要么全部执行,要么完全不执行。 前一例从一个账户借入的货币将加入另一个账户,突出了原子性原则。 请注意,在MongoDB中,单个文档中的更新(无论文档结构有多复杂和嵌入)总是_原子_,即使不用交易. 只有在你处理一个以上文件时,交易才会提供更强大的原子性保证.
  • ** 一致性 ** 表示对数据库的任何修改都必须遵守数据库的现有限制,否则整个交易会失败. 例如,如果其中一项操作违反了独特的索引或计划验证规则,MongoDB将中止交易。
  • ** 孤立** 是认为分离,同时运行的交易是相互隔离的,两者都不会影响对方的结果. 如果两个交易同时被执行,则隔离规则保证最终结果与一个又一个被执行的结果相同.
  • ** 耐久性** 保证一旦交易成功,客户端就可以确定数据是否得到正确坚持. 即使是硬件故障或电源中断之类的东西也不会使交易无效. (英语)

MongoDB 中的交易符合这些 ACID 原则,并且可以在需要一次更改多个文档的情况下可靠地使用。

步骤 1 — 将您的独立 MongoDB 实例转换为复制集

正如前面提到的,由于它们在MongoDB中实现的方式,交易只能在运行作为更大的集群的一部分的数据库上执行。

如果您已经配置了可用于运行交易的复制集或碎片集群,您可以跳过此步骤并在步骤 2 中使用该集群,如果不是,则此步骤概述了如何将独立的 MongoDB 实例转换为单节点复制集。

<$>[警告] 警告:一个单节点复制集类似于你在这个步骤中配置的复制集有用用于测试目的,但它不会适合用于生产环境的原因是,复制集旨在运行在多个分布式节点上,因为这有助于保持数据库高度可用:如果任何一个节点失败,则在集合中仍然会有其他客户可以连接到。

如果您想了解更多关于 MongoDB 复制的信息以及它所涉及的安全影响,我们强烈建议您查看我们关于 [如何在 Ubuntu 20.04 上配置 MongoDB 复制设置] 的教程。

要将独立的 MongoDB 实例转换为复制集,请先使用您偏好的文本编辑器打开 MongoDB 配置文件。

1sudo nano /etc/mongod.conf

尋找閱讀「#replication:」的部分,指向此檔案的底部:

1[label /etc/mongod.conf]
2. . .
3#replication:
4. . .

然后在这个行下面添加一个replSetName指令,然后添加一个名称,MongoDB 将用来识别复制组:

1[label /etc/mongod.conf]
2. . .
3replication:
4  replSetName: "rs0"
5. . .

在本示例中,replSetName指令的值为rs0,您可以提供您想要的任何名称,但使用描述性名称可能有帮助。

<$>[注意] 注意:当复制启用时,MongoDB还要求您配置一些身份验证手段,而不是密码验证,如 keyfile 身份验证或设置 x.509 证书。

而不是设置更高级的安全措施,为本教程的目的,在你的mongod.conf文件中禁用安全块是明智的。

1[label /etc/mongod.conf]
2. . .
3
4#security:
5#  authorization: enabled
6
7. . .

只要您只打算将此数据库用于实践交易或其他测试目的,则不会带来安全风险,但是,如果您打算在未来使用此MongoDB实例来存储任何敏感数据,请确保删除这些行以重新启用身份验证。

这些是您需要对该文件进行的唯一更改,所以您可以保存和关闭它. 如果您使用nano来编辑该文件,您可以通过按CTRL + XYENTER来完成。

接下来,重新启动mongod服务,实现新的配置更改:

1sudo systemctl restart mongod

重新启动服务后,打开 MongoDB 壳以连接到运行在您的服务器上的 MongoDB 实例:

1mongo

从 MongoDB 提示中运行以下 rs.initiate() 方法,将您的独立 MongoDB 实例转化为可用于测试的单节点复制集:

1rs.initiate()

如果该方法在输出中返回OK : 1,则意味着复制集已成功启动:

1[secondary_label Output]
2{
3. . .
4    "ok" : 1,
5. . .

假设是这样的情况,您的 MongoDB 壳提示将更改,表示该壳所连接的实例现在是 rs0 复制组的成员:

请注意,此示例提示反映了这个 MongoDB 实例是复制组的次要成员,这是可以预料的,因为通常在复制组启动的时间和其成员之一被推广成为主要成员的时间之间存在差距。

如果您在等待几分钟后要运行命令,甚至仅按ENTER,提示会更新以反映您已连接到复制组的主要成员:

您的独立的 MongoDB 实例现在作为一个单节点复制集运行,您可以使用它来测试交易. 现在请保持提示打开,因为您将在下一步使用 MongoDB 壳创建示例集合并插入一些样本数据。

步骤 2 – 准备样本数据

为了解释 MongoDB 中的交易是如何工作的,以及如何使用它们,此步骤概述了如何打开 MongoDB 壳以连接到您的复制组的主要节点。

如果您错过了前一个步骤,因为您有一个现有的 MongoDB 集群或复制集,请连接到您可以编写数据的任何节点:

1mongo

<$>[注] 注: 新连接时,MongoDB 壳将默认情况下自动连接到测试数据库,您可以安全地使用此数据库来实验 MongoDB 和 MongoDB 壳。

或者,您也可以切换到另一个数据库以运行本教程中提供的所有示例命令. 要切换到另一个数据库,请运行使用命令,然后是您的数据库名称:

1use database_name

美元

要了解交易的行为,您需要一组文件来处理。本指南将使用代表世界上一些人口最多的城市的集合文件。

1[label The Tokyo document]
2{
3    "name": "Tokyo",
4    "country": "Japan",
5    "continent": "Asia",
6    "population": 37.400
7}

本文件包含以下信息:

  • 名称:城市的名字.
  • 国家:城市所在的国家.
  • 大陆:城市所在的大陆.
  • 人口:城市的人口,以百万计。

运行以下insertMany()方法,同时创建一个名为cities的集合并插入三个文档:

1db.cities.insertMany([
2    {"name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 },
3    {"name": "Delhi", "country": "India", "continent": "Asia", "population": 28.514 },
4    {"name": "Seoul", "country": "South Korea", "continent": "Asia", "population": 25.674 }
5])

输出将包含新插入对象分配的对象标识符列表:

1[secondary_label Output]
2{
3        "acknowledged" : true,
4        "insertedIds" : [
5                ObjectId("61646915c66c110cc07ca59b"),
6                ObjectId("61646915c66c110cc07ca59c"),
7                ObjectId("61646915c66c110cc07ca59d")
8        ]
9}

您可以通过运行无参数的find()方法来验证文档是否正确插入,该方法将检索城市集合中的每个文档:

1db.cities.find()
1[secondary_label Output]
2{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
3{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

最后,使用createIndex()方法创建一个索引,以确保集合中的每个文档都有一个独特的名称字段值。

1db.cities.createIndex( { "name": 1 }, { "unique": true } )

MongoDB 将确认该索引已成功创建:

1[secondary_label Output]
2{
3        "createdCollectionAutomatically" : false,
4        "numIndexesBefore" : 1,
5        "numIndexesAfter" : 2,
6        "commitQuorum" : "votingMembers",
7        "ok" : 1,
8        . . .
9}

有了它,您已经成功创建了人口最多的城市的示例文档列表,这些文档将作为测试数据来测试交易的使用情况。

步骤3 - 创建您的第一个完整交易

此步骤描述了如何创建一个单个操作的交易,将从上一个步骤插入新文档到样本集合。

首先,打开两个独立的 MongoDB 壳会话,其中一个会用来执行交易中的命令,另一个会让你在不同时间点查看数据库的其他用户在交易之外的数据。

<$>[注] :为了帮助保持事情清晰,本指南将使用不同颜色的代码块来区分这两个环境。

1[environment second]

第二个实例将位于交易外部,允许您检查您在交易中所做的任何更改如何可见于交易外部的客户。

1[environment third]

美元

在此时,如果您查询了城市集合,两个壳的会话都应该列出您之前插入的三个城市,请通过在两个壳的会话中发出一个find()查询来验证这一点,从第一个开始:

1[environment second]
2db.cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

然后在您的第二个壳会话中运行相同的查询:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

确认此查询的输出在两个会话中一致后,请尝试将新文档插入集合中,但是,而不是使用insertOne()方法,您将将该文档插入作为交易的一部分。

通常情况下,交易不是从 MongoDB Shell 编写和执行的,正如本指南所描述的。 更常见的是,交易被外部应用程序使用。 为了确保它运行的任何交易仍然是原子、一致、孤立和持久的,应用程序必须启动 session

在MongoDB中,会话是由应用程序通过适当的MongoDB驱动程序(https://docs.mongodb.com/drivers/)管理的数据库对象,这允许驱动程序将数据库陈述的序列相互关联,这意味着它们将有一个共享的背景,并可能有额外的配置应用于它们作为一个组,例如允许使用交易。

而不是设置外部应用程序,本教程概述了用简化JavaScript语法直接在MongoDB壳中处理交易所需的概念和个别步骤。

<$>[注] :您可以在 官方 MongoDB 文档中了解更多有关如何使用不同编程语言的交易的信息。

尽管本指南描述了如何通过 MongoDB 壳而不是在应用程序中使用交易,但仍然需要启动会话以执行一组操作作为一个交易。

1[environment second]
2var session = db.getMongo().startSession()

此命令会创建一个会话变量,该变量将存储会话对象. 在下面的示例中,每次你指向会话对象时,都会指向你刚刚开始的会话。

有了此会话对象,您可以通过以下方式调用startTransaction方法开始交易:

1[environment second]
2session.startTransaction({
3    "readConcern": { "level": "snapshot" },
4    "writeConcern": { "w": "majority" }
5})

请注意,该方法是在会话变量上调用而不是db,就像上一步中的命令一样。

startTransaction()方法接受两种选项:readConcernwriteConcernwriteConcern设置可以接受几个选项,但本示例只包括w选项,该选项要求集群承认在集群中特定数量的节点上已接受该交易的写作操作。

假设您开始交易,但在您这样做之后,另一个用户会将文档添加到您在集群中的另一个节点上使用的集合中。 您的交易是否应该读取该新数据,或者仅读取在该节点上写的数据? 设置readConcern级别允许您指定交易在进行交易时应该读取哪些数据。

请注意,设置交易的readConcern级别要求您将writeConcern设置为多数。这些读写问题值在大多数情况下都是安全的默认值,它们提供可靠的数据持久性保证,除非您对复制集中的表现和认可写作有非常特殊的要求。

如果此方法成功,您将处于正在运行的交易中,您可以开始执行将成为交易的一部分的陈述。

<$>[警告] **警告:**默认情况下,MongoDB会自动取消运行超过60秒的任何交易,原因在于交易不是设计用于在MongoDB壳中进行交互式构建,而是用于现实世界的应用程序。

因此,如果您在 60 秒的时间限制内不执行每个命令,您可能会在遵循本教程时遇到意外的错误。

 1[label Error message]
 2Error: error: {
 3        "errorLabels" : [
 4                "TransientTransactionError"
 5        ],
 6        "operationTime" : Timestamp(1634032826, 1),
 7        "ok" : 0,
 8        "errmsg" : "Transaction 1 has been aborted.",
 9        "code" : 251,
10        "codeName" : "NoSuchTransaction",
11        "$clusterTime" : {
12                "clusterTime" : Timestamp(1634032826, 1),
13                "signature" : {
14                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
15                        "keyId" : NumberLong(0)
16                }
17        }
18}

如果发生这种情况,您必须通过运行abortTransaction()方法将交易标记为已终止:

1session.abortTransaction()

然后,您必须使用您之前运行的相同的 startTransaction() 方法重新启动交易:

1session.startTransaction({
2    "readConcern": { "level": "snapshot" },
3    "writeConcern": { "w": "majority" }
4})

考虑到这一点,您可能有助于先阅读此步骤的其余部分,然后在您更好地理解相关概念后,在60秒的时间限制内执行命令。

当您在运行交易中工作时,您作为交易的一部分运行的任何陈述都必须在由您之前创建的会话变量表示的会话的共享背景中。

同样地,在运行交易中工作时,可以有助于创建另一个变量,代表您希望在会话框架内工作的集合。下列操作将通过从测试数据库中返回城市集合来创建一个名为城市的变量。

1[environment second]
2var cities = session.getDatabase('test').getCollection('cities')

从现在开始,直到您完成交易,您可以使用cities变量,就像您使用db.cities来参考cities集合一样。

通过检查该对象是否可以用来查找来自收藏的文档来测试:

1[environment second]
2cities.find()

该命令将返回与之前相同的文档列表,因为数据尚未更改:

1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

然后,作为正在运行的交易的一部分,将代表纽约市的新文档插入集合中,使用insertOne方法,但在cities变量上执行,以确保它在会话中运行:

1[environment second]
2cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDB 會返回成功訊息:

1[secondary_label Output]
2[environment second]
3{
4        "acknowledged" : true,
5        "insertedId" : ObjectId("6164849d53abeea9d9dd10cf")
6}

如果您再次執行「cities.find()」,您會注意到新插入的文件在相同的會議中立即可見:

1[environment second]
2cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

但是,如果您在第二个 MongoDB 壳实例中运行 `db.cities.find()',则代表纽约的文档将不存在:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

此原因是插入声明已在正在运行的交易中执行,但交易本身尚未进行,此时,交易仍然可以成功并保持数据,或者它可能会失败,从而取消所有更改,并将数据库留在您启动交易之前的状态。

要执行交易并将插入的文档永久保存到数据库中,请在会话对象上运行commitTransaction方法:

1[environment second]
2session.commitTransaction()

startTransaction一样,如果成功,此命令不会输出。

现在,在 MongoDB 壳中列出来自城市集中的文档,开始在运行会话中查询城市变量:

1[environment second]
2cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

然后查询在交易外运行的第二个壳中的城市集合:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

这次,新插入的文档既在会话内部又在会话外可见。该交易已成功完成,并持续对数据库进行的更改。

现在你知道如何开始和执行交易,你可以转到下一个步骤,该步骤描述了在启动交易后如何中止交易,以返回你在执行之前所做的任何更改。

步骤4 - 堕胎交易

此步骤跟上一步的路径类似,因为它使您以相同的方式开始交易. 然而,此步骤描述了如何取消交易而不是做出更改。

在遵循前一步后,您将有四个城市在收藏中,包括新添加的代表纽约的城市。

在第一个 MongoDB 壳中,启动会话并重新分配给会话变量:

1[environment second]
2var session = db.getMongo().startSession()

然后开始交易:

1[environment second]
2session.startTransaction({
3    "readConcern": { "level": "snapshot" },
4    "writeConcern": { "w": "majority" }
5})

再次,如果成功,此方法不会返回任何输出;如果成功,您将处于正在运行的交易中。

您可以通过重新创建一个cities变量来代表会话中的cities集合来做到这一点:

1[environment second]
2var cities = session.getDatabase('test').getCollection('cities')

从现在开始,您可以使用城市变量在会话中对城市集合进行操作。

现在交易已经开始,作为正在运行的交易的一部分,将另一个新文档插入这个集合中。本示例中的文档将代表布宜诺斯艾利斯。使用insertOne方法,但在cities变量上执行,以确保它将在会话中运行:

1[environment second]
2cities.insertOne({"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 })

MongoDB 将返回成功消息:

1[environment second]
2[secondary_label Output]
3{
4        "acknowledged" : true,
5        "insertedId" : ObjectId("6164887e322518cf706858b5")
6}

接下来,运行cities.find()查询:

1[environment second]
2cities.find()

请注意,新插入的文档在交易中的同一会话中立即可见:

1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
7{ "_id" : ObjectId("6164887e322518cf706858b5"), "name" : "Buenos Aires", "country" : "Argentina", "continent" : "South America", "population" : 14.967 }

但是,如果您要查询您第二个 MongoDB 壳实例中的城市集合,而该实例不在交易中运行,则返回列表不会包含布宜诺斯艾利斯文档,因为交易尚未完成:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

假设你犯了一个错误,而你不再想要进行交易。相反,你想要取消你作为该会话的一部分运行过的任何陈述,并完全取消交易。

1[environment second]
2session.abortTransaction()

abortTransaction() 方法告诉 MongoDB 抛弃所有在交易中引入的更改,并将数据库恢复到以前的状态,与 startTransactioncommitTransaction 一样,如果成功,此命令不会产生任何输出。

成功取消交易后,在 MongoDB 壳中列出城市集合中的文档,首先在运行会话中执行以下操作:

1[environment second]
2cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

然后在您第二个 shell 实例中运行以下查询,该实例在该会话之外运行:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

布宜诺斯艾利斯不在这两个名单中,在文件被插入后,但在交易发生之前,取消交易,就像插入它从未发生过一样。

在此步骤中,您学会了如何终止交易并回滚其存在期间引入的更改,然而,交易并不总是像这样手动中断。

步骤5 – 由于错误而中止交易

此步骤与之前的步骤类似,但这次您将了解在交易中执行的任何陈述中发生错误时会发生什么。

您的收藏包含四个城市,包括新添加的代表纽约的文件,但是代表布宜诺斯艾利斯的文件没有被插入,因为它在您在上一步中取消交易时被丢弃。

在第一个 MongoDB 壳中,启动会话并将其分配给会话变量:

1[environment second]
2var session = db.getMongo().startSession()

然后开始交易:

1[environment second]
2session.startTransaction({
3    "readConcern": { "level": "snapshot" },
4    "writeConcern": { "w": "majority" }
5})

再次创建城市变量:

1[environment second]
2var cities = session.getDatabase('test').getCollection('cities')

然后,作为正在运行的交易的一部分,将另一个新文档插入本集合中. 此示例插入代表日本大阪的文档:

1[environment second]
2cities.insertOne({"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 })

MongoDB 将返回成功消息:

1[secondary_label Output]
2[environment second]
3{
4        "acknowledged" : true,
5        "insertedId" : ObjectId("61648bb3322518cf706858b6")
6}

新插入的城市将立即从交易内部可见:

1[environment second]
2cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
7{ "_id" : ObjectId("61648bb3322518cf706858b6"), "name" : "Osaka", "country" : "Japan", "continent" : "Asia", "population" : 19.281 }

然而,大阪文件不会在第二个壳中可见,因为它不在交易范围内:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

该交易仍在运行,可用于对数据库进行进一步更改。

执行以下操作以尝试将另一个文档插入到收藏中作为该交易的一部分. 此命令将创建另一个代表纽约市的文档. 但是,由于您在设置该收藏时对名称字段的独特性限制,并且由于该收藏已经有一个文档,其名称字段的值是纽约,此插入One操作将与该限制相冲突,并导致错误:

1[environment second]
2cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDB 會返回錯誤訊息,指出此操作違反了唯一的限制:

 1[secondary_label Output]
 2[environment second]
 3WriteError({
 4        "index" : 0,
 5        "code" : 11000,
 6        "errmsg" : "E11000 duplicate key error collection: test.cities index: name_1 dup key: { name: \"New York\" }",
 7        "op" : {
 8                "_id" : ObjectId("61648bdc322518cf706858b7"),
 9                "name" : "New York",
10                "country" : "United States",
11                "continent" : "North America",
12                "population" : 18.819
13        }
14})
15. . .

此输出表明,代表纽约的新文档没有被插入数据库,但这并不解释您之前作为交易的一部分添加的代表大阪的文档发生了什么。

假设试图添加第二份纽约文档是错误的,但您确实打算将大阪文档保存在收藏中。

1[environment second]
2session.commitTransaction()

MongoDB 不会允许这样做,而是扔出一个错误:

 1[secondary_label Output]
 2[environment second]
 3uncaught exception: Error: command failed: {
 4        "errorLabels" : [
 5                "TransientTransactionError"
 6        ],
 7        "operationTime" : Timestamp(1633979403, 1),
 8        "ok" : 0,
 9        "errmsg" : "Transaction 0 has been aborted.",
10        "code" : 251,
11        "codeName" : "NoSuchTransaction",
12        "$clusterTime" : {
13                "clusterTime" : Timestamp(1633979403, 1),
14                "signature" : {
15                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
16                        "keyId" : NumberLong(0)
17                }
18        }
19}
20. . .

每当交易内部出现错误时,它会导致MongoDB自动中止交易,而且,由于交易是以一切或没有的方式执行的,在这种情况下不会发生交易内部的任何变化。

您可以通过在两个壳中运行find()查询来验证此情况,在第一个壳中,在会话的背景下运行查询:

1[environment second]
2cities.find()
1[secondary_label Output]
2[environment second]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

然后在第二个壳中,运行在会话之外,运行find()db.cities:

1[environment third]
2db.cities.find()
1[secondary_label Output]
2[environment third]
3{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
4{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
5{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
6{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

当MongoDB自动取消交易时,它也确保了所有更改被扭转。

结论

通过阅读本文,您熟悉了MongoDB中的ACID原则和多文档交易,您启动了交易,作为该交易的一部分插入了文档,并了解了文档在交易内部和外部时如何可见。

有了这些新技能,您可以在可能需要的应用程序中利用多文档交易的ACID保证,但请记住,MongoDB是一个以文档为导向的数据库,在许多情况下,文档模型本身,以及谨慎的方案设计,可以减少处理多文档交易的需求。

该教程仅提供了一个简短的介绍 MongoDB 中的交易,我们鼓励您研究官方 MongoDB 文档(https://docs.mongodb.com/v4.4/mongo/),以了解更多有关交易的运作方式。

Published At
Categories with 技术
comments powered by Disqus