原创陈小生网易游戏运维平台
陈小生
网易游戏运维工程师,目前主要负责数据库相关的运维工作。
MongoDB 4.0 引入了多文档事务的支持,不过目前仅限于单个复本集(replica sets) 的支持,预计 4.2 可以看到对 Sharding 架构的分布式事务支援。在 4.0 中,MongoDB 为多文档事务提供了 "all-or-nothing" 的原子语义保证,你可以在不同的操作、不同的 documents、不同的 collections,甚至不同的 databases 之间使用多文档事务:
事务成功提交之前,所有的数据变更在事务之外不可见;
事务中的任何一项操作失败后,所有的变更会被丢弃,事务回滚;
支持事务,需要保证使用的是 MongoDB 4.0 或以上版本,并且 featureCompatibilityVersion(https://docs.mongodb.com/manual/reference/command/setFeatureCompatibilityVersion/#dbcmd.setFeatureCompatibilityVersion)需要设置为 4.0。从旧版本升级上来的复本集群可能为 3.6,需要特别注意。可以通过如下命令检查:
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )
只有 wiredTiger 引擎支持事务。同时请留意,4.0 已经标记 MMAPv1 为 Deprecated,并且将会在未来的某一版本移除对 MMAPv1 引擎的支持;
Mongo Shell 事务示例
mongodb40:PRIMARY> session = db.getMongo().startSession()
session { "id" : UUID("3bfd3739-caed-4c06-8e61-bfe405658166") }
mongodb40:PRIMARY> trans = session.getDatabase('chenxs').trans
chenxs.trans
mongodb40:PRIMARY> session.startTransaction()
mongodb40:PRIMARY> trans.insert({"a": 3})
WriteResult({ "nInserted" : 1 })
mongodb40:PRIMARY> db.trans.find({a: 3})
mongodb40:PRIMARY> trans.find({a: 3})
{ "_id" : ObjectId("5b5e8579b54570c9fb04ed4c"), "a" : 3 }
mongodb40:PRIMARY> session.abortTransaction()
mongodb40:PRIMARY> trans.find({a: 3})
mongodb40:PRIMARY> session.startTransaction()
mongodb40:PRIMARY> trans.insert({a: 3})
WriteResult({ "nInserted" : 1 })
mongodb40:PRIMARY> db.trans.find({a: 3})
mongodb40:PRIMARY> session.commitTransaction()
mongodb40:PRIMARY> db.trans.find({a: 3})
{ "_id" : ObjectId("5b5e85c3b54570c9fb04ed4d"), "a" : 3 }
mongodb40:PRIMARY>
上面的示例分别演示了一个 abortTransaction
和 commitTransaction
的简单示例,其中 trans
和 db.trans
为对同一个 collection chenxs.trans
的操作,我们可以发现:
所有事务都是 session 相关的,否则相应的操作会被认为是事务之外的操作。如 trans.insert({"a": 3})
属于相应 session 事务中的操作,db.trans.find({a: 3})
则属于事务外的操作,因此查不到事务中的提交;
事务回滚后,事务内、事务外均查不到相应的变更,即所有变更被丢弃;
事务成功提交后,外部 session 可以查到提交后的变更结果;
session
session 是在 MongoDB 3.6 版本中引入的,session 本质上可以理解为一个「上下文」,为 3.6 版本提供了「因果一致性」和可重试写入,在 4.0 版本中,session 做为事务的基础而存在,我们可以认为,session 的引入就是为多文档事务做准备。
transaction
在 mongo shell 中,
我们使用 Session.startTransAction()
开始一个事务。
session.startTransaction( {
readConcern: { level: },
writeConcern: { w: , j: , wtimeout: }
} );
该函数主要包括 readConcern
和 writeConcern
两个可选配置项。
readConcern
多文档事务支持 local、majority、snapshot 三个 read concern 设置,其中 snapshot 主要是在 majority 的基础上实现了对因果一致性的保证;请留意:
readConcern 的设置是事务级别的,不能对事务内的操作单独设置 readConcern;
snapshot 设置只在多文档事务中支持,如:session 并不支持设置 readConcern 为 snapshot;
mongodb40:PRIMARY> session = db.getMongo().startSession({readConcern: {"level": "snapshot"}})
session { "id" : UUID("535f02bf-17ae-48c8-8cb9-cdf89b7c48bc") }
mongodb40:PRIMARY> session.getDatabase("chenxs")['trans'].find()
Error: error: {
"operationTime" : Timestamp(1532930368, 1),
"ok" : 0,
"errmsg" : "readConcern level snapshot is only valid in multi-statement transactions",
"code" : 72,
"codeName" : "InvalidOptions",
"$clusterTime" : {
"clusterTime" : Timestamp(1532930368, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
mongodb40:PRIMARY>
如果在多文档事务中配置了 readConcern,则 readPreference 只允许设置为 Primary;
writeConcern
writeConcern 的设置是事务级别的,不能对事务内的操作单独设置 writeConcern;
多文档事务中,不允许设置 w: 0
模式;
只有设置了 w: majority
时,readConcern snapshot
和 majority
才能保证获取到已经在多数节点提交的数据;
主要配置参数
transactionLifetimeLimitSeconds
设备单个事务的最长生命周期,默认为 60 秒,最小值为 1 秒;可通过如下方式进行设置:
db.adminCommand( { setParameter: 1, transactionLifetimeLimitSeconds: 30 } )
maxTransactionLockRequestTimeoutMillis
事务中的锁等待时间,默认为 5 毫秒,如果超过该时间,则事务回滚(aborts);可通过如下方式进行设置:
db.adminCommand( { setParameter: 1, maxTransactionLockRequestTimeoutMillis: 20 } )
当设置为 0 时,表示不等待,如果请求锁失败,则事务立即回滚;
当设置为 -1 时,表示一直等待,直到操作超时;
其它注意事项
所有事务中的 CRUD 操作,均要求 collection 已经预先存在;
所有事务均不能对 config、admin、local 三个 database 进行操作;
事务中不能对 system.* 的 collection 进行写操作(增删改);
事务内部不能对事务外部创建的游标(cursor)进行 getMore
操作,反之亦然;
如果在创建或删除 collection 后需要马上进入事务,并且事务中有对原 collection 的操作,则建议在创建或删除时,设置 writeConcern 为 majority
;
事务中不能直接使用 Count Operation,需要在 aggregation 中使用 $count
或 $group
来替代,如:
mongodb40:PRIMARY> session.startTransaction()
mongodb40:PRIMARY> trans.count()
2018-07-30T14:31:44.187+0800 E QUERY [js] Error: count failed: {
"operationTime" : Timestamp(1532932298, 1),
"ok" : 0,
"errmsg" : "Cannot run 'count' in a multi-document transaction. Please see http://dochub.mongodb.org/core/transaction-count for a recommended alternative.",
"code" : 50851,
"codeName" : "Location50851",
"$clusterTime" : {
"clusterTime" : Timestamp(1532932298, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
DBQuery.prototype.count@src/mongo/shell/query.js:382:11
DBCollection.prototype.count@src/mongo/shell/collection.js:1424:12
@(shell):1:1
mongodb40:PRIMARY> session.abortTransaction()
mongodb40:PRIMARY> session.startTransaction()
mongodb40:PRIMARY> trans.aggregate([{$match: {a: {$gte: 0}}}, {$count: "col_num"}])
{ "col_num" : 12 }
[Important] 使用事务将不可避免的大幅降低服务性能,尽可能避免事务的使用;
[Repeat] 多文档事务目前只支持单复本集群,预计在 4.2 才会开始支持 Sharding 架构;
客户端驱动
请特别留意:所有与事务相关的读写操作,均需与 transaction session 相关联。(参考下方 Python 样例及官方样例)
Language | Version |
---|---|
Java | 3.8.0 |
Python | 3.7.0 |
C | 1.11.0 |
C# | 2.7 |
CXX | 3.4.x |
Python 示例
mc = MongoClient("mongodb://192.168.93.12:27017,192.168.93.18:27017,192.168.93.11:27017/?replicaSet=mongodb40")
with mc.start_session() as session:
collection = session.client.chenxs.trans
session.start_transaction()
collection.insert_one({"a": 14}, session=session)
result_inside_trans = collection.find_one({"a": 14}, session=session)
# with transaction session: got a
print "inside trans before commit, we got: %s" % result_inside_trans
result_outside_trans = collection.find_one({"a": 14})
# without transaction session: got none
print "outside trans before commit, we got: %s" % result_outside_trans
session.abort_transaction()
参考文档
往期精彩
﹀
﹀
﹀
MongoDB Change streams 与数据订阅同步
人工智障入门
网易游戏《荒野行动》《阴阳师》等出海实践-AWS技术峰会演讲实录
MySQL Flashback拯救手抖党
函数式编程在JavaScript下应用实践