如何在MongoDB中使用事务

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。

介绍

transaction 是一系列数据库操作,只有在事务中的每个操作都已正确执行时才会成功。 多年来,事务一直是关系数据库的一个重要特征,但直到最近,面向文档的数据库中才几乎没有出现。 面向文档的数据库的本质——单个文档可以是一个健壮的嵌套结构,包含嵌入的文档和数组,而不仅仅是简单的值——简化了在单个文档中存储相关数据。 因此,作为单个逻辑操作的一部分修改多个文档通常是不必要的,这限制了许多应用程序中对事务的需求。

但是,即使使用面向文档的数据库,也需要在单个操作中访问和修改多个文档并保证完整性的应用程序。 MongoDB 在数据库引擎 4.0 版本中引入了多文档 ACID 事务,以满足此类用例的需求。 在本文中,您将探索什么是事务、事务的 ACID 属性以及如何在 MongoDB 中使用事务。

先决条件

由于它们在 MongoDB 中实现的方式,事务只能在作为更大集群的一部分运行的 MongoDB 实例上执行。 这可以是分片数据库集群或副本集。 如果您有一个现有的 MongoDB 分片集群或 副本集 正在运行,您可以将其用于测试目的,那么您可以继续下一节了解 ACID 事务。

但是,设置一个适当的、功能性的副本集或一个分片的 MongoDB 集群需要您至少拥有三个正在运行的 MongoDB 实例,最好在单独的服务器上运行。 此外,本指南的示例都涉及使用作为副本集成员运行的单个节点。 在本指南中,您可以将独立的 MongoDB 实例转换为可以使用的单节点副本集,而不是让您完成配置多个服务器并在每个服务器上配置 MongoDB 以仅使用其中一个的工作练习运行事务。

本指南的第一步概述了如何执行此操作,因此要完成本教程,您只需要以下内容:

  • 一台具有常规非 root 用户的服务器,具有 sudo 权限和配置了 UFW 的防火墙。 本教程使用运行 Ubuntu 20.04 的服务器进行了验证,您可以按照此 Ubuntu 20.04 初始服务器设置教程来准备您的服务器。
  • MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。

了解 ACID 事务

transaction 是一组数据库操作(例如读取和写入),这些操作按顺序和全有或全无的方式执行。 这意味着,为了将运行这些操作的结果保存在数据库中并在事务之外对其他数据库客户端可见,所有单个操作都必须成功。 如果任何操作未能正确执行,事务将中止,并且从事务开始所做的所有更改都将被撤消。 然后数据库将恢复到之前的状态,就好像这些操作从未发生过一样。

为了说明交易对数据库系统至关重要的原因,假设您在一家银行工作,您需要将资金从客户 A 转移到客户 B。 这意味着您必须同时减少源账户的余额并增加目标账户的余额。

如果两个操作中的任何一个单独失败而另一个通过,银行记录就会变得不一致。 要么客户 B 不知从何而来(如果客户 A 的帐户余额没有减少),要么客户 A 会无缘无故地赔钱(如果他们的余额减少了,但客户 B 没有记入贷方)。 为了确保结果始终一致,两个操作都必须成功,或者都必须失败。 事务在这种情况下特别方便,可确保执行全有或全无。

确保此类复杂操作可以安全可靠地执行,保证数据在错误或中断的情况下仍然有效的数据库事务的四个属性,缩写为 ACID: atomicity, consistency[ X248X]、隔离耐久性。 如果数据库系统能够保证所有这四个操作在一个事务中分组的一组操作,它也可以保证即使在执行中发生意外错误时数据库也将保持在有效状态。

  • Atomicity 意味着事务中的所有动作都被视为一个工作单元,要么全部执行要么不执行,中间不执行任何操作。 上一个从一个账户中扣除的钱被添加到另一个账户的例子突出了原子性原则。 请注意,在 MongoDB 中,单个文档中的更新(无论文档结构有多复杂和嵌套)始终是 atomic,即使不使用事务也是如此。 只有在处理多个文档的情况下,事务才能提供更强的原子性保证。
  • Consistency 表示对数据库所做的任何更改都必须遵守数据库现有的约束,否则整个事务将失败。 例如,如果其中一个操作违反了唯一索引或模式验证规则,MongoDB 将中止事务。
  • Isolation 是一种想法,即单独的并发运行的事务彼此隔离,并且两者都不会影响对方的结果。 如果两个事务同时执行,隔离规则保证最终结果与一个接一个执行一样。
  • Durability保证一旦事务成功,客户端就可以确定数据已经被正确持久化了。 即使是硬件故障或电源中断之类的事情也不会使交易无效。

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

第 1 步 — 将您的独立 MongoDB 实例转换为副本集

如前所述,由于它们在 MongoDB 中的实现方式,事务只能在作为更大集群的一部分运行的数据库上执行。 该集群可以是分片数据库集群或副本集。

如果您已经配置了可用于练习运行事务的副本集或分片集群,则可以跳过此步骤并在步骤 2 中使用该集群。 如果没有,此步骤将概述如何将独立 MongoDB 实例转换为单节点副本集。

警告:像您将在此步骤中配置的单节点副本集可用于测试目的,但不适合在生产环境中使用。 这样做的原因是副本集旨在在多个分布式节点上运行,因为这有助于保持数据库的高可用性:如果任何一个节点发生故障,客户端仍然可以连接到集合中的其他节点。 这个单节点集不会有任何这种冗余,并且应该只在需要使用副本集的情况下用于测试。

如果您想了解有关 MongoDB 中的复制及其涉及的安全隐患的更多信息,我们强烈建议您查看我们关于 如何在 Ubuntu 20.04 上配置 MongoDB 副本集的教程。


要将您的独立 MongoDB 实例转换为副本集,首先使用您喜欢的文本编辑器打开 MongoDB 配置文件。 此示例使用 nano

sudo nano /etc/mongod.conf

找到该文件底部的 #replication: 部分:

/etc/mongod.conf

. . .
#replication:
. . .

通过删除井号 (#) 取消注释此行。 然后在这一行下面添加一个 replSetName 指令,后跟一个 MongoDB 将用来标识副本集的名称:

/etc/mongod.conf

. . .
replication:
  replSetName: "rs0"
. . .

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

注意:启用复制后,MongoDB 还要求您配置除密码验证之外的一些验证方式,如密钥文件验证或设置 x.509 证书。 如果您按照我们的 如何在 Ubuntu 20.04 上保护 MongoDB 教程并在您的 MongoDB 实例上启用身份验证,您将只启用密码身份验证。

为了本教程的目的,最好不要设置更高级的安全措施,而是禁用 mongod.conf 文件中的 security 块。 通过用井号注释掉 security 块中的每一行来做到这一点:

/etc/mongod.conf

. . .

#security:
#  authorization: enabled

. . .

只要您只打算将此数据库用于练习交易或其他测试目的,这不会带来安全风险。 但是,如果您计划将来使用此 MongoDB 实例存储任何敏感数据,请务必取消注释这些行以重新启用身份验证


这些是您需要对此文件进行的唯一更改,因此您可以保存并关闭它。 如果您使用 nano 编辑文件,您可以按 CTRL + XY,然后按 ENTER 进行编辑。

之后,重新启动 mongod 服务以实现新的配置更改:

sudo systemctl restart mongod

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

mongo

在 MongoDB 提示符下,运行以下 rs.initiate() 方法。 这会将您的独立 MongoDB 实例转换为可用于测试的单节点副本集:

rs.initiate()

如果此方法在其输出中返回 "ok" : 1 ,则表示副本集已成功启动:

Output{
. . .
    "ok" : 1,
. . .

假设是这种情况,您的 MongoDB shell 提示符将更改以指示 shell 连接到的实例现在是 rs0 副本集的成员:


请注意,此示例提示反映了此 MongoDB 实例是副本集的辅助成员。 这是意料之中的,因为在启动副本集的时间与其其中一个成员被提升为主要成员的时间之间通常存在差距。

如果您要运行一个命令,或者在等待片刻后直接按下 ENTER,提示将更新以反映您已连接到副本集的主要成员:


您的独立 MongoDB 实例现在作为可用于测试事务的单节点副本集运行。 暂时保持提示打开,因为您将在下一步中使用 MongoDB shell 创建示例集合并将一些示例数据插入其中。

第 2 步 — 准备样本数据

为了解释 MongoDB 中的事务如何工作以及如何使用它们,此步骤概述了如何打开 MongoDB shell 以连接到副本集的主节点。 它还解释了如何创建示例集合并将一些示例文档插入其中。 本指南将使用此示例数据来说明如何启动和执行事务。

如果您跳过了上一步,因为您有一个现有的分片 MongoDB 集群或副本集,请连接到您可以写入数据的任何节点:

mongo

注意: 在新连接时,MongoDB shell 默认会自动连接到 test 数据库。 您可以安全地使用此数据库来试验 MongoDB 和 MongoDB shell。

或者,您也可以切换到另一个数据库来运行本教程中给出的所有示例命令。 要切换到另一个数据库,请运行 use 命令,后跟数据库名称:

use database_name

要了解事务的行为,您需要使用一组文档。 本指南将使用代表世界上人口最多的几个城市的集合文件。 例如,以下示例文档代表东京:

东京文件

{
    "name": "Tokyo",
    "country": "Japan",
    "continent": "Asia",
    "population": 37.400
}

本文档包含以下信息:

  • name:城市名称。
  • country:城市所在的国家。
  • continent:城市所在的大陆。
  • population:城市人口,百万。

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

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

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

Output{
        "acknowledged" : true,
        "insertedIds" : [
                ObjectId("61646915c66c110cc07ca59b"),
                ObjectId("61646915c66c110cc07ca59c"),
                ObjectId("61646915c66c110cc07ca59d")
        ]
}

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

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

最后,使用 createIndex() 方法创建一个索引,以确保集合中的每个文档都具有唯一的 name 字段值。 这将有助于在本指南后面运行事务时测试一致性要求:

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

MongoDB 将确认索引创建成功:

Output{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        . . .
}

至此,您已成功创建人口最多城市的示例文档列表,这些示例文档将用作测试交易使用情况的测试数据。 接下来,您将学习如何设置交易。

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

此步骤概述了如何创建由单个操作组成的事务,该操作会将新文档插入上一步的示例集合中。

首先打开两个单独的 MongoDB shell 会话。 一个将用于在事务中执行命令,另一个将允许您在不同的时间点检查事务之外的数据库的其他用户可以使用哪些数据。

注意:为了帮助保持清晰,本指南将使用不同颜色的代码块来区分这两种环境。 您将用于执行事务的第一个实例将具有蓝色背景,如下所示:


第二个实例将在事务之外,允许您检查您在事务中所做的任何更改如何对其外部的客户可见。 第二个环境将具有红色背景,如下所示:


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

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

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

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

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

通常,事务不是从 MongoDB Shell 编写和执行的,如本指南所述。 通常,事务被外部应用程序使用。 为确保其运行的任何事务保持原子性、一致、隔离和持久性,应用程序必须启动 会话

在 MongoDB 中,会话是由应用程序通过适当的 MongoDB 驱动程序 管理的数据库对象。 这允许驱动程序将一系列数据库语句相互关联,这意味着它们将具有共享上下文,并且可以将其他配置作为一个组应用于它们,例如启用事务的使用。 正如此步骤将说明的那样,外部世界可能不会立即看到单个会话中发生的事情。

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

注意:您可以在官方MongoDB文档中了解如何使用不同的编程语言使用事务。 官方文档中描述的代码示例将比本指南中包含的更复杂,但两种方法都遵循相同的原则。


尽管本指南概述了如何通过 MongoDB shell 而不是在应用程序中使用事务,但仍然需要启动会话才能将一组操作作为事务执行。 您可以使用以下命令启动会话:

var session = db.getMongo().startSession()

此命令创建一个 session 变量来存储会话对象。 在以下示例中,每次引用 session 对象时,您指的是刚刚开始的会话。

有了这个会话对象,您可以通过调用 startTransaction 方法来启动事务,如下所示:

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

请注意,该方法是在 session 变量上调用的,而不是在 db 上调用的,正如上一步中的命令所做的那样。 startTransaction() 方法接受两个选项:readConcernwriteConcernwriteConcern 设置可以接受几个选项,但是这个例子只包括 w 选项,它要求集群在事务的写操作在指定数量的节点上被接受时进行确认簇。 此示例指定仅当 majority 个节点确认写入操作时,才认为事务已成功保存,而不是单个数字。

假设您启动了一个事务,但在您这样做之后,另一个用户将一个文档添加到您在集群中另一个节点上使用的集合中。 您的事务应该读取这些新数据还是只读取在启动事务的节点上写入的数据? 设置 readConcern 级别允许您指定在提交事务时事务应读取哪些数据。 将其设置为 snapshot 意味着事务将读取集群中大多数节点已提交的数据的快照。

请注意,设置事务的 readConcern 级别需要您将 writeConcern 设置为 majority。 在大多数情况下,这些读取和写入问题的值是安全的默认值。 它们提供数据持久性的可靠保证,除非您对性能和确认跨副本集的写入有非常特殊的要求。 您可以在 官方 MongoDB 文档 中了解有关 MongoDB 提供用于事务的不同写入和读取关注点的更多信息。

如果正确执行,startTransaction 方法不会返回任何输出。 如果此方法成功,您将处于正在运行的事务中,您可以开始执行将成为事务一部分的语句。

警告: 默认情况下,MongoDB 将自动中止任何运行超过 60 秒的事务。 这样做的原因是事务并非设计为在 MongoDB shell 中以交互方式构建,而是在实际应用程序中使用。

因此,如果您没有在 60 秒的时间限制内执行每个命令,则在学习本教程时可能会遇到意外错误。 如果遇到如下错误,则说明 MongoDB 因为超过了时间限制而中止了事务:

错误信息

Error: error: {
        "errorLabels" : [
                "TransientTransactionError"
        ],
        "operationTime" : Timestamp(1634032826, 1),
        "ok" : 0,
        "errmsg" : "Transaction 1 has been aborted.",
        "code" : 251,
        "codeName" : "NoSuchTransaction",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1634032826, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

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

session.abortTransaction()

然后,您必须使用之前运行的相同 startTransaction() 方法重新初始化事务:

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

之后,您需要从头开始再次执行事务中的每个语句。 考虑到这一点,在您更好地理解所涉及的概念后,首先阅读此步骤的其余部分,然后在 60 秒的时间限制内执行命令可能会对您有所帮助。


当您在正在运行的事务中工作时,作为事务的一部分运行的任何语句都必须在您之前创建的 session 变量表示的会话的共享上下文中。

出于类似的目的,在正在运行的事务中工作时,创建另一个变量来表示您要在会话上下文中使用的集合会很有帮助。 以下操作将通过从 test 数据库返回 cities 集合来创建一个名为 cities 的变量。 但是,它不是直接从 db 对象中提取它,而是引用 session 对象以确保此变量代表正在运行的会话上下文中的 cities 集合:

var cities = session.getDatabase('test').getCollection('cities')

从现在开始直到您提交事务,您可以使用 cities 变量,就像使用 db.cities 来引用 cities 集合一样。 这个新分配的变量将保证您在会话中运行语句,同样,在启动的事务中。

通过检查该对象是否可用于从集合中查找文档来对此进行测试:

cities.find()

由于数据尚未更改,该命令将返回与以前相同的文档列表:

Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

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

cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDB 将返回一条成功消息:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("6164849d53abeea9d9dd10cf")
}

如果您要再次执行 cities.find(),您会注意到新插入的文档在同一会话中立即可见:

cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

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

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

原因是insert语句已经在运行的事务内部执行了,但是事务本身还没有提交。 此时,事务仍然可以成功并保留数据,也可以失败,这将撤消所有更改并使数据库保持与您启动事务之前相同的状态。

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

session.commitTransaction()

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

现在,列出两个 MongoDB shell 中 cities 集合中的文档。 首先在运行会话中查询 cities 变量:

cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

然后在事务外运行的第二个 shell 中查询 cities 集合:

db.cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

这一次,新插入的文档在会话内部和外部都可见。 事务已成功提交并成功结束,并保留了它对数据库所做的更改。 您现在可以在事务外部和所有进一步事务内部访问 New York 对象。

现在您知道如何启动和执行事务,您可以继续下一步,该步骤概述了如何在启动事务后中止事务以回滚您在执行之前所做的任何更改。 确保打开两个 MongoDB shell 环境,因为您将在本教程的其余部分继续使用这两个环境。

第 4 步 — 中止交易

此步骤遵循与前一个类似的路径,因为它让您以相同的方式启动事务。 但是,此步骤概述了如何中止事务而不是提交更改。 这样做时,事务引入的所有更改都会回滚,将数据库返回到之前的状态,就好像事务从未发生过一样。

完成上一步之后,您将在集合中拥有四个城市,包括新添加的代表纽约的城市。

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

var session = db.getMongo().startSession()

然后开始交易:

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

同样,如果成功,此方法将不会返回任何输出。 如果成功,您将处于正在运行的事务中。

您将再次需要访问会话上下文中的 cities 集合。 您可以通过再次创建一个 cities 变量来表示会话中的 cities 集合来做到这一点:

var cities = session.getDatabase('test').getCollection('cities')

从现在开始,您可以使用 cities 变量在会话上下文中作用于 cities 集合。

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

cities.insertOne({"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 })

MongoDB 将返回成功消息:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("6164887e322518cf706858b5")
}

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

cities.find()

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

Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
{ "_id" : ObjectId("6164887e322518cf706858b5"), "name" : "Buenos Aires", "country" : "Argentina", "continent" : "South America", "population" : 14.967 }

但是,如果您要在第二个 MongoDB shell 实例中查询 cities 集合,该实例不在事务中运行,则返回的列表将不包含布宜诺斯艾利斯文档,因为尚未提交事务:

db.cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

假设您犯了一个错误,并且您不再想提交交易。 相反,您希望取消作为此会话的一部分运行的任何语句并完全中止事务。 为此,请运行 abortTransaction() 方法:

session.abortTransaction()

abortTransaction() 方法告诉 MongoDB 丢弃事务中引入的所有更改,并将数据库返回到以前的状态。 与 startTransactioncommitTransaction 一样,如果成功,此命令不会给出任何输出。

成功中止事务后,列出两个 MongoDB shell 中 cities 集合中的文档。 首先,在运行会话中运行以下操作:

cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

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

db.cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

布宜诺斯艾利斯没有出现在两个名单中。 在插入文档之后但在事务提交之前中止事务,就像插入它从未发生过一样。

在这一步中,您学习了如何终止事务并回滚在其存在期间引入的更改。 但是,事务并不总是像这样手动中止。 通常,MongoDB 在事务执行之前终止事务的原因是事务中的一个操作导致了错误。

第 5 步 - 由于错误而中止交易

此步骤与上一步类似,但这次您将了解在事务中执行的任何语句期间发生错误时会发生什么。

此时您应该仍然有两个打开的 shell 会话。 您的收藏包含四个城市,包括新添加的代表纽约的文件。 但是,没有插入代表布宜诺斯艾利斯的文档,因为在上一步中中止事务时它已被丢弃。

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

var session = db.getMongo().startSession()

然后开始交易:

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

再次创建 cities 变量:

var cities = session.getDatabase('test').getCollection('cities')

之后,将另一个新文档作为正在运行的事务的一部分插入到此集合中。 此示例插入代表日本大阪的文档:

cities.insertOne({"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 })

MongoDB 将返回成功消息:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("61648bb3322518cf706858b6")
}

新插入的城市将立即从事务内部可见:

cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
{ "_id" : ObjectId("61648bb3322518cf706858b6"), "name" : "Osaka", "country" : "Japan", "continent" : "Asia", "population" : 19.281 }

但是,大阪文档在第二个 shell 中不可见,因为它在事务之外:

db.cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

事务仍在运行,可用于对数据库执行进一步的更改。

运行以下操作以尝试将一个文档作为此事务的一部分插入到集合中。 此命令将创建另一个代表纽约市的文档。 但是,由于您在设置此集合时应用到 name 字段的唯一性约束,并且由于此集合已经有一个文档,其 name 字段的值为 New York,因此insertOne 操作将与该约束冲突并导致错误:

cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDB 将返回错误消息,指出此操作违反了唯一约束:

OutputWriteError({
        "index" : 0,
        "code" : 11000,
        "errmsg" : "E11000 duplicate key error collection: test.cities index: name_1 dup key: { name: \"New York\" }",
        "op" : {
                "_id" : ObjectId("61648bdc322518cf706858b7"),
                "name" : "New York",
                "country" : "United States",
                "continent" : "North America",
                "population" : 18.819
        }
})
. . .

这个输出表明这个代表纽约的新文档没有插入到数据库中。 但是,这并不能解释您之前在交易中添加的代表大阪的文件发生了什么情况。

假设尝试添加第二个纽约文档是一个错误,但您确实打算将大阪文档保留在集合中。 您可以尝试提交事务以持久保存大阪文档:

session.commitTransaction()

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

Outputuncaught exception: Error: command failed: {
        "errorLabels" : [
                "TransientTransactionError"
        ],
        "operationTime" : Timestamp(1633979403, 1),
        "ok" : 0,
        "errmsg" : "Transaction 0 has been aborted.",
        "code" : 251,
        "codeName" : "NoSuchTransaction",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1633979403, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
. . .

每当事务内部发生错误时,都会导致 MongoDB 自动中止事务。 此外,由于事务以全有或全无的方式执行,因此在这种情况下不会保留来自事务内部的任何更改。 添加代表纽约的第二个文档导致的错误导致 MongoDB 中止事务并丢弃代表大阪的文档。

您可以通过在两个 shell 中运行 find() 查询来验证这一点。 在第一个 shell 中,在会话的上下文中运行查询:

cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

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

db.cities.find()
Output{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
{ "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
{ "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
{ "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

既不会显示大阪,也不会显示重复的纽约条目。 当 MongoDB 自动中止事务时,它还确保所有更改都已还原。 Osaka 在事务上下文中短暂可见,但在事务之外对其他数据库用户不可用。

结论

通过阅读本文,您熟悉了 MongoDB 中的 ACID 原理和多文档事务。 您启动了一个事务,将文档作为该事务的一部分插入,并了解了文档何时在事务内部和外部可见。 您学习了如何提交事务以及如何中止它并回滚任何更改,以及当事务内部发生错误时会发生什么。

掌握这些新技能后,您可以在可能需要的应用程序中利用多文档事务的 ACID 保证。 但请记住,MongoDB 是一个面向文档的数据库。 在许多情况下,文档模型本身以及仔细的架构设计可以减少处理多文档事务的需要。

本教程仅简要介绍了 MongoDB 中的事务。 我们鼓励您学习官方 官方 MongoDB 文档 以了解有关事务如何工作的更多信息。