MongoDB 4.0 的多文档 ACID 事务支持详解
(本文与 MongoDB 合作创作。感谢您支持使 SitePoint 成为可能的合作伙伴。)
MongoDB 4.0 新增了对多文档 ACID 事务的支持。但这是否意味着 MongoDB 此前不支持事务?并非如此,MongoDB 一直以来都支持单文档事务。MongoDB 4.0 将这些事务保证扩展到多个文档、多个语句、多个集合和多个数据库。如果没有某种形式的事务数据完整性保证,数据库还有什么用呢?
在深入探讨本文之前,您可以在这里找到所有代码并尝试多文档 ACID 事务。
关键要点
快速入门
步骤 1:启动 MongoDB
在 localhost 的 27017 端口上启动版本至少为 4.0.0 的单节点 MongoDB ReplicaSet。
如果您使用 Docker:
start-mongo.sh
。stop-mongo.sh
。connect-mongo.sh
。如果您更喜欢手动启动 mongod:
mkdir /tmp/data && mongod --dbpath /tmp/data --replSet rs
mongo --eval 'rs.initiate()'
步骤 2:启动 Java
此演示包含两个主程序:ChangeStreams.java
和 Transactions.java
。
您需要两个 shell 来运行它们。
如果您使用 Docker:
第一个 shell:
./compile-docker.sh ./change-streams-docker.sh
第二个 shell:
./transactions-docker.sh
如果您不使用 Docker,则需要安装 Maven 3.5.X 和 JDK 10(或 JDK 8 最低版本,但您需要更新 pom.xml
中的 Java 版本):
第一个 shell:
./compile-docker.sh ./change-streams-docker.sh
第二个 shell:
./transactions-docker.sh
让我们将现有的单文档事务与 MongoDB 4.0 的 ACID 兼容多文档事务进行比较,并了解如何使用 Java 利用此新功能。
MongoDB 4.0 之前的版本
即使在 MongoDB 3.6 和更早版本中,每个写入操作都表示为作用域在存储层单个文档级别的交易。由于文档模型将相关数据组合在一起,否则这些数据会在表格模式中跨不同的父子表建模,因此 MongoDB 的原子单文档操作提供满足大多数应用程序数据完整性需求的事务语义。
每个修改多个文档的典型写入操作实际上都会发生在几个独立的事务中:每个文档一个事务。
让我们以一个非常简单的库存管理应用程序为例。
首先,我需要一个 MongoDB Replica Set,因此请按照上面给出的说明启动 MongoDB。
现在让我们将以下文档插入到产品集合中:
./compile.sh ./change-streams.sh
假设正在进行促销活动,我们想为客户提供所有产品的 20% 折扣。
但在应用此折扣之前,我们想使用 Change Streams 监控这些操作在 MongoDB 中发生的时间。
在 Mongo Shell 中执行以下操作:
./transactions.sh
将此 shell 保留在一边,打开另一个 Mongo Shell 并应用折扣:
MongoDB Enterprise rs:PRIMARY> db.product.insertMany([ { "_id" : "beer", "price" : NumberDecimal("3.75"), "stock" : NumberInt(5) }, { "_id" : "wine", "price" : NumberDecimal("7.5"), "stock" : NumberInt(3) } ])
如您所见,两个文档都使用单个命令行进行了更新,但并非在一个事务中。以下是我们在 Change Stream shell 中看到的:
cursor = db.product.watch([{$match: {operationType: "update"}}]); while (!cursor.isExhausted()) { if (cursor.hasNext()) { print(tojson(cursor.next())); } }
如您所见,这两个操作的集群时间(请参见 clusterTime
密钥)不同:这些操作发生在同一秒内,但时间戳的计数器已递增 1。
因此,这里每个文档一次更新一个,即使这发生得非常快,其他人也可能在更新运行时读取文档,并且只能看到其中一种产品有折扣。
大多数情况下,这是您可以在 MongoDB 数据库中容忍的事情,因为我们尽可能尝试将紧密关联或相关的数据嵌入到同一个文档中。因此,对同一文档的两次更新在一个事务中发生:
PRIMARY> db.product.updateMany({}, {$mul: {price:0.8}}) { "acknowledged" : true, "matchedCount" : 2, "modifiedCount" : 2 } PRIMARY> db.product.find().pretty() { "_id" : "beer", "price" : NumberDecimal("3.00000000000000000"), "stock" : 5 } { "_id" : "wine", "price" : NumberDecimal("6.0000000000000000"), "stock" : 3 }
但是,有时您无法将所有相关数据建模到单个文档中,并且有很多理由选择不嵌入文档。
使用多文档 ACID 事务的 MongoDB 4.0
MongoDB 中的多文档 ACID 事务与您可能已经从传统关系数据库中了解到的非常相似。
MongoDB 的事务是一组相关的对话式操作,必须以全有或全无的执行方式原子地提交或完全回滚。
事务用于确保操作即使跨多个集合或数据库也是原子的。因此,使用快照隔离读取,另一个用户只能看到所有操作或没有操作。
现在让我们向我们的示例中添加一个购物车。
在此示例中,需要 2 个集合,因为我们正在处理 2 个不同的业务实体:库存管理和每个客户端在购物期间可以创建的购物车。这些集合中每个文档的生命周期都不同。
产品集合中的文档表示我正在销售的商品。这包含产品的当前价格和当前库存。我创建了一个 POJO 来表示它:Product.java
。
./compile-docker.sh ./change-streams-docker.sh
当客户端将其第一个商品添加到购物车时,就会创建购物车,当客户端继续结账或离开网站时,就会删除购物车。我创建了一个 POJO 来表示它:Cart.java
。
./transactions-docker.sh
这里的挑战在于我不能卖出超过我拥有的东西:如果我有 5 瓶啤酒要卖,我不能在不同客户端的购物车中拥有超过 5 瓶啤酒。
为了确保这一点,我必须确保创建或更新客户端购物车的操作与库存更新是原子的。这就是多文档事务发挥作用的地方。如果有人试图购买我没有库存的东西,则事务必须失败。我将在产品库存上添加一个约束:
./compile.sh ./change-streams.sh
(请注意,这已包含在 Java 代码中。)
为了监控我们的示例,我们将使用在 MongoDB 3.6 中引入的 MongoDB Change Streams。
在这个名为 ChangeStreams.java
的进程的每个线程中,我将监控 2 个集合中的一个,并打印每个操作及其关联的集群时间。
...(以下内容需要根据提供的代码补充Java代码片段和解释,篇幅过长,此处省略)...
后续步骤
感谢您抽出时间阅读我的文章——我希望您觉得它有用且有趣。提醒一下,所有代码都可以在此 Github 存储库中找到,供您试验。
如果您正在寻找一个非常简单的入门方法,您可以在云端的 MongoDB Atlas 数据库服务中只需 5 次点击即可完成。
此外,多文档 ACID 事务并非 MongoDB 4.0 中的唯一新功能,因此您可以随意查看我们在 MongoDB University 上的免费课程 M040:MongoDB 4.0 中的新功能和工具以及我们关于 MongoDB 4.0 新功能的指南,您可以在其中了解有关原生类型转换、新的可视化和分析工具以及 Kubernetes 集成的更多信息。
...(以下内容为FAQ,篇幅过长,此处省略)...
以上是Java和MongoDB 4.0支持多文件酸交易的详细内容。更多信息请关注PHP中文网其他相关文章!