歡迎光臨
每天分享高質量文章

資料庫村的旺財和小強

來自:碼農翻身(微訊號:coderising)

作者:劉欣

本來想單獨寫個MVCC的,但是後來一想,如果不扯上事務的隔離級別,理解起來不是很方便, 還是在原來的一篇文章《資料庫村的旺財和小強》上改寫吧。 沒看過的同學自然可以從頭看起,看過的同學可以複習下,再想想事務隔離級別的本質。

這是一篇超級乾的乾貨,尤其是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 (序列化)

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

(點選看大圖)

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

5MVCC

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

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

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

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

許久不見的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老頭兒自信地說:“沒問題,你們來再想想,有問題再找我吧。”

(註:本文講解了可重覆讀的情況,對於Read Committed 這個可以適用同樣的演演算法,只是每次讀操作的時候,都要建立新的Read view,朋友們可自行分析下。)

(完)


●編號305,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂