MVCC多版本控制

1. 丟失的數據

旺財是數據庫村的一個程序, 小強也是。

數據庫村有個特點, 很多數據支持共享操作,多個程序可以同時讀寫,他們倆經常會爲了讀寫同一個數據, 爭奪的不可開交。

這一天,當旺財和小強對同一個銀行賬戶A進行寫操作時候, 出現了這麼一個錯誤:

 

看看, 本來旺財要加上的20元就丟掉了。  

同樣的事情發生的多了, 他倆給這種情況起了一個名字,叫“丟失修改”, 其實說白了就是倆人都去寫一個數據, 一個人的數據把另外一個給覆蓋了。

村裏的MySQL老頭兒說: “你們兩個小傢伙,寫數據的時候連加鎖都不做,肯定會出大亂子!"

旺財說:“加什麼鎖?”

“來來來, 我教你們一個排他鎖(Exclusive Lock) ,   簡稱X鎖, 旺財你要寫數據了, 就把它用X鎖鎖住, 鎖住後,除非你釋放, 否則小強無法獲得X鎖。 這不就解決你們的問題了?  ”

小強想了想, 就把上面的操作過程用X鎖改了一下:

財說:“果然不錯, 確實可以解決兩個人同時修改導致的問題。”

 

2. 髒數據

小強說:“旺財, 我們約定,寫數據的時候都用X鎖吧?”

旺財說: “這沒問題, 可是X鎖只在寫數據的時候用, 我們讀數據是不用加鎖的, 我想起了一種情況, 你看看怎麼辦?”

 

小強在旺財執行的途中讀了A的值, 但是旺財把對A的修改給回滾(Rollback)了, 這下小強尷尬了, 他讀到了髒數據

“要不我們在讀取數據的時候也加個X鎖 ? ” 小強說。

“那樣太嚴格了, 就是讀一個數據啊, 值得嗎?”

“這樣吧, 我們再搞一個新的鎖出來, 專門用於共享數據的讀取, 就叫共享鎖(Share lock) ,簡稱S鎖, 這個鎖和之前的排他鎖X鎖有區別, 主要用於讀取數據,  如果一個數據加了X鎖, 就沒法加S鎖, 同樣加了S鎖, 就沒法加X鎖”   小強想出了一個點子。

“那如果我加了S鎖, 你還能加S鎖嗎? ”  旺財問。

“應該可以吧,  咱們倆都是讀數據, 互不影響啊。 還有爲了防止長時間的鎖住, 我們可以約定一下,不管我們要做的事情有多少, 讀一個數據之前加S鎖, 讀完之後立刻釋放該S鎖 ! ”

果然,這樣一來“髒數據”的問題就解決了 !

 

3. 沒法重複讀?

旺財和小強兩個程序相安無事了很久, 但是S鎖在讀完數據後立刻釋放的約定, 導致出了一個新問題。

旺財在一次數據處理中, 先讀取了A和B的值, 相加得到了150 ,  然後小強把B改成了30

旺財再次讀取A和B, 發現求和以後是130 , 剛纔的不一樣了!

(碼農翻身注: 假定旺財的處理是在一個事務當中)

旺財說: “小強,  我在讀取數據的時候你不能改啊 , 要不然我這裏會出現不一致, 你看剛開始是A+B是 150, 現在變成130了”

小強說: “我們之前的約定是讀數據時加S鎖, 讀完立馬釋放,  問題就出現在這裏了。”

“看來在讀數據的時候, 也需要一直鎖定了, 直到事務提交。”

 

4. 幻覺出現

旺財和小強現在已經能靈活的使用X鎖和S鎖了。

他們倆總結了一下, 分爲了這麼幾種情況:

1.  寫數據時加上X鎖,直到事務結束, 讀的時候不加鎖。

雖然能夠避免丟失數據,  但是可以讀到沒有提交或者回滾的內容 (髒數據), 這其實就是數據庫最低的事務隔離級別 --- Read uncommitted

2. 寫數據的時候加上X鎖, 直到事務結束,  讀的時候加上S鎖, 讀完數據立刻釋放。

這能避免“丟失數據”和“髒數據”,  但是會出現“不可重複讀”的問題  ,  這是第二級的事務隔離級別 -- Read committed

3.  寫數據的時候加上X鎖,  直到事務結束, 讀數據的時候加S鎖, 也是直到事務結束。

這能避免“丟失數據”和“髒數據”, “不可重複讀”三個問題 , 這是數據庫常用的隔離級別 --Repeatable read

整個世界似乎清淨了。

有一次旺財對一個“學生表”進行操作,選取了年齡是18歲的所有行, 用X鎖鎖住, 並且做了修改。

改完以後旺財再次選擇所有年齡是18歲的行, 想做一個確認, 沒想到有一行竟然沒有修改!

這是怎麼回事?  出了幻覺嗎?

原來就在旺財查詢並修改的的時候,  小強也對學生表進行操作, 他插入了一個新的行,其中的年齡也是18歲!  雖然兩個人的修改都沒有問題, 互不影響, 但從最終效果看, 還是出了事。

(碼農翻身注: 正是小強的操作, 讓旺財出現了“幻讀”)

旺財說: “沒轍了, 我們倆非得串行執行不可, 你必須得等我執行完。 ”

這就是數據庫事務隔離級別的終極大招:Serializable (串行化)

最後, 爲了方便記憶, 他們倆倒騰了半天, 整出了一張表, 用於記錄各種情況:

兩個人看着這張表, 感慨的說:“唉, 這數據庫村的事務隔離級別可真是不容易啊!”

 

5. MVCC

旺財和小強使用了一段時間的“串行化”隔離級別,雖然不會出錯,但是效率實在太低了。數據庫村的人都笑話他倆幹活太慢, 於是倆人商量着退到“可重複讀”,雖然會出現幻讀,但是也能忍受。

“可重複讀”用了一段時間,他們又不滿意了。

旺財唉聲嘆氣地說:“爲了實現可重複讀, 我們需要在事務中對讀操作加鎖,並且得持續到整個事務結束,這實在是不爽啊!”

小強說:“是啊,我修改數據的時候,還得等待你讀完成,效率就太低嘍。”

許久不見的MySQL聽到他倆的抱怨,插嘴道:“看來你們兩個已經開始思考了啊,我有一個辦法, 可以在讀的時候不用加鎖,也能實現可重複讀。”

“你就吹吧!這怎麼可能?” 旺財和小強根本不相信。

MySQL老頭兒說: “你們兩個太孤陋寡聞了,這個方法叫做MVCC(多版本併發控制)。”

頓了一下, MySQL老頭兒故意激他們:“可是有點難啊,你們倆不一定能弄明白。”

旺財和小強很不服氣:“說來聽聽!”

“假設啊,數據庫中有一個叫做users的表,裏邊有這麼一行數據:” MySQL老頭兒開始畫圖:

“現在,我要給他加兩個隱藏的字段:”

“事務ID? 是不是每次開始事務的時候分配的? ”

“沒錯,這個事務ID就表明這一行數據是哪個事務操作的,注意啊,事務ID是一個遞增的數字,每次開始新事務,這個數字就會增加。”

“這有什麼用?”

“別急,馬上就會講到,” MySQL老頭兒地說:“ 旺財,小強,假設你們倆的事務中SQL的執行次序如下: ”

在標號爲 (1) 的地方,數據是這樣的:

與此同時,需要建立一個叫做Read View的數據結構,它有三個部分:

(1) 當前活躍的事務列表 ,即[101,102]

(2) Tmin ,就是活躍事務的最小值, 在這裏 Tmin = 101

(3) Tmax, 是系統中最大事務ID(不管事務是否提交)加上1。 在這裏例子中,Tmax = 103

(注: 在可重複讀的隔離級別下,當第一個Read操作發生的時候,Read view就會建立。 在Read Committed隔離級別下,每次發出Read操作,都會建立新的Read view。)

旺財和小強還不知道有什麼用處,只是死記硬背,生怕跟不上老頭兒的思路。

MySQL老頭兒接着說道: “在標號爲(2)的地方,小強做了修改,數據是這樣的:”

“看到回滾指針沒有? 它指向了上一條記錄。”

“怪不得叫做多版本併發控制,你這裏維護了數據的多個版本啊。” 小強感慨道。

“按照可重複讀的要求,我開始了一個事務,無論我讀多少次,我總是能讀到age=20的那行記錄,即使小強修改了age,我也不受影響。你這個結構該怎麼實現啊? ” 旺財問道。

“關鍵部分要到了,我這裏有個算法,用來判斷這些數據版本記錄中哪些對你來說是可見的(可讀的)。 ”

旺財只覺得覺得自己的頭嗡地一下就大了:“這....怎麼這麼麻煩!”

MySQL老頭說:“這還麻煩? 已經很簡單的算法了,就是幾個if else ,加上幾個循環而已! 連這個都整不明白,別在我們數據庫村混了! 對於上面的例子,ReadView 中事務列表是[101,102], Tmin= 101, Tmax = 103,你們想想,第一次讀和第二次讀是什麼樣子。”

只聽到小強嘴裏嘟囔着:“ 當旺財第一次讀的時候,只有一條記錄, tid = 100 ,小於Tmin,所以是可以讀的。 然後我做了修改, 當旺財第二次讀的時候,tid=102,程序走到了‘tid是否在Read View中這一分支,由於102確實在Read View的活動事務列表中,那就順着回滾指針找到下一行記錄,即tid爲100那一行,再次判斷,這就和第一次讀一樣了。”

MySQL老頭兒得意地說:“對嘍,這不就實現了可保證可重複讀嘛! 旺財你想想,你在讀數據的時候,需不需要加鎖操作?”

旺財搖頭:“不用加鎖, 我只要找到正確的版本就可以了。 ”

(注: 但是在寫數據的時候,MySQL還是要加鎖的,防止寫-寫衝突)

“這就是MVCC的好處啊,讀寫不互相等待,能極大地提高數據庫的併發能力啊。”

旺財還是有點不放心,覺得這種方式太複雜了,但是轉念一想,讀的時候不用加鎖,這個誘惑實在太大, 他說:“這樣吧,我和小強再合計合計。”

MySQL老頭兒自信地說:“沒問題,你們來再想想,有問題再找我吧。”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章