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