分享到

简介

事务是在数据库中对处理过程进行逻辑分组的方式,它封装了一个或多个操作,例如跨多个文档的读取和/或写入。数据库能够将这组操作解释为一个有凝聚力的单元并对其进行操作,而不是在单个语句中执行。这有助于确保数据集在许多密切相关的语句过程中的一致性。

在本指南中,我们将首先讨论什么是事务、何时在文档模型中使用事务,以及事务在 MongoDB 中的概念性工作方式。

什么是事务?

事务是将多个语句分组并隔离以作为单个操作进行处理的一种方式。在事务中,命令被捆绑在一起并在与其他请求不同的上下文中执行,而不是像发送到服务器那样单独执行每个命令。

隔离性是事务的重要组成部分。在事务内部,执行的语句只能影响事务本身内部的环境。从事务内部来看,语句可以修改数据,结果会立即可见。从外部来看,在事务提交之前不会进行任何更改,届时事务内的所有操作将一次性变为可见。

这些特性通过提供原子性隔离性,帮助数据库实现 ACID 合规性。这些共同帮助数据库保持一致性。此外,事务中的更改在提交到非易失性存储之前不会作为成功返回,这提供了持久性

MongoDB 支持多文档 ACID 事务分布式多文档 ACID 事务。本质上,文档模型允许将相关数据存储在单个文档中。文档模型与原子文档更新相结合,消除了大多数用例中对事务的需求。但是,在某些情况下,多文档、多集合 MongoDB 事务是您的最佳选择。

Database Transactions

何时在文档模型中使用事务

文档模型本身就解决了很多事务目标的需求。尽管如此,在文档模型中,仍有一些用例因需要利用事务而脱颖而出。

需要事务的应用程序通常是在不同方之间交换值的应用程序。一些示例包括“记录系统”“业务线”应用程序。

多文档事务的优势专为资金流转系统(如银行应用程序或支付处理)、供应链或运输系统(其中商品所有权发生转移)或电子商务而量身定制。这些示例通常是将跨多个流的更改打包在一起,并且需要与事务提供的全有或全无方法保持强一致性。

如何使用事务?

MongoDB 提供了两种 API 来使用事务。第一种是核心 API,其语法与关系数据库类似。第二种是回调 API,是 MongoDB 中使用事务的推荐方法。

两种 API 的比较最好总结在下表中

核心 API回调 API
需要显式调用来启动和提交事务启动事务,执行指定的操作,并提交(或在出错时中止)
不自动合并 TransientTransactionErrorUnknownTransactionCommitResult 的错误处理逻辑,而是授予集成自定义错误处理的能力。自动合并 TransientTransactionErrorUnknownTransactionCommmitResult 的错误处理逻辑。
需要将显式逻辑会话传递给特定事务的 API。需要将显式逻辑会话传递给特定事务的 API。

创建事务会话

事务通常通过 API 方法从外部应用程序编写和执行,并使用应用程序语言的相应 MongoDB 驱动程序

为了演示,我们将逐步介绍如何通过 MongoDB shell 创建事务。我们将使用一个示例数据库和集合来帮助概念化事务在实践中的工作方式。

注意:如果事务会话在初始 startTransaction() 方法之后运行超过 60 秒,MongoDB 将自动中止操作。发生这种情况是因为事务通常在自动工作的应用程序中进行,而不是由人员在 shell 中发出命令。以下步骤用于概念化从开始到结束的事务会话。

首先,我们有一个名为 literature 的简单数据库,其中包含一个名为 authors 的集合。运行快速 find() 查询,我们可以看到包含作者姓名及其作品标题的文档结构。

db.authors.find()
[
{
_id: ObjectId("620397dd4b871fc65c193106"),
first_name: 'James',
last_name: 'Joyce',
title: 'Ulysses'
},
{
_id: ObjectId("620398016ed0bb9e23785973"),
first_name: 'William',
last_name: 'Gibson',
title: 'Neuromancer'
},
{
_id: ObjectId("6203981d6ed0bb9e23785974"),
first_name: 'George',
last_name: 'Orwell',
title: 'Homage to Catalonia'
},
{
_id: ObjectId("620398516ed0bb9e23785975"),
first_name: 'James',
last_name: 'Baldwin',
title: 'The Fire Next Time'
}
]

现在我们有了数据库,我们可以展示直接在 MongoDB shell 中使用事务的步骤。我们还将演示在一个会话中进行的事务可能无法被外部源检测到。

首先,我们需要创建一个会话,就像使用 API 时一样。

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

这个新创建的 session 变量将存储会话对象。

下一步是通过调用 startTransaction() 方法来启动事务

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

startTransaction() 方法有两个选项,readConcernwriteConcern。您可以在 MongoDB 文档中阅读有关这些选项的更多详细信息。我们将 readConcern level 设置为 snapshot,如果事务使用 writeConcern “majority” 提交,则它会从大多数已提交数据的快照返回数据。

当您使用 w: "majority" 写入关注以及 "snapshot" 的读取关注级别提交时,这保证了操作具有大多数已提交数据的同步快照。如果没有特定要求,则可以将这种写入和读取关注配置视为良好的默认设置。

如果成功,该方法将不返回任何内容,如果出现任何错误,则需要中止事务,我们将在稍后讨论。

在事务会话内部工作

现在您已经启动了事务会话,您必须在上一节的 session 变量的上下文中运行语句。

在会话中用变量表示您的集合有助于简化语法。您可以按如下方式执行此操作

var authors = session.getDatabase('literature').getCollection('authors')

当在事务会话外部的 shell 中工作时,这个新创建的变量将像 db.authors 一样工作。您可以通过打开第二个 shell 窗口,连接到您的集群并运行 db.authors.find() 来验证这一点。这两个语句都将返回相同的文档。

在会话内部,我们现在想要模拟外部应用程序可能执行的操作,并将记录添加到数据库。我们可以通过以下方式将记录添加到我们的 authors 集合中

authors.insertOne( {
"first_name": "Virginie",
"last_name": "Despentes",
"title": "Vernon Subutex") }
)

MongoDB 将返回成功

{
acknowledged: true,
insertedId: ObjectId("6203a075c374636bc6976baa")
}

如果我们现在在会话中运行 authors.find(),我们将返回先前的结果,其中包含我们添加的内容

[
{
_id: ObjectId("620397dd4b871fc65c193106"),
first_name: 'James',
last_name: 'Joyce',
best_title: 'Ulysses'
},
{
_id: ObjectId("620398016ed0bb9e23785973"),
first_name: 'William',
last_name: 'Gibson',
best_title: 'Neuromancer'
},
{
_id: ObjectId("6203981d6ed0bb9e23785974"),
first_name: 'George',
last_name: 'Orwell',
best_title: 'Homage to Catalonia'
},
{
_id: ObjectId("620398516ed0bb9e23785975"),
first_name: 'James',
last_name: 'Baldwin',
best_title: 'The Fire Next Time'
},
{
_id: ObjectId("6203a075c374636bc6976baa"),
first_name: 'Virginie',
last_name: 'Despentes',
best_title: 'Vernon Subutex'
}
]

由于此事务会话尚未提交,因此我们不会在会话外部存在的 MongoDB shell 中看到它返回相同的结果。为了确认,我们在另一个 MongoDB shell 实例中运行 db.authors.find(),我们返回原始文档。

[
{
_id: ObjectId("620397dd4b871fc65c193106"),
first_name: 'James',
last_name: 'Joyce',
best_title: 'Ulysses'
},
{
_id: ObjectId("620398016ed0bb9e23785973"),
first_name: 'William',
last_name: 'Gibson',
best_title: 'Neuromancer'
},
{
_id: ObjectId("6203981d6ed0bb9e23785974"),
first_name: 'George',
last_name: 'Orwell',
best_title: 'Homage to Catalonia'
},
{
_id: ObjectId("620398516ed0bb9e23785975"),
first_name: 'James',
last_name: 'Baldwin',
best_title: 'The Fire Next Time'
}
]

这种差异表明事务当前仍处于不确定状态。它仍然可以成功并提交到数据库,也可能失败并中止。无论哪种方式,数据库最终都将处于一致的状态。它将包含新记录,或者回滚到事务会话开始之前的状态。

我们在会话中运行 commitTransaction() 方法以提交事务并将数据库带到新的 consistent 状态

session.commitTransaction()

现在,新记录将同时存在于事务会话发生的 MongoDB shell 实例和事务外部的 MongoDB shell 中。

中止事务

现在我们已经从启动会话一直到提交会话,我们可以探索事务的替代结果。

此路径的开头相同,仅在提交点发生更改。如果我们想放弃事务正在进行的更改,那么我们在会话 shell 中使用 abortTransaction() 方法,如下所示

session.abortTransaction()

此方法取消事务会话并放弃任何潜在的更改。数据库不会产生新的 consistent 状态,而是回滚更改并保持与事务会话首次启动时相同的状态。

结论

在本指南中,我们讨论了什么是事务,以及事务最适合 MongoDB 的哪些用例。我们还在 MongoDB shell 中概念性地演练了事务会话的过程,以便您可以了解外部应用程序将如何运行。

事务是关系数据库的基本需求,也是文档模型数据库(如 MongoDB)中特定用例的需求。对于文档模型,一般建议是将一起访问的数据存储在一起。但是,有时情况并非如此,因此了解事务的基础知识非常重要。

常见问题解答

是的,MongoDB 事务可以在 Node.js 中使用。

MongoDB 提供了一个有用的入门指南 “如何在 Node.js 中使用 MongoDB 事务”

ACID 是原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability) 的首字母缩写。所有这些属性都与保持数据库处于有效状态有关。

由于事务是将数据库操作组合在一起执行,因此确保在发生意外错误时数据库的有效性至关重要。ACID 兼容事务可防止无效的数据库状态。

MongoDB 事务存在于会话中。您可以使用 startSession() 创建会话,然后使用 session.startTransaction() 来暂存要提交的事务操作。

要有意回滚这些更改,您可以使用 session.abortTransaction() 方法来放弃会话中启动的操作。这必须在 session.commitTransaction() 方法之前进行。

使用 MongoDB 时,需要考虑几个事务最佳实践。您需要确保进行优化,以使事务运行时保持在启动后 60 秒内,因为 MongoDB 会自动中止任何时间更长的事务。

事务中的操作数不应超过 1,000 个文档在事务中被修改。此外,由于事务中会发生多个操作,因此选择适当的读取和写入关注非常重要。MongoDB 提供了一份详尽的最佳实践指南,其中涵盖了这一点以及更多内容。

是的,MongoDB 在 MongoDB 4.0 及更高版本中支持多文档 ACID 事务

关于作者
Alex Emerich

Alex Emerich

Alex 是典型的观鸟爱好者、嘻哈音乐爱好者和书虫,他也喜欢撰写有关数据库的文章。他目前居住在柏林,在那里可以看到他像利奥波德·布卢姆一样漫无目的地走在城市中。