简介
事务是数据库中处理的逻辑组,它封装了一个或多个操作,例如跨多个文档的读取和/或写入。数据库能够将这组操作解释并作为一个有凝聚力的单元来执行,而不是以单独的语句进行。这有助于确保数据集在许多密切相关的语句中的一致性。
在本指南中,我们将首先讨论什么是事务,何时在文档模型中使用它们,以及它们在 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 是原子性、一致性、隔离性和持久性的首字母缩写。所有这些属性都与保持数据库处于有效状态有关。
由于事务是一组同时执行的数据库操作,因此在发生意外错误时确保数据库有效性至关重要。符合 ACID 的事务可以防止无效的数据库状态。
MongoDB 事务存在于会话中。您使用 startSession() 创建一个会话,然后使用 session.startTransaction() 来暂存您的事务操作以进行提交。
要有意回滚这些更改,您可以使用 session.abortTransaction() 方法来丢弃在会话中启动的操作。这必须在 session.commitTransaction() 方法之前进行。
在使用 MongoDB 时,有几个事务最佳实践需要考虑。您需要确保优化以将事务运行时保持在启动后 60 秒内,因为 MongoDB 会自动中止任何更长的事务。
事务中的操作数量不应超过 1,000 个文档。此外,由于事务中发生多个操作,因此选择适当的读写关注点很重要。MongoDB 提供了一份全面的最佳实践指南,涵盖了这一点以及更多内容。
是的,MongoDB 4.0 及更高版本支持多文档 ACID 事务。

