MongoDB4.0事務實現解析
上個月底 MongoDB Wolrd 宣佈釋出 MongoDB 4.0, 支援複製集多文件事務,阿里雲資料庫團隊 研發工程師第一時間對事務功能的時間進行了原始碼分析,解析事務實現機制。
MongoDB 4.0 引入的事務功能,支援多文件ACID特性,例如使用 mongo shell
進行事務操作
> s = db.getMongo().startSession()
session { "id" : UUID("3bf55e90-5e88-44aa-a59e-a30f777f1d89") }
> s.startTransaction()
> session.getDatabase("mytest").coll01.insert({x: 1, y: 1})
WriteResult({ "nInserted" : 1 })
> session.getDatabase("mytest").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 劃分到到多個執行緒併發重放,為了提升併發效率,事務產生的 oplog 包含的所有修改操作,跟一條普通單條操作的 oplog 一樣,會據文件ID劃分到多個執行緒。
- 更新
ApplyThrough
時間戳為 Batch 裡最後一條 oplog 時間戳,標識下一次重啟後,從該位置重新同步,如果本步驟之前失敗,重啟恢復時,會從ApplyThrough
上一次的值(上一個 Batch 最後一條 oplog)拉取 oplog。 - 更新 oplog 可見時間戳,如果有其他節點從該備節點同步,此時就能讀到這部分新寫入的 oplog
- 更新本地 Snapshot(時間戳),新的寫入將對使用者可見。
事務與儲存引擎
事務時序統一
WiredTiger 很早就支援事務,在 3.x 版本里,MongoDB 就通過 WiredTiger 事務,來保證一條修改操作,對資料、索引、oplog 三者修改的原子性。但實際上 MongoDB 經過多個版本的迭代,才提供了事務介面,核心難點就是時序問題。
MongoDB 通過 oplog 時間戳來標識全域性順序,而 WiredTiger 通過內部的事務ID來標識全域性順序,在實現上,2者沒有任何關聯。這就導致在併發情況下, MongoDB 看到的事務提交順序與 WiredTiger 看到的事務提交順序不一致。
為解決這個問題,WiredTier 3.0 引入事務時間戳(transaction timestamp)機制,應用程式可以通過 WT_SESSION::timestamp_transaction
介面顯式的給 WiredTiger 事務分配 commit timestmap,然後就可以實現指定時間戳讀(read "as of" a timestamp
)。有了 read "as of" a timestamp
特性後,在重放 oplog 時,備節點上的讀就不會再跟重放 oplog 有衝突了,不會因重放 oplog 而阻塞讀請求,這是4.0版本一個巨大的提升。
/*
* __wt_txn_visible --
* Can the current transaction see the given ID / timestamp?
*/
static inline bool
__wt_txn_visible(
WT_SESSION_IMPL *session, uint64_t id, const wt_timestamp_t *timestamp)
{
if (!__txn_visible_id(session, id))
return (false);
/* Transactions read their writes, regardless of timestamps. */
if (F_ISSET(&session->txn, WT_TXN_HAS_ID) && id == session->txn.id)
return (true);
#ifdef HAVE_TIMESTAMPS
{
WT_TXN *txn = &session->txn;
/* Timestamp check. */
if (!F_ISSET(txn, WT_TXN_HAS_TS_READ) || timestamp == NULL)
return (true);
return (__wt_timestamp_cmp(timestamp, &txn->read_timestamp) <= 0);
}
#else
WT_UNUSED(timestamp);
return (true);
#endif
}
從上面的程式碼可以看到,再引入事務時間戳之後,在可見性判斷時,還會額外檢查時間戳,上層讀取時指定了時間戳讀,則只能看到該時間戳以前的資料。而 MongoDB 在提交事務時,會將 oplog 時間戳跟事務關聯,從而達到 MongoDB Server 層時序與 WiredTiger 層時序一致的目的。
事務對 cache 的影響
WiredTiger(WT) 事務會開啟一個快照,而快照的存在的 WiredTiger cache evict 是有影響的。一個 WT page 上,有N個版本的修改,如果這些修改沒有全域性可見(參考 __wt_txn_visible_all
),這個 page 是不能 evict 的(參考 __wt_page_can_evict
)。
在 3.x 版本里,一個寫請求對資料、索引、oplog的修改會放到一個 WT 事務裡,事務的提交由 MongoDB 自己控制,MongoDB 會盡可能快的提交事務,完成寫清求;但 4.0 引入事務之後,事務的提交由應用程式控制,可能出現一個事務修改很多,並且很長時間不提交,這會給 WT cache evict 造成很大的影響,如果大量記憶體無法 evict,最終就會進入 cache stuck 狀態。
為了儘量減小 WT cache 壓力,MongoDB 4.0 事務功能有一些限制,但事務資源佔用超過一定閾值時,會自動 abort 來釋放資源。規則包括
- 事務的生命週期不能超過
transactionLifetimeLimitSeconds
(預設60s),該配置可線上修改 - 事務修改的文件數不能超過 1000 ,不可修改
- 事務修改產生的 oplog 不能超過 16mb,這個主要是 MongoDB 文件大小的限制, oplog 也是一個普通的文件,也必須遵守這個約束。
Read as of a timestamp 與 oldest timestamp
Read as of a timestamp
依賴 WiredTiger 在記憶體裡維護多版本,每個版本跟一個時間戳關聯,只要 MongoDB 層可能需要讀的版本,引擎層就必須維護這個版本的資源,如果保留的版本太多,也會對 WT cache 產生很大的壓力。
WiredTiger 提供設定 oldest timestamp
的功能,允許由 MongoDB 來設定該時間戳,含義是Read as of a timestamp
不會提供更小的時間戳來進行一致性讀,也就是說,WiredTiger 無需維護 oldest timestamp
之前的所有歷史版本。MongoDB 層需要頻繁(及時)更新 oldest timestamp
,避免讓 WT cache 壓力太大。
引擎層 Rollback 與 stable timestamp
在 3.x 版本里,MongoDB 複製集的回滾動作是在 Server 層面完成,但節點需要回滾時,會根據要回滾的 oplog 不斷應用相反的操作,或從回滾源上讀取最新的版本,整個回滾操作效率很低。
4.0 版本實現了儲存引擎層的回滾機制,當複製集節點需要回滾時,直接呼叫 WiredTiger 介面,將資料回滾到某個穩定版本(實際上就是一個 Checkpoint),這個穩定版本則依賴於 stable timestamp
。WiredTiger 會確保 stable timestamp
之後的資料不會寫到 Checkpoint裡,MongoDB 根據複製集的同步狀態,當資料已經同步到大多數節點時(Majority commited),會更新 stable timestamp
,因為這些資料已經提交到大多數節點了,一定不會發生 ROLLBACK,這個時間戳之前的資料就都可以寫到 Checkpoint 裡了。
MongoDB 需要確保頻繁(及時)的更新 stable timestamp
,否則影響 WT Checkpoint 行為,導致很多記憶體無法釋放。
分散式事務
MongoDB 4.0 支援副本集多文件事務,並計劃在 4.2 版本支援分片叢集事務功能。下圖是從 MongoDB 3.0 引入 WiredTiger 到 4.0 支援多文件事務的功能迭代圖,可以發現一盤大棋即將上線,敬請期待。
相關文章
- MongoDB 4.0 事務實現解析MongoDB
- 事務管理最佳實踐全面解析
- MySQL事務實現原理MySql
- Kafka事務實現原理Kafka
- spring原始碼解析 (七) 事務底層原始碼實現Spring原始碼
- MongoDB4.0支援多文件ACID事務意味著什麼?MongoDB
- 分散式事務之資料庫事務與JDBC事務實現(一)分散式資料庫JDBC
- Spring事務實現原理Spring
- 【Spring】事務實現原理Spring
- MySQL innodb 事務的實現MySql
- 事務機制如何實現
- 分散式事務(3)---RocketMQ實現分散式事務原理分散式MQ
- 分散式事務(4)---RocketMQ實現分散式事務專案分散式MQ
- 如何在Redis中實現事務Redis
- AOP實現事務控制的疑惑
- MySQL 事務隔離級別解析和實戰MySql
- 從JDBC到ORM的事務實現JDBCORM
- Laravel 之巢狀事務 transactions 實現Laravel巢狀
- 實現多資料來源事務
- Redis 設計與實現 (七)--事務Redis
- hibernate能否實現巢狀事務巢狀
- 基於RocketMQ實現分散式事務MQ分散式
- 使用Spring Boot實現事務管理Spring Boot
- Spring 事務原始碼解析Spring原始碼
- Spring事務管理全解析Spring
- 事務標識(xid)解析
- Spring分散式事務XA事務(兩段提交2PC)實現Spring分散式
- MySQL是如何實現事務的ACIDMySql
- 分散式事務的幾種實現方式分散式
- 搞懂MySQL InnoDB事務ACID實現原理MySql
- Apache ShardingSphere 如何實現分散式事務Apache分散式
- MySQL實現事務的提交和回滾MySql
- Golang 實現 Redis(8): TCC分散式事務GolangRedis分散式
- 【spring】事務底層的實現流程Spring
- SpringCloud+RocketMQ實現分散式事務SpringGCCloudMQ分散式
- 使用JOTM實現分散式事務的例子分散式
- MySQL 事務隔離級別實現原理MySql
- MogDB/openGauss如何實現事務的rollback