MongoDB 4.0 adds support for multi-document ACID transactions.
MongoDB 4.0添加了对多文档ACID事务的支持。
But wait... Does that mean MongoDB did not support transactions until now? No, actually MongoDB has always supported transactions in the form of single document transactions. MongoDB 4.0 extends these transactional guarantees across multiple documents, multiple statements, multiple collections, and multiple databases. What good would a database be without any form of transactional data integrity guarantee?
但是,等等...这是否意味着MongoDB直到现在还不支持事务? 不,实际上,MongoDB始终以单文档交易的形式支持交易。 MongoDB 4.0将这些事务保证扩展到多个文档,多个语句,多个集合和多个数据库。 没有任何形式的交易数据完整性保证的数据库有什么好处?
Before we dive into this blog post, you can find all the code and try multi-document ACID transactions here.
在深入探讨此博客文章之前,您可以在此处找到所有代码并尝试进行多文档ACID事务。
Start a single node MongoDB ReplicaSet in version 4.0.0 minimum on localhost, port 27017.
在本地主机端口27017上至少启动4.0.0版本的单节点MongoDB ReplicaSet。
If you use Docker:
如果您使用Docker:
start-mongo.sh
. 您可以使用start-mongo.sh
。 stop-mongo.sh
. 完成后,可以使用stop-mongo.sh
。 connect-mongo.sh
. 如果要使用Mongo Shell连接到MongoDB,可以使用connect-mongo.sh
。 If you prefer to start mongod manually:
如果您想手动启动mongod:
mkdir /tmp/data && mongod --dbpath /tmp/data --replSet rs
mkdir /tmp/data && mongod --dbpath /tmp/data --replSet rs
mongo --eval 'rs.initiate()'
mongo --eval 'rs.initiate()'
This demo contains two main programs: ChangeStreams.java
and Transactions.java
.
该演示包含两个主要程序: ChangeStreams.java
和Transactions.java
。
You need two shells to run them.
您需要两个shell来运行它们。
If you use Docker:
如果您使用Docker:
First shell:
第一壳:
./compile-docker.sh
./change-streams-docker.sh
Second shell:
第二层:
./transactions-docker.sh
If you do not use Docker, you will need to install Maven 3.5.X and a JDK 10 (or JDK 8 minimum but you will need to update the Java versions in the pom.xml):
如果您不使用Docker,则需要安装Maven 3.5.X和JDK 10(或最低JDK 8,但需要更新pom.xml中的Java版本):
First shell:
第一壳:
./compile.sh
./change-streams.sh
Second shell:
第二层:
./transactions.sh
Let’s compare our existing single document transactions with MongoDB 4.0’s ACID compliant multi-document transactions and see how we can leverage this new feature with Java.
让我们将现有的单文档事务与MongoDB 4.0的ACID兼容多文档事务进行比较,看看如何在Java中利用这一新功能。
Even in MongoDB 3.6 and earlier, every write operation is represented as a transaction scoped to the level of an individual document in the storage layer. Because the document model brings together related data that would otherwise be modeled across separate parent-child tables in a tabular schema, MongoDB’s atomic single-document operations provide transaction semantics that meet the data integrity needs of the majority of applications.
即使在MongoDB 3.6和更早版本中,每个写操作都表示为一个事务,范围限定在存储层中单个文档的级别 。 由于文档模型将相关数据汇总在一起,否则它们将以表格模式在单独的父子表之间进行建模,因此MongoDB的原子单文档操作提供了满足大多数应用程序的数据完整性需求的事务语义。
Every typical write operation modifying multiple documents actually happens in several independent transactions: one for each document.
每个修改多个文档的典型写操作实际上都发生在几个独立的事务中:每个文档一个。
Let’s take an example with a very simple stock management application.
让我们以一个非常简单的库存管理应用程序为例。
First of all, I need a MongoDB Replica Set so please follow the instructions given above to start MongoDB.
首先,我需要一个MongoDB副本集,因此请按照上面给出的说明启动MongoDB。
Now let’s insert the following documents into a product
collection:
现在,将以下文档插入product
集合中:
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) }
])
Let’s imagine there is a sale on and we want to offer our customers a 20% discount on all our products.
假设有一个销售,我们想为我们的客户提供所有产品20%的折扣。
But before applying this discount, we want to monitor when these operations are happening in MongoDB with Change Streams.
但是在应用此折扣之前,我们想使用更改流监视在MongoDB中何时进行这些操作。
Execute the following in Mongo Shell:
在Mongo Shell中执行以下操作:
cursor= db.product.watch([{$match: {operationType: "update"}}]);
while (!cursor.isExhausted()) {
if (cursor.hasNext()) {
print(tojson(cursor.next()));
}
}
Keep this shell on the side, open another Mongo Shell and apply the discount:
将此壳放在一边,打开另一个Mongo Shell并应用折扣:
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
}
As you can see, both documents were updated with a single command line but not in a single transaction. Here is what we can see in the Change Stream shell:
如您所见,两个文档都是使用单个命令行更新的,而不是使用单个事务更新的。 这是我们在Change Stream Shell中可以看到的内容:
{
"_id" : {
"_data" : "825B4637290000000129295A1004374DC58C611E4C8DA4E5EDE9CF309AC5463C5F6964003C62656572000004"
},
"operationType" : "update",
"clusterTime" : Timestamp(1531328297, 1),
"ns" : {
"db" : "test",
"coll" : "product"
},
"documentKey" : {
"_id" : "beer"
},
"updateDescription" : {
"updatedFields" : {
"price" : NumberDecimal("3.00000000000000000")
},
"removedFields" : [ ]
}
}
{
"_id" : {
"_data" : "825B4637290000000229295A1004374DC58C611E4C8DA4E5EDE9CF309AC5463C5F6964003C77696E65000004"
},
"operationType" : "update",
"clusterTime" : Timestamp(1531328297, 2),
"ns" : {
"db" : "test",
"coll" : "product"
},
"documentKey" : {
"_id" : "wine"
},
"updateDescription" : {
"updatedFields" : {
"price" : NumberDecimal("6.0000000000000000")
},
"removedFields" : [ ]
}
}
As you can see the cluster times (see the clusterTime
key) of the two operations are different: the operations occurred during the same second but the counter of the timestamp has been incremented by one.
如您所见,这两个操作的集群时间(请参阅clusterTime
键)是不同的:这些操作在同一秒内发生,但时间戳记的计数器增加了1。
Thus here each document is updated one at a time and even if this happens really fast, someone else could read the documents while the update is running and see only one of the two products with the discount.
因此,这里每个文档一次更新一次,即使这种更新发生得非常快,其他人也可以在更新运行时阅读这些文档,并且只能看到两个打折产品中的一个。
Most of the time, it is something you can tolerate in your MongoDB database because, as much as possible, we try to embed tightly linked, or related data in the same document. As a result, two updates on the same document happen within a single transaction :
在大多数情况下,您可以在MongoDB数据库中容忍这种情况,因为我们尽可能地将紧密链接或相关数据嵌入到同一文档中。 结果,同一文档中的两次更新在一次事务中发生:
PRIMARY> db.product.update({_id: "wine"},{$inc: {stock:1}, $set: {description : "It’s the best wine on Earth"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
PRIMARY> db.product.findOne({_id: "wine"})
{
"_id" : "wine",
"price" : NumberDecimal("6.0000000000000000"),
"stock" : 4,
"description" : "It’s the best wine on Earth"
}
However, sometimes, you cannot model all of your related data in a single document, and there are a lot of valid reasons for choosing not to embed documents.
但是,有时候,您无法在单个文档中对所有相关数据建模,并且有很多正当理由选择不嵌入文档。
Multi-document ACID transactions in MongoDB are very similar to what you probably already know from traditional relational databases.
MongoDB中的多文档ACID事务与您从传统关系数据库中可能已经知道的非常相似。
MongoDB’s transactions are a conversational set of related operations that must atomically commit or fully rollback with all-or-nothing execution.
MongoDB的事务是一组对话式相关操作,这些操作必须以原子方式提交或完全回滚,执行全部或全部不执行。
Transactions are used to make sure operations are atomic even across multiple collections or databases. Thus, with snapshot isolation reads, another user can only see all the operations or none of them.
事务用于确保操作即使在多个集合或数据库中也是原子的。 因此,使用快照隔离读取,另一个用户只能看到所有操作,也可能看不到任何操作。
Let’s now add a shopping cart to our example.
现在,将购物车添加到示例中。
For this example, 2 collections are required because we are dealing with 2 different business entities: the stock management and the shopping cart each client can create during shopping. The lifecycle of each document in these collections is different.
对于此示例,需要2个集合,因为我们正在处理2个不同的业务实体:库存管理和每个客户在购物期间可以创建的购物车。 这些集合中每个文档的生命周期都不同。
A document in the product collection represents an item I’m selling. This contains the current price of the product and the current stock. I created a POJO to represent it : Product.java.
产品集合中的文档代表我要销售的商品。 这包含产品的当前价格和当前库存。 我创建了一个POJO来表示它:Product.java。
{ "_id" : "beer", "price" : NumberDecimal("3"), "stock" : NumberInt(5) }
A shopping cart is created when a client adds its first item in the cart and is removed when the client proceeds to checkout or leaves the website. I created a POJO to represent it : Cart.java.
当客户将其第一项添加到购物车中时,将创建一个购物车;当客户进行结帐或离开网站时,将其删除。 我创建了一个POJO来表示它:Cart.java。
{
"_id" : "Alice",
"items" : [
{
"price" : NumberDecimal("3"),
"productId" : "beer",
"quantity" : NumberInt(2)
}
]
}
The challenge here resides in the fact that I cannot sell more than I possess: if I have 5 beers to sell, I cannot have more than 5 beers distributed across the different client carts.
这里的挑战在于我不能卖出比我拥有的更多的啤酒:如果我要卖出5杯啤酒,那么在不同的客户手推车上分配的啤酒就不能超过5杯。
To ensure that, I have to make sure that the operation creating or updating the client cart is atomic with the stock update. That’s where the multi-document transaction comes into play. The transaction must fail in the case someone tries to buy something I do not have in my stock. I will add a constraint on the product stock:
为了确保这一点,我必须确保创建或更新客户购物车的操作与库存更新是原子的。 这就是多文档交易起作用的地方。 如果有人试图购买我的库存中没有的东西,则交易必须失败。 我将对产品库存添加一个约束:
db.createCollection("product", {
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "_id", "price", "stock" ],
properties: {
_id: {
bsonType: "string",
description: "must be a string and is required"
},
price: {
bsonType: "decimal",
minimum: 0,
description: "must be a positive decimal and is required"
},
stock: {
bsonType: "int",
minimum: 0,
description: "must be a positive integer and is required"
}
}
}
}
})
Node that this is already included in the Java code.
Java代码中已包含此节点。
To monitor our example, we are going to use MongoDB Change Streams that were introduced in MongoDB 3.6.
为了监控我们的示例,我们将使用MongoDB 3.6中引入的MongoDB变更流。
In each of the threads of this process called ChangeStreams.java
, I am going to monitor one of the 2 collections and print each operation with its associated cluster time.
在这个名为ChangeStreams.java
过程的每个线程中,我将监视2个集合之一,并打印每个操作及其关联的群集时间。
// package and imports
public class ChangeStreams {
private static final Bson filterUpdate = Filters.eq("operationType", "update");
private static final Bson filterInsertUpdate = Filters.in("operationType", "insert", "update");
private static final String jsonSchema = "{ $jsonSchema: { bsonType: \"object\", required: [ \"_id\", \"price\", \"stock\" ], properties: { _id: { bsonType: \"string\", description: \"must be a string and is required\" }, price: { bsonType: \"decimal\", minimum: 0, description: \"must be a positive decimal and is required\" }, stock: { bsonType: \"int\", minimum: 0, description: \"must be a positive integer and is required\" } } } } ";
public static void main(String[] args) {
MongoDatabase db = initMongoDB(args[0]);
MongoCollection<Cart> cartCollection = db.getCollection("cart", Cart.class);
MongoCollection<Product> productCollection = db.getCollection("product", Product.class);
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> watchChangeStream(productCollection, filterUpdate));
executor.submit(() -> watchChangeStream(cartCollection, filterInsertUpdate));
ScheduledExecutorService scheduled = Executors.newSingleThreadScheduledExecutor();
scheduled.scheduleWithFixedDelay(System.out::println, 0, 1, TimeUnit.SECONDS);
}
private static void watchChangeStream(MongoCollection<?> collection, Bson filter) {
System.out.println("Watching " + collection.getNamespace());
List<Bson> pipeline = Collections.singletonList(Aggregates.match(filter));
collection.watch(pipeline)
.fullDocument(FullDocument.UPDATE_LOOKUP)
.forEach((Consumer<ChangeStreamDocument<?>>) doc -> System.out.println(
doc.getClusterTime() + " => " + doc.getFullDocument()));
}
private static MongoDatabase initMongoDB(String mongodbURI) {
getLogger("org.mongodb.driver").setLevel(Level.SEVERE);
CodecRegistry providers = fromProviders(PojoCodecProvider.builder().register("com.mongodb.models").build());
CodecRegistry codecRegistry = fromRegistries(MongoClient.getDefaultCodecRegistry(), providers);
MongoClientOptions.Builder options = new MongoClientOptions.Builder().codecRegistry(codecRegistry);
MongoClientURI uri = new MongoClientURI(mongodbURI, options);
MongoClient client = new MongoClient(uri);
MongoDatabase db = client.getDatabase("test");
db.drop();
db.createCollection("cart");
db.createCollection("product", productJsonSchemaValidator());
return db;
}
private static CreateCollectionOptions productJsonSchemaValidator() {
return new CreateCollectionOptions().validationOptions(
new ValidationOptions().validationAction(ValidationAction.ERROR).validator(BsonDocument.parse(jsonSchema)));
}
}
In this example we have 5 beers to sell. Alice wants to buy 2 beers but we are not going to use the new MongoDB 4.0 multi-document transactions for this. We will observe in the change streams two operations : one creating the cart and one updating the stock at 2 different cluster times.
在此示例中,我们有5杯啤酒要出售。 爱丽丝想买2杯啤酒,但是我们不会为此使用新的MongoDB 4.0多文档交易。 我们将在更改流中观察到两种操作:一种创建购物车,另一种在2个不同的群集时间更新库存。
Then Alice adds 2 more beers in her cart and we are going to use a transaction this time. The result in the change stream will be 2 operations happening at the same cluster time.
然后,爱丽丝在她的购物车中又添加了2杯啤酒,这次我们将使用一笔交易。 变更流中的结果将是在同一集群时间发生2次操作。
Finally, she will try to order 2 extra beers but the jsonSchema validator will fail the product update and result in a rollback. We will not see anything in the change stream. Here is the Transaction.java
source code:
最后,她将尝试订购2杯额外的啤酒,但jsonSchema验证程序将使产品更新失败并导致回滚。 我们不会在变更流中看到任何东西。 这是Transaction.java
源代码:
// package and import
public class Transactions {
private static MongoClient client;
private static MongoCollection<Cart> cartCollection;
private static MongoCollection<Product> productCollection;
private final BigDecimal BEER_PRICE = BigDecimal.valueOf(3);
private final String BEER_ID = "beer";
private final Bson stockUpdate = inc("stock", -2);
private final Bson filterId = eq("_id", BEER_ID);
private final Bson filterAlice = eq("_id", "Alice");
private final Bson matchBeer = elemMatch("items", eq("productId", "beer"));
private final Bson incrementBeers = inc("items.$.quantity", 2);
public static void main(String[] args) {
initMongoDB(args[0]);
new Transactions().demo();
}
private static void initMongoDB(String mongodbURI) {
getLogger("org.mongodb.driver").setLevel(Level.SEVERE);
CodecRegistry codecRegistry = fromRegistries(MongoClient.getDefaultCodecRegistry(), fromProviders(
PojoCodecProvider.builder().register("com.mongodb.models").build()));
MongoClientOptions.Builder options = new MongoClientOptions.Builder().codecRegistry(codecRegistry);
MongoClientURI uri = new MongoClientURI(mongodbURI, options);
client = new MongoClient(uri);
MongoDatabase db = client.getDatabase("test");
cartCollection = db.getCollection("cart", Cart.class);
productCollection = db.getCollection("product", Product.class);
}
private void demo() {
clearCollections();
insertProductBeer();
printDatabaseState();
System.out.println("######### NO TRANSACTION #########");
System.out.println("Alice wants 2 beers.");
System.out.println("We have to create a cart in the 'cart' collection and update the stock in the 'product' collection.");
System.out.println("The 2 actions are correlated but can not be executed on the same cluster time.");
System.out.println("Any error blocking one operation could result in stock error or beer sale we don't own.");
System.out.println("---------------------------------------------------------------------------");
aliceWantsTwoBeers();
sleep();
removingBeersFromStock();
System.out.println("####################################\n");
printDatabaseState();
sleep();
System.out.println("\n######### WITH TRANSACTION #########");
System.out.println("Alice wants 2 extra beers.");
System.out.println("Now we can update the 2 collections simultaneously.");
System.out.println("The 2 operations only happen when the transaction is committed.");
System.out.println("---------------------------------------------------------------------------");
aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback();
sleep();
System.out.println("\n######### WITH TRANSACTION #########");
System.out.println("Alice wants 2 extra beers.");
System.out.println("This time we do not have enough beers in stock so the transaction will rollback.");
System.out.println("---------------------------------------------------------------------------");
aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback();
client.close();
}
private void aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback() {
ClientSession session = client.startSession();
try {
session.startTransaction(TransactionOptions.builder().writeConcern(WriteConcern.MAJORITY).build());
aliceWantsTwoExtraBeers(session);
sleep();
removingBeerFromStock(session);
session.commitTransaction();
} catch (MongoCommandException e) {
session.abortTransaction();
System.out.println("####### ROLLBACK TRANSACTION #######");
} finally {
session.close();
System.out.println("####################################\n");
printDatabaseState();
}
}
private void removingBeersFromStock() {
System.out.println("Trying to update beer stock : -2 beers.");
try {
productCollection.updateOne(filterId, stockUpdate);
} catch (MongoCommandException e) {
System.out.println("##### MongoCommandException #####");
System.out.println("##### STOCK CANNOT BE NEGATIVE #####");
throw e;
}
}
private void removingBeerFromStock(ClientSession session) {
System.out.println("Trying to update beer stock : -2 beers.");
try {
productCollection.updateOne(session, filterId, stockUpdate);
} catch (MongoCommandException e) {
System.out.println("##### MongoCommandException #####");
System.out.println("##### STOCK CANNOT BE NEGATIVE #####");
throw e;
}
}
private void aliceWantsTwoBeers() {
System.out.println("Alice adds 2 beers in her cart.");
cartCollection.insertOne(new Cart("Alice", Collections.singletonList(new Cart.Item(BEER_ID, 2, BEER_PRICE))));
}
private void aliceWantsTwoExtraBeers(ClientSession session) {
System.out.println("Updating Alice cart : adding 2 beers.");
cartCollection.updateOne(session, and(filterAlice, matchBeer), incrementBeers);
}
private void insertProductBeer() {
productCollection.insertOne(new Product(BEER_ID, 5, BEER_PRICE));
}
private void clearCollections() {
productCollection.deleteMany(new BsonDocument());
cartCollection.deleteMany(new BsonDocument());
}
private void printDatabaseState() {
System.out.println("Database state:");
printProducts(productCollection.find().into(new ArrayList<>()));
printCarts(cartCollection.find().into(new ArrayList<>()));
System.out.println();
}
private void printProducts(List<Product> products) {
products.forEach(System.out::println);
}
private void printCarts(List<Cart> carts) {
if (carts.isEmpty())
System.out.println("No carts...");
else
carts.forEach(System.out::println);
}
private void sleep() {
System.out.println("Sleeping 3 seconds...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.err.println("Oups...");
e.printStackTrace();
}
}
}
Here is the console of the Change Stream :
这是变更流的控制台:
$ ./change-streams.sh
Watching test.cart
Watching test.product
Timestamp{value=6570052721557110786, seconds=1529709604, inc=2} => Cart{id='Alice', items=[Item{productId=beer, quantity=2, price=3}]}
Timestamp{value=6570052734442012673, seconds=1529709607, inc=1} => Product{id='beer', stock=3, price=3}
Timestamp{value=6570052764506783745, seconds=1529709614, inc=1} => Product{id='beer', stock=1, price=3}
Timestamp{value=6570052764506783745, seconds=1529709614, inc=1} => Cart{id='Alice', items=[Item{productId=beer, quantity=4, price=3}]}
As you can see here, we only get four operations because the two last operations were never committed to the database, and therefore the change stream has nothing to show.
如您在这里看到的,我们仅获得四个操作,因为最后两个操作从未提交给数据库,因此更改流没有任何显示。
You can also note that the two first cluster times are different because we did not use a transaction for the two first operations, and the two last operations share the same cluster time because we used the new MongoDB 4.0 multi-document transaction system, and thus they are atomic.
您还可以注意到,两个第一个群集时间是不同的,因为我们没有为两个第一个操作使用事务,而两个后一个操作共享相同的群集时间,因为我们使用了新的MongoDB 4.0多文档事务系统,因此他们是原子的。
Here is the console of the Transaction java process that sum up everything I said earlier.
这是Transaction Java流程的控制台,总结了我之前说过的所有内容。
$ ./transactions.sh
Database state:
Product{id='beer', stock=5, price=3}
No carts...
######### NO TRANSACTION #########
Alice wants 2 beers.
We have to create a cart in the 'cart' collection and update the stock in the 'product' collection.
The 2 actions are correlated but can not be executed on the same cluster time.
Any error blocking one operation could result in stock error or a sale of beer that we can’t fulfill as we have no stock.
---------------------------------------------------------------------------
Alice adds 2 beers in her cart.
Sleeping 3 seconds...
Trying to update beer stock : -2 beers.
####################################
Database state:
Product{id='beer', stock=3, price=3}
Cart{id='Alice', items=[Item{productId=beer, quantity=2, price=3}]}
Sleeping 3 seconds...
######### WITH TRANSACTION #########
Alice wants 2 extra beers.
Now we can update the 2 collections simultaneously.
The 2 operations only happen when the transaction is committed.
---------------------------------------------------------------------------
Updating Alice cart : adding 2 beers.
Sleeping 3 seconds...
Trying to update beer stock : -2 beers.
####################################
Database state:
Product{id='beer', stock=1, price=3}
Cart{id='Alice', items=[Item{productId=beer, quantity=4, price=3}]}
Sleeping 3 seconds...
######### WITH TRANSACTION #########
Alice wants 2 extra beers.
This time we do not have enough beers in stock so the transaction will rollback.
---------------------------------------------------------------------------
Updating Alice cart : adding 2 beers.
Sleeping 3 seconds...
Trying to update beer stock : -2 beers.
##### MongoCommandException #####
##### STOCK CANNOT BE NEGATIVE #####
####### ROLLBACK TRANSACTION #######
####################################
Database state:
Product{id='beer', stock=1, price=3}
Cart{id='Alice', items=[Item{productId=beer, quantity=4, price=3}]}
Thanks for taking the time to read my post - I hope you found it useful and interesting. As a reminder, all the code is available on this Github repository for you to experiment.
感谢您抽出宝贵的时间阅读我的帖子-希望您觉得它有用且有趣。 提醒一下,该Github存储库中提供了所有代码,供您进行试验。
If you are looking for a very simple way to get started with MongoDB, you can do that in just 5 clicks on our MongoDB Atlas database service in the cloud.
如果您正在寻找一种非常简单的MongoDB入门方法,那么只需在Cloud中的MongoDB Atlas数据库服务上单击5次即可实现。
Also, multi-document ACID transactions is not the only new feature in MongoDB 4.0, so feel free to take a look at our free course on MongoDB University M040: New Features and Tools in MongoDB 4.0 and our guide to what’s new in MongoDB 4.0 where you can learn more about native type conversions, new visualization and analytics tools, and Kubernetes integration.
此外,多文档ACID事务并不是MongoDB 4.0中的唯一新功能,因此,请随意阅读我们有关MongoDB大学M040的免费课程:MongoDB 4.0中的新功能和工具,以及有关MongoDB 4.0中新功能的指南。您可以了解有关本机类型转换,新的可视化和分析工具以及Kubernetes集成的更多信息。
翻译自: https://scotch.io/tutorials/java-and-mongodb-40-support-for-multi-document-acid-transactions579