一起學mongodb第五卷之事務
前言
事務是 mongoDB 中非常核心的一個功能,在 4.0 版本以前,mongoDB 只支持單個文檔的事務,在 4.0 和 4.2 版本之后,分別支持了復制集事務和分片事務,也可以說在大多數的數據庫中都是非常重要的一個功能,值得我們單獨拉一章去講解。
那「怎么樣在 mongoDB 中合理的使用事務來保證數據安全呢」?
后續我將會從讀、寫和多文檔事務這三個方向去闡述。
寫事務
使用 writeConcern 保證數據準確落盤。
writeConcern 中有兩個選項。
w(決定一條數據落到寫到多少個節點才算真正成功)。
- 0:不關心(最不安全)。
- 數字:寫到 n 個節點才算成功(自定義)。
- majority:寫入至少一半的節點才算成功(推薦,性能和安全均衡)。
- all:全部寫完才算成功(性能差點,很安全,但是只要有一個失敗就會失敗)。
j(決定怎樣才算真正成功)。
- true:寫入 journal 日志 才算成功。
- false:寫入內存就算成功。
db.collection.insert({a:1},{writeConcen:{w:"majority",j:true});
對于一些「普通數據可以使用 w:1 來確保最佳性能」,對于「重要數據可以用 w:majority 來保證數據安全」。
讀事務
readPreference 來確定從哪里讀。
readPreference 有幾個屬性。
- primary:只從主節點讀。
- primaryPreferred:先讀主節點,如果掛了再讀從節點。
- secondary:只從從節點讀。
- secondaryPreferred:先讀從節點,如果掛了在讀主節點。
- nearest:讀最近的節點。
「primiry 和 primaryPreferred 適用于對延遲敏感讀較高」的數據,比如訂單信息。
「secondary 和 secondaryPreferred 適用于對延遲敏感度要求較低」的數據,比如日志信息。
「nearest 適用于業務域較廣的應用」,比如將業務信息同步到全球各地的節點,「中國用戶會訪問中國的節點,俄羅斯用戶會訪問俄羅斯的節點」, nearest 的判斷也是比較簡單的,直接是使用應用到 mongo 服務器的的 ping time 來決定。
當然,還有一種是給「服務器打標簽(tag) 的方式」,比如要將讀取操作定向到標記有 "name": "a"和"key": "person"的輔助節點集:
db.collection.find({}).readPref( "secondary", [ { "name": "a", "key": "person" } ] )
readConcern 來確定可以讀什么樣的數據。
readConcern 有幾個屬性。
- available:讀取所有可用的數據。
- loacl:讀取所有可用且僅屬于當前分片的數據。
- majority:讀取大多數節點都寫入的數據。
「通過快照來維護多個不同的版本,使用 MVCC 實現」,每個被大多數節點確認過的數據就是一個快照。
- linearizable:可線性化讀取文檔。
有時會被阻塞,其保證如果一個線程已經完成了寫入并且告知了其他線程,那么這其他的線程就可以看到這些改動。如果某一瞬間你的副本集出現了兩個主節點(有一個還未來得及降級)然后你從這個老的主節點上進行讀取,與此同時新的主節點上已經有了新的數據,你讀到的數據就是舊數據。
- snapshot:讀取快照中的數據(類似于可串行化)。
loacl 和 available 的區別體現在分片集群中的 chunk 遷移上,如果讀 shard2 ,loacl 不能讀到 x ,但是 available 可以讀到。
多文檔事務
- 4.0 版本 mongoDB 支持了復制集的多文檔事務。
- 4.2 版本 mongoDB 支持了分片集群的多文檔事務。
也就是說是說,mongoDB 在 4.2 版本的是有擁有了和 mysql 這種關系型數據庫一樣的事務能力,這對于業務的選擇角度來講,又給 mongoDB 添加了一筆濃重的色彩。
在整個數據庫的分布式事務當中,還需要重點提一嘴的就是時間問題,我們先來看看會有什么問題存在。
比如有兩個操作發向 a、b 兩個節點。
- 客戶端將 a = 1 發向 a、b 節點。
- a 節點操作 a =1。
- 客戶端將 a = a +1 發送給 a、b 節點。
- a 節點操作 a = a + 1。
- b 節點由于業務網絡等原因先執行了 a = a + 1,后操作了 a = 1。
最后我們就發現,a、b 兩個節點的數據不一致了,那么 mongo 是怎么解決的呢,一般是兩種方式:
- 「全局授時」:;比如我們可以采用GPS時鐘或者是NTP服務這種全局授時點。
- 「邏輯時鐘」:也就是我們采用一種局部的時間戳的方式去演進,這個就叫邏輯時鐘。
mongo 采用的是「混合邏輯時鐘」:
在這個混合邏輯時鐘中,將物理時鐘和邏輯時鐘混合起來做一個全局的時間出來處理。我們的混合邏輯時鐘會采用一種本地的推進方式,這個就是剛才說的一個接受的時候,他會比較本地的時間戳,然后在本地時間戳、本地真實的物理時間和收到最短 request 的時間,「三者取最大的時間,作為本地時間的一個推進」,需要說明的是,這個時間戳的分配是取決于 oplog 的時間戳。只有「當 oplog 真正寫入數據的時候,本地的邏輯時鐘才會向前推進」。在整個混合邏輯時鐘,在整個集群中采用動態推進的方式,「每一條發送和接收的請求,都會依據請求中的時間來推進本地的時鐘」,這樣在全局的情況下,每個節點的混合邏輯時鐘最終會趨同,趨向同一個地址,趨向同一個時間。這樣的話,剛才說的時間偏差就已經不存在了,才可以在集群中做分布式事務。
再說說 mongo 提交事務的過程吧。
mongoDb 的分布式事務和 mysql 一樣,也是基于「兩階段協議」。
- 第一階段就是 prepare 階段,在 prepare 過程中,所有的 coordinator 會向所有的節點去發送 prepare 命令,所有的節點收到了這個命令以后會返回自己的 prepare timestamp,然后由協調節點去決定選取一個最大的 prepare ts 作為 commit timestamp。
coordinator 和所有的 shard 之間的通訊會促使所有的事務參與者得到一個協調一致的 HLC。在這種邏輯時鐘一致的情況下,commit timestamp 就是全局順序一致的。
- 第二階段的話就是提交階段, coordinator 會將剛剛的 committed ts 作為 commit timestamp 的時間戳,然后向所有的節點去廣播。
需要關注一點,就是在對具有 prepare timestamp 的事務進行讀取的時候,如果當前的事務是處于 prepare 狀態的,并不確定自身的讀時間戳和 prepare 狀態的大小的話,需要去一直等待這個事務,等到事務提交或者 abort 以后才去會處理,這個就是剛才所說的。
- https://mongoing.com/archives/77608。
- 巨人的肩膀。
mongoDB 整個事務實現的方式都是按照「讀提交」這種關系來設計的,也就是說,在客戶端讀取數據的時候,只能讀到該事務節點前已經做了 commit 的數據。