简介
事务是数据库中处理的逻辑组,它封装了跨多个文档的一个或多个操作,例如读取和/或写入。数据库不是单独执行每个语句,而是能够将这组操作作为一个有凝聚力的单元进行解释和执行。这有助于确保数据集在许多密切相关的语句执行过程中的一致性。
在本指南中,我们将首先讨论什么是事务,何时在文档模型中使用它们,以及它们在 MongoDB 中如何概念性地工作。
如果您正在使用 MongoDB,请查看 Prisma 的 MongoDB 连接器!您可以使用 Prisma Client 信心十足地管理生产环境中的 MongoDB 数据库。
要开始使用 MongoDB 和 Prisma,请查看我们的从零开始指南或如何添加到现有项目。
什么是事务?
事务是将多个语句组合并隔离起来,作为一个单一操作进行处理的方式。事务不是单独执行每个发送到服务器的命令,而是将命令捆绑在一起,并在与其它请求分离的上下文中执行。
隔离性是事务的重要组成部分。在事务中,执行的语句只能影响事务本身的环境。从事务内部看,语句可以修改数据,结果立即可见。从外部看,在事务提交之前不会进行任何更改,此时事务中的所有操作将一次性可见。
这些特性通过提供原子性和隔离性,帮助数据库实现 ACID 合规性。这些特性共同帮助数据库维护一致性。此外,事务中的更改在提交到非易失性存储之前不会被视为成功,这提供了持久性。
MongoDB 支持多文档 ACID 事务和分布式多文档 ACID 事务。本质上,文档模型允许将相关数据存储在单个文档中。文档模型与原子文档更新相结合,在大多数用例中消除了对事务的需求。然而,在某些情况下,多文档、多集合的 MongoDB 事务是您的最佳选择。
何时在文档模型中使用事务
文档模型本身就解决了事务所针对的许多需求。尽管如此,仍有一些用例即使在文档模型中也突出需要利用事务。
需要事务的应用程序通常是那些在不同方之间交换值的应用程序。一些例子包括“记录系统”或“业务线”应用程序。
多文档事务的优势专为资金流转系统量身定制,例如银行应用程序或支付处理、货物所有权转移的供应链或运输系统,或电子商务。这些示例通常将跨多个流的更改打包在一起,并且需要事务提供的“全有或全无”方法来保证强一致性。
如何使用事务?
MongoDB 提供了两种使用事务的 API。第一种是核心 API,其语法与关系型数据库类似。第二种是回调 API,它是 MongoDB 中使用事务的推荐方法。
两种 API 的比较最好总结在下表中
核心 API | 回调 API |
---|---|
需要显式调用以启动和提交事务 | 启动事务,执行指定操作,并提交(或在出错时中止) |
不会自动包含针对 TransientTransactionError 和 UnknownTransactionCommitResult 的错误处理逻辑,而是允许集成自定义错误处理。 | 自动包含针对 TransientTransactionError 和 UnknownTransactionCommmitResult 的错误处理逻辑。 |
需要将显式逻辑会话传递给 API 以进行特定事务。 | 需要将显式逻辑会话传递给 API 以进行特定事务。 |
如果您正在寻找开始使用 MongoDB 和 Prisma 的方法,请查看我们的从零开始指南或如何添加到现有项目。
创建事务会话
事务通常通过 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()
方法有两个选项,readConcern
和 writeConcern
。您可以在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()
方法,以提交事务并将数据库带到新的一致状态。
session.commitTransaction()
现在,新记录将同时存在于两个实例中:进行事务会话的 MongoDB shell 和事务之外的 MongoDB shell。
中止事务
现在我们已经从启动会话到提交会话,我们可以探讨事务的另一种结果。
此路径开始时相同,仅在提交点发生变化。如果我们想丢弃事务正在进行的更改,则在会话 shell 中使用 abortTransaction()
方法如下:
session.abortTransaction()
此方法取消事务会话并丢弃任何潜在更改。数据库不会产生新的一致状态,而是回滚更改,保持与事务会话首次启动时相同的状态。
结论
在本指南中,我们讨论了什么是事务以及事务在 MongoDB 中最适合的用例。我们还概念性地介绍了 MongoDB shell 中的事务会话过程,以便您了解外部应用程序将如何操作。
事务是关系型数据库的基本需求,对于 MongoDB 等文档模型数据库中的特定用例也需要事务。对于文档模型,一般的建议是,一起访问的数据应该存储在一起。然而,有时情况并非如此,因此了解事务的基础知识很重要。
如果您正在使用 MongoDB,请查看 Prisma 的 MongoDB 连接器!您可以使用 Prisma Client 信心十足地管理生产环境中的 MongoDB 数据库。
要开始使用 MongoDB 和 Prisma,请查看我们的从零开始指南或如何添加到现有项目。
常见问题
是的,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 4.0 及更高版本支持多文档 ACID 事务。