MongoDB 4.0 事务功能 最佳实践

首先

今年六月,MongoDB发布了4.0版本,使得MongoDB也能够使用跨多个文档的事务。现在有很多地方提供了代码示例等内容,所以可能会有人已经尝试过了。

迄今为止,MongoDB曾经在以前的版本中存在问题,也有用户离开,并且遭到了各种指责。从3.0版本开始将存储引擎更改为WiredTiger后,虽然来得晚,但付出了对一致性和可靠性的重视,最终实现了这次的交易。我认为这是一个里程碑。

期待着能够颠覆以往的印象并持续发展,我们将试验交易功能并总结注意事项。

源自

本次使用的源代码已经存储在以下的仓库中。

多份文件交易

我会用Java给出一些代码示例。

ClientSession clientSession = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build());
try {
    clientSession.startTransaction(TransactionOptions.builder().readConcern(ReadConcern.SNAPSHOT).writeConcern(WriteConcern.MAJORITY));
    collection.updateOne(clientSession, eq("name", "satoshi"), combine(set("flag", flagValue), currentDate("lastMofified"));
    collection.updateOne(clientSession, eq("name", "vigyan"), combine(set("flag", flagValue), currentDate("lastModified"));
    // Thread.sleep(10000); // テスト用
    clientSession.commitTransaction();
} catch (MongoException me) {
    clientSession.abortTransaction();
} finally {
    clientSession.close();
}

处理概要

由于交易(トランザクション)与ClientSession相关联,这导致必须使用从3.6版本开始引入的ClientSession。在正常情况下,会执行提交(commit)操作,而在发生异常的情况下,则会在catch语句中通过abortTransaction进行回滚(rollback)。

更新内容是以简单的update形式呈现。与Mongo Shell和JavaScript中的查询方式有很大的不同,不能简单地写入字符串,而需要使用专用的类作为搜索条件的过滤器和Update的操作符,因此可能需要一定的熟悉度。

异常处理

在考虑异常处理时,与Java异常相关的Mongo异常全部继承自RuntimeException的MongoException,这一点需要特别注意。当异常发生时,我们参考了另一篇博客文章中的Java实现,确保在关闭之前一定要发出abortTransaction。

即使commitTransaction失败,驱动程序的功能会在内部自动重试一次(不管retryWrites设置如何)。根据MongoException的内容,有时也可以进行重试,但我们认为只通过驱动程序的一次重试功能就足够了,以保持简单的实现。10

此外,ClientSession实现了AutoCloseable接口,因此也可以使用try-with-resources形式来编写。

读关注 / 写关注 / 因果一致性

在这里,我们将会话设为Causal Consistent。另外,对于事务,我们将读关注点设为snapshot,写关注点设为majority。这样一来,在事务开始之前的操作也将保持Causal Consistency。11

试一试

我会试着去执行。使用Atlas的免费版作为环境是比较方便的选择。也可以在本地搭建12个副本集。

第一次 单次执行

我会尝试进行一次单独的执行。

java -cp mongotransaction-0.0.1-SNAPSHOT.jar;dependency\bson-3.9.1.jar;dependency\mongodb-driver-core-3.9.1.jar;dependency\mongodb-driver-sync-3.9.1.jar com.qiita.kabao.Sample1 "mongodb://<....>/test?replicaSet=rsokano&authSource=admin&retryWrites=true" 0000
Start Session
Start Transaction
{ "_id" : { "$oid" : "5c18d7a46a5db864b14f3a47" }, "name" : "satoshi", "address" : { "country" : "日本", "pref" : "神奈川", "city" : "横浜", "zipcode" : "220-0001" }, "lastModified" : { "$date" : 1545131967068 } }
Commit....
....done
close
end

结果

rsokano:PRIMARY> db.sample1.find().pretty()
{
        "_id" : ObjectId("5c18d7a46a5db864b14f3a47"),
        "name" : "satoshi",
        "address" : {
                "country" : "日本",
                "pref" : "神奈川",
                "city" : "横浜",
                "zipcode" : "220-0001"
        },
        "lastModified" : ISODate("2018-12-18T11:19:00.286Z"),
        "flag" : "0000"
}
{
        "_id" : ObjectId("5c18d7a46a5db864b14f3a49"),
        "name" : "vigyan",
        "address" : {
                "country" : "Australia",
                "state" : "VIC",
                "city" : "Melbourne",
                "street" : "120 Collins Street",
                "postcode" : "3000"
        },
        "lastModified" : ISODate("2018-12-18T11:19:27.111Z")
}
rsokano:PRIMARY>

同时执行第二次

接下来,我将尝试同时执行两个任务。

第一个。运作正常。

java -cp mongotransaction-0.0.1-SNAPSHOT.jar;dependency\bson-3.9.1.jar;dependency\mongodb-driver-core-3.9.1.jar;dependency\mongodb-driver-sync-3.9.1.jar com.qiita.kabao.Sample1 "mongodb://<....>/test?replicaSet=rsokano&authSource=admin&retryWrites=true" 1111
Start Session
Start Transaction
{ "_id" : { "$oid" : "5c18d7a46a5db864b14f3a47" }, "name" : "satoshi", "address" : { "country" : "日本", "pref" : "神奈川", "city" : "横浜", "zipcode" : "220-0001" }, "lastModified" : { "$date" : 1545132857511 } }
Commit....
....done
close
end

第二个。出现了MongoCommandException的WriteConflict。

java -cp mongotransaction-0.0.1-SNAPSHOT.jar;dependency\bson-3.9.1.jar;dependency\mongodb-driver-core-3.9.1.jar;dependency\mongodb-driver-sync-3.9.1.jar com.qiita.kabao.Sample1 "mongodb://<....>/test?replicaSet=rsokano&authSource=admin&retryWrites=true" 2222
Start Session
Start Transaction
MongoException
Abort....
....done
com.mongodb.MongoCommandException: Command failed with error 112 (WriteConflict): 'WriteConflict' on server 192.168.56.102:27017. The full response is { "errorLabels" : ["TransientTransactionError"], "operationTime" : { "$timestamp" : { "t" : 1545132829, "i" : 1 } }, "ok" : 0.0, "errmsg" : "WriteConflict", "code" : 112, "codeName" : "WriteConflict", "$clusterTime" : { "clusterTime" : { "$timestamp" : { "t" : 1545132829, "i" : 1 } }, "signature" : { "hash" : { "$binary" : "AAAAAAAAAAAAAAAAAAAAAAAAAAA=", "$type" : "00" }, "keyId" : { "$numberLong" : "0" }} } }
        at com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:179)
        at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:299)
        at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:255)
        at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:99)
        at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:444)
        at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:72)
        at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:200)
        at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:269)
        at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:131)
        at com.mongodb.operation.MixedBulkWriteOperation.executeCommand(MixedBulkWriteOperation.java:419)
        at com.mongodb.operation.MixedBulkWriteOperation.executeBulkWriteBatch(MixedBulkWriteOperation.java:257)
        at com.mongodb.operation.MixedBulkWriteOperation.access$700(MixedBulkWriteOperation.java:68)
        at com.mongodb.operation.MixedBulkWriteOperation$1.call(MixedBulkWriteOperation.java:201)
        at com.mongodb.operation.MixedBulkWriteOperation$1.call(MixedBulkWriteOperation.java:192)
        at com.mongodb.operation.OperationHelper.withReleasableConnection(OperationHelper.java:424)
        at com.mongodb.operation.MixedBulkWriteOperation.execute(MixedBulkWriteOperation.java:192)
        at com.mongodb.operation.MixedBulkWriteOperation.execute(MixedBulkWriteOperation.java:67)
        at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:193)
        at com.mongodb.client.internal.MongoCollectionImpl.executeSingleWriteRequest(MongoCollectionImpl.java:960)
        at com.mongodb.client.internal.MongoCollectionImpl.executeReplaceOne(MongoCollectionImpl.java:602)
        at com.mongodb.client.internal.MongoCollectionImpl.replaceOne(MongoCollectionImpl.java:597)
        at com.qiita.kabao.Sample1.main(Sample1.java:83)
close
end

结果。只有第一个选项被反映出来。

rsokano:PRIMARY> db.sample1.find().pretty()
{
        "_id" : ObjectId("5c18d7a46a5db864b14f3a47"),
        "name" : "satoshi",
        "address" : {
                "country" : "日本",
                "pref" : "神奈川",
                "city" : "横浜",
                "zipcode" : "220-0001"
        },
        "lastModified" : ISODate("2018-12-18T11:33:50.876Z"),
        "flag" : "1111"
}
{
        "_id" : ObjectId("5c18d7a46a5db864b14f3a49"),
        "name" : "vigyan",
        "address" : {
                "country" : "Australia",
                "state" : "VIC",
                "city" : "Melbourne",
                "street" : "120 Collins Street",
                "postcode" : "3000"
        },
        "lastModified" : ISODate("2018-12-18T11:34:17.563Z")
}
rsokano:PRIMARY>

写冲突

在某个事务中对正在被修改的文档进行更改,如果其他事务也试图对其进行更改,则后续事务将等待锁定,并在等待一定时间后仍未获得锁定时,将产生写冲突错误。

可以使用maxTransactionLockRequestTimeoutMillis参数来指定锁定确认的超时时间,默认为5毫秒。

在MongoDB中,有一个类似于RDB世界中的锁等待和键重复的概念,但需要注意的是,这是自4.0版本以后出现的新错误。

最佳实践 (Zuì jiā

如果使用事务功能,我们需要特别注意的点应该是处理写冲突问题。

然而,在此之前,MongoDB似乎建议不一定要使用事务,而是要考虑设计一个可以在单个文档级别进行处理的数据模型。

    1. 即使使用4.0或更高版本,也不要在任何地方都使用事务。应考虑文档数据模型,以便可以针对单个文档进行处理。

 

    1. 要考虑写冲突。

 

    1. 如果使用事务,请考虑在短时间内完成。如果超过transactionLifetimeLimitSeconds(默认为60秒),事务将被中止。

 

    1. 事务中包含的数据量不要超过16MB。

事务内不可包含DDL操作。在同一命名空间中存在进行中的事务时,要注意DDL操作将被阻塞。

Jepsen报告称,在MongoDB过去的版本中存在数据丢失的可能性。

有关MongoDB 3.4.0-rc3的先前版本,有关Lost update、Dirty Read和Stale Read的问题已经报道。

设计缺陷:MongoDB容错性。

哪些公司已经放弃了MongoDB?他们为什么放弃了?他们转向了什么?

谁退出了MongoDB?为什么?(Quora翻译)

RethinkDB:为什么我们失败了“每次MongoDB发布新版本并受到人们庆祝他们改进的时候,我都感到一丝愤怒。”

MongoDB的多文档ACID事务已经成为GA 3.0以来,在三年以上的时间里,持续在底层实现了基础功能,如存储。

7的日语翻译。

对于JDBC的情况,根据实现的依赖性,在发生异常等情况时,执行close而不执行commit或rollback的行为被推荐为未定义。在MongoDB 4.0中,未明确说明在执行commitTransaction或abortTransaction之前执行close的行为规范,在文档中没有记录。在这里,我们建议在调用close之前执行commitTransaction或abortTransaction,参考JDBC的情况以及上述其他博客文章。值得一提的是,对于Oracle数据库,如果在不执行commit也不执行rollback的情况下执行close,则会发生隐式的commit。

还有支持整个事务的重试功能。

事务选项(读取关注点/写入关注点/读取偏好)/因果一致性/因果一致性和读写关注点。

CosmosDB目前与MongoDB 3.2兼容,3.4的功能处于预览状态。MongoDB 4.0的事务功能不可用,也没有计划。实际上,在针对CosmosDB运行此程序时,会发生以下错误:Exception in thread “main” com.mongodb.MongoClientException: Sessions are not supported by the MongoDB cluster to which this client is connected。

广告
将在 10 秒后关闭
bannerAds