MongoDB 4.0 引入的事务功能,支持多文档ACID特性,例如使用 mongo shell
进行事务操作
> s = db.getMongo().startSession()
session { "id" : UUID("3bf55e90-5e88-44aa-a59e-a30f777f1d89") }
> s.startTransaction()
> db.coll01.insert({x: 1, y: 1})
WriteResult({ "nInserted" : 1 })
> db.coll02.insert({x: 1, y: 1})
WriteResult({ "nInserted" : 1 })
> s.commitTransaction() (或者 s.abortTransaction()回滚事务)
支持 MongoDB 4.0 的其他语言 Driver 也封装了事务相关接口,用户需要创建一个 Session
,然后在 Session
上开启事务,提交事务。例如
python 版本
with client.start_session() as s:
s.start_transaction()
collection_one.insert_one(doc_one, session=s)
collection_two.insert_one(doc_two, session=s)
s.commit_transaction()
java 版本
try (ClientSession clientSession = client.startSession()) {
clientSession.startTransaction();
collection.insertOne(clientSession, docOne);
collection.insertOne(clientSession, docTwo);
clientSession.commitTransaction();
}
Session
Session
是 MongoDB 3.6 版本引入的概念,引入这个特性主要就是为实现多文档事务做准备。Session
本质上就是一个「上下文」。
在以前的版本,MongoDB 只管理单个操作的上下文,mongod
服务进程接收到一个请求,为该请求创建一个上下文 (源码里对应 OperationContext
),然后在服务整个请求的过程中一直使用这个上下文,内容包括,请求耗时统计、请求占用的锁资源、请求使用的存储快照等信息。有了 Session
之后,就可以让多个请求共享一个上下文,让多个请求产生关联,从而有能力支持多文档事务。
每个 Session
包含一个唯一的标识 lsid,在 4.0 版本里,用户的每个请求可以指定额外的扩展字段,主要包括:
- lsid: 请求所在 Session 的 ID, 也称 logic session id
- txnNmuber: 请求对应的事务号,事务号在一个 Session 内必须单调递增
- stmtIds: 对应请求里每个操作(以insert为例,一个insert命令可以插入多个文档)操作ID
实际上,用户在使用事务时,是不需要理解这些细节,MongoDB Driver 会自动处理,Driver 在创建 Session
时分配 lsid,接下来这个 Session
里的所以操作,Driver 会自动为这些操作加上 lsid,如果是事务操作,会自动带上 txnNumber。
值得一提的是,Session
lsid 可以通过调用 startSession
命令让 server 端分配,也可以客户端自己分配,这样可以节省一次网络开销;而事务的标识,MongoDB 并没有提供一个单独的 startTransaction
的命令,txnNumber 都是直接由 Driver 来分配的,Driver 只需保证一个 Session 内,txnNumber 是递增的,server 端收到新的事务请求时,会主动的开始一个新事务。
MongoDB 在 startSession
时,可以指定一系列的选项,用于控制 Session
的访问行为,主要包括:
- causalConsistency: 是否提供
causal consistency
的语义,如果设置为true,不论从哪个节点读取,MongoDB 会保证 “read your own write” 的语义。参考 causal consistency - readConcern:参考 MongoDB readConcern 原理解析
- writeConcern:参考 MongoDB writeConcern 原理解析
- readPreference: 设置读取时选取节点的规则,参考 read preference
- retryWrites:如果设置为true,在复制集场景下,MongoDB 会自动重试发生重新选举的场景; 参考retryable write
ACID
Atomic
针对多文档的事务操作,MongoDB 提供 “All or nothing” 的原子语义保证。
Consistency
太难解释了,还有抛弃 Consistency 特性的数据库?
Isolation
MongoDB 提供 snapshot 隔离级别,在事务开始创建一个 WiredTiger snapshot,然后在整个事务过程中使用这个快照提供事务读。
Durability
事务使用 WriteConcern {j: ture}
时,MongoDB 一定会保证事务日志提交才返回,即使发生 crash,MongoDB 也能根据事务日志来恢复;而如果没有指定 {j: true}
级别,即使事务提交成功了,在 crash recovery 之后,事务的也可能被回滚掉。
事务与复制
复制集配置下,MongoDB 整个事务在提交时,会记录一条 oplog(oplog 是一个普通的文档,所以目前版本里事务的修改加起来不能超过文档大小 16MB的限制),包含事务里所有的操作,备节点拉取oplog,并在本地重放事务操作。
事务 oplog 示例,包含事务操作的 lsid,txnNumber,以及事务内所有的操作日志(applyOps字段)
“ts” : Timestamp(1530696933, 1), “t” : NumberLong(1), “h” : NumberLong(“4217817601701821530”), “v” : 2, “op” : “c”, “ns” : “admin.$cmd”, “wall” : ISODate(“2018-07-04T09:35:33.549Z”), “lsid” : { “id” : UUID(“e675c046-d70b-44c2-ad8d-3f34f2019a7e”), “uid” : BinData(0,”47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=”) }, “txnNumber” : NumberLong(0), “stmtId” : 0, “prevOpTime” : { “ts” : Timestamp(0, 0), “t” : NumberLong(-1) }, “o” : { “applyOps” : [ { “op” : “i”, “ns” : “test.coll2”, “ui” : UUID(“a49ccd80-6cfc-4896-9740-c5bff41e7cce”), “o” : { “_id” : ObjectId(“5b3c94d4624d615ede6097ae”), “x” : 20000 } }, { “op” : “i”, “ns” : “test.coll3”, “ui” : UUID(“31d7ae62-fe78-44f5-ba06-595ae3b871fc”), “o” : { “_id” : ObjectId(“5b3c94d9624d615ede6097af”), “x” : 20000 } } ] } }
整个重放过程如下:
- 获取当前 Batch (后台不断拉取 oplog 放入 Batch)
- 设置
OplogTruncateAfterPoint
时间戳为 Batch里第一条 oplog 时间戳 (存储在 local.replset.oplogTruncateAfterPoint 集合) - 写入 Batch 里所有的 oplog 到 local.oplog.rs 集合,根据 oplog 条数,如果数量较多,会并发写入加速
- 清理
OplogTruncateAfterPoint
, 标识 oplog 完全成功写入;如果在本步骤完成前 crash,重启恢复时,发现oplogTruncateAfterPoint
被设置,会将 oplog 截短到该时间戳,以恢复到一致的状态点。 - 将 oplog 划分到到多个线程并发重放,为