分享到

简介

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

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

什么是事务?

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

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

这些功能通过提供 原子性隔离 来帮助数据库实现 ACID 兼容性。 这些功能共同帮助数据库保持 一致性。 此外,事务中的更改只有在提交到非易失性存储后才会返回为成功,这提供了 持久性

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

Database Transactions

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

文档模型 本身解决了事务所针对的许多需求。 尽管如此,在文档模型中仍然有一些用例脱颖而出,因为它们需要利用事务。

需要事务的应用程序通常是那些在不同方之间交换值的应用程序。 例如,"记录系统""业务线" 应用程序。

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

您如何使用事务?

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

这两个 API 的比较最好用下表总结

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

创建事务会话

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

为了演示,我们将逐步介绍通过 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 中看到相同的返回结果(该 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() 方法来提交事务并将数据库置于新的一致状态。

session.commitTransaction()

现在,新记录将存在于两个实例中:进行事务会话的 MongoDB shell 和 MongoDB shell(存在于事务之外)。

中止事务

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

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

session.abortTransaction()

此方法将取消事务会话并丢弃任何潜在更改。它不会导致新的一致状态,而是会回滚更改,使数据库保持与事务会话首次启动时相同的状态。

结论

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

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

常见问题

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

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

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

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

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

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

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

单个事务中的操作数量不应超过 1,000 个在事务中修改的文档。此外,由于事务中会执行多个操作,因此选择适当的读取和写入关注点至关重要。MongoDB 提供了关于这方面以及更多内容的全面 最佳实践指南

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

关于作者
Alex Emerich

Alex Emerich

Alex 是一个典型的观鸟爱好者,喜欢嘻哈音乐和阅读,也喜欢写关于数据库的文章。他目前住在柏林,经常被看到像利奥波德·布鲁姆一样漫无目的地在城市中漫步。