這是數(shù)據(jù)庫事務(wù)分享的第二篇,上一篇講解數(shù)據(jù)庫事務(wù)并發(fā)會(huì)產(chǎn)生的問題,這篇會(huì)詳細(xì)講數(shù)據(jù)庫如何避免這些問題,也就是如何實(shí)現(xiàn)隔離,主要是講兩種主流技術(shù)方案——MVCC與鎖,理解了MVCC與鎖,就可以舉一反三地看各種數(shù)據(jù)庫并發(fā)控制方案,并理解每種實(shí)現(xiàn)能解決的問題以及需要開發(fā)者自己注意的并發(fā)問題,以更好支撐業(yè)務(wù)開發(fā)。
先回顧一下上一篇討論過的,如果沒有隔離或者隔離級別不足,會(huì)帶來的問題:
- 臟寫(Dirty Write)
- 臟讀(Dirty Read)
- 不可重復(fù)讀(Unrepeatable Read)
- 幻讀(Phantom)
- 讀偏差(Read Skew)
- 寫偏差(Write Skew)
- 丟失更新(Lost Updates)
可見,所有問題本質(zhì)上都是由寫造成的,根源都是數(shù)據(jù)的改變。
讀是不改變數(shù)據(jù)的,因此無論多少讀并發(fā),都不會(huì)出現(xiàn)沖突,如果所有的事務(wù)都只由讀組成,那么無論如何調(diào)度它們,它們都是可串行化的,因?yàn)樗鼈兊膱?zhí)行結(jié)果,都與某個(gè)串行執(zhí)行的結(jié)果相同,但是寫會(huì)造成數(shù)據(jù)的改變,稍有不慎,這個(gè)并發(fā)調(diào)度的結(jié)果就會(huì)與串行調(diào)度的結(jié)果不符合。
在進(jìn)行下面的討論下,先定義好我們描述事務(wù)的模型:
我們用 R 表示讀(read),用 W 表示寫(Write),在操作后跟數(shù)字,代表哪個(gè)事務(wù)在進(jìn)行操作,在數(shù)字后跟括號,代表操作哪個(gè)元素
用 R1(A) 表示事務(wù)1讀元素A,用 R2(A)表示事務(wù)2讀A
看一下寫操作如何造成并發(fā)調(diào)度與串行執(zhí)行的結(jié)果不符合:
事務(wù)1讀A的值,并且在此基礎(chǔ)上增加50并回寫A,但是在回寫之前,事務(wù)2將A修改為了200,這兩個(gè)事務(wù)按照此調(diào)度執(zhí)行后,A的最終值為150,不符合任何串行調(diào)度的結(jié)果。
如果串行調(diào)度為 事務(wù)1 => 事務(wù)2,那么A最終應(yīng)該是200
如果串行調(diào)度為 事務(wù)2 => 事務(wù)1,那么A最終應(yīng)該是250
由此可見,不同事務(wù)間,讀-寫、寫-寫都是沖突的,不加控制的寫操作,會(huì)導(dǎo)致并發(fā)調(diào)度不可串行化。
一、基于鎖實(shí)現(xiàn)可串行化
(本節(jié)以MySQL InnoDB為基本模型)
1. 讀鎖與寫鎖
實(shí)現(xiàn)可串行化的基石是控制沖突,強(qiáng)行保證沖突操作的串行化,那么應(yīng)該遵循以下原則:
- 讀-寫應(yīng)該排隊(duì)
- 寫-寫應(yīng)該排隊(duì)
讀的時(shí)候不能寫,寫的時(shí)候不能讀也不能寫,但是讀的時(shí)候可以讀,因?yàn)樽x不沖突,于是數(shù)據(jù)庫需要兩種鎖:
-
排它鎖(exclusive lock)
又稱X鎖,這是最好理解的鎖,在一般的并發(fā)編程中,我們?yōu)橘Y源加上的一般都是排它鎖,要獲取鎖,必須是資源處于未被加鎖狀態(tài),如果有人已經(jīng)為資源加鎖,則需要等待鎖釋放才能獲取鎖,這種鎖能夠保證并發(fā)時(shí)也能夠串行處理某個(gè)資源,實(shí)現(xiàn)排隊(duì)的目的。 -
共享鎖(share lock)
又稱S鎖,這是比排它鎖更加寬松的鎖,當(dāng)一個(gè)資源沒有被加鎖或者當(dāng)前加鎖為共享鎖時(shí),可以為它加上共享鎖,也就是一個(gè)資源可以同時(shí)被加無限個(gè)共享鎖。此時(shí)由于資源已經(jīng)被加鎖,雖然可以繼續(xù)加共享鎖,但是不能加排它鎖,需要等待資源的鎖被完全釋放才能獲取排它鎖。共享鎖的目的是為了提高非沖突操作的并發(fā)數(shù),同時(shí)能夠保證沖突操作的排隊(duì)執(zhí)行。
兼容性
這兩種鎖和讀、寫是什么關(guān)系呢?
讀寫都會(huì)加鎖,但是讀-讀可以并發(fā),寫則需要與任何操作排隊(duì),所以:
- 獲取記錄的共享鎖(S鎖),則僅允許事務(wù)讀取它,簡單來說共享鎖只是讀鎖,記錄被加讀鎖后,其他記錄也可以往上加讀鎖,也就是大家都可以讀。
- 獲取記錄的排它鎖(X鎖),則允許這個(gè)事務(wù)更新它,排它鎖讓事務(wù)既可以讀也可以寫,是讀寫通用的鎖,記錄被加排他鎖后,其他事務(wù)不論是想加排它鎖還是共享鎖,都需要排隊(duì)等待目前的排它鎖釋放才能加鎖。由于強(qiáng)行排隊(duì)的特性導(dǎo)致效率比較低,讀-讀不沖突所以大多數(shù)讀取都不會(huì)加排它鎖,不過在MySQL中可以使用SELECT FOR UPDATE語句指定為記錄加上排它鎖。
通過讀寫操作加鎖,實(shí)現(xiàn)了讀寫、寫寫的排隊(duì),但是靠簡單加鎖保證的排隊(duì),但排隊(duì)粒度太小,僅僅是操作與操作之間的排隊(duì),不足以解決上面圖中的不可串行化問題,因?yàn)槿绻聞?wù)1讀A后馬上釋放讀鎖,則事務(wù)2可以馬上獲取到A的寫鎖,改變A的值,還是會(huì)出現(xiàn)上面的不可串行化問題,因此事務(wù)需要保證更大粒度的排隊(duì)——如果一個(gè)記錄被某個(gè)事務(wù)讀取或者寫入,則直到這個(gè)事務(wù)提交,才能被別的事務(wù)修改
, 嚴(yán)格兩階段加鎖(Strict Two-Phase Locking) 由此誕生。
2. 嚴(yán)格兩階段加鎖(Strict Two-Phase Locking)
首先提一句什么是兩階段加鎖協(xié)議(2PL),它規(guī)定事務(wù)的加鎖與解鎖分為2個(gè)獨(dú)立階段,加鎖階段只能加鎖不能解鎖,一旦開始解鎖,則進(jìn)入解鎖階段,不能再加鎖。
嚴(yán)格兩階段加鎖(S2PL)在2PL的基礎(chǔ)上規(guī)定事務(wù)的解鎖階段只能是執(zhí)行commit或者rollback后,因此S2PL保證了一個(gè)事務(wù)曾經(jīng)讀取或?qū)懭氲挠涗洠诖耸聞?wù)commit或rollback前都不會(huì)被釋放鎖,因此不能被其他記錄加鎖,不會(huì)造成記錄的改變,由此實(shí)現(xiàn)了可串行化。
3. 多粒度加鎖與意向鎖(Intention Lock)
InnoDB中不止支持行級鎖,還支持表級鎖,為了兼容多粒度的鎖,設(shè)計(jì)了一種特殊的鎖——意向鎖(Intention Lock),它本身不具備鎖的功能,只承擔(dān)“指示”功能。
如果要加表級鎖,則必須保證行級鎖已完全釋放,整張表都沒有任何鎖時(shí),才能為表加上表鎖。那么問題來了,怎么判斷是否整張表的每一條記錄都已經(jīng)釋放鎖?
如果通過遍歷每條記錄的加鎖狀態(tài),未免效率太低,因此需要意向鎖,它只是一個(gè)指示牌,告訴數(shù)據(jù)庫,在此粒度之下有沒有被加鎖,被加了什么鎖。就像停車場會(huì)在門口立一個(gè)牌子指示“車位已滿”還是“內(nèi)有空余”,不需要開車進(jìn)去一個(gè)個(gè)車位檢查,提高了效率。
InnoDB如果要對一條記錄進(jìn)行加鎖,它需要先向表加上意向鎖,然后才能對記錄加普通鎖,獲取意向鎖失敗,則不能繼續(xù)向下獲取鎖。
意向鎖之間是完全兼容的,很好理解,因?yàn)橐庀蜴i只代表事務(wù)想向下獲取鎖,具體是哪條記錄不確定,因此意向鎖是完全兼容的,即使表上已經(jīng)被其他事務(wù)加了某種意向鎖,事務(wù)還是能夠成功為表加意向鎖。
一般我們不會(huì)在事務(wù)中加表鎖,表鎖效率太低,我們加的一般是行級鎖,行級鎖是加在某條特定的記錄上,我們稱之為記錄鎖。 這一節(jié)的內(nèi)容主要是對多粒度加鎖有個(gè)概念,現(xiàn)實(shí)中很少用表鎖。
上面說的共享鎖、排它鎖是按照鎖兼容性定義,表鎖、記錄鎖(Record Lock)則是按加鎖范圍定義,根據(jù)加鎖范圍不同,還有其他N種鎖,下面會(huì)提到一些。
4. 避免幻讀(Phantom)
間隙鎖(Gap Lock)
考慮一個(gè)例子:
事務(wù)1執(zhí)行“SELECT name FROM students WHERE age = 18”返回結(jié)果為“張三”,而事務(wù)2馬上插入一行記錄“INSERT INTO students VALUES("李四",18)”并提交,事務(wù)1再次執(zhí)行相同的SELECT語句,發(fā)現(xiàn)結(jié)果變?yōu)榱恕皬埲?“李四”,這就是幻讀,同一個(gè)事務(wù)進(jìn)行的兩次相同條件的讀取,卻讀取到了之前沒有讀到的記錄
。
有了記錄鎖雖然可以實(shí)現(xiàn)對已存在記錄進(jìn)行并發(fā)控制,也就是對于更新、刪除操作,再也不會(huì)有并發(fā)問題,但是無法對插入做并發(fā)控制,因?yàn)椴迦氩僮魇菍Σ淮嬖诘挠涗洠€不存在的記錄,我們無法為其加記錄鎖,因此可能會(huì)產(chǎn)生幻讀現(xiàn)象。
為了解決這個(gè)問題,出現(xiàn)了間隙鎖,間隙鎖也是加在某一條記錄上,可是它并不鎖住記錄本身,它只鎖住這條記錄與它的上一條記錄之間的間隙,防止插入
。
如下圖所示,如果一張表有主鍵為1、2、5的三條記錄,如果5被加上間隙鎖,只會(huì)鎖住開區(qū)間(2,5)間隙,而不會(huì)鎖住5這條記錄本身。
如果事務(wù)要插入記錄,需要獲取插入意向鎖(Insert Intention Lock),如果需要插入的間隙有間隙鎖,則獲取插入意向鎖會(huì)失敗必須進(jìn)行鎖等待,從而實(shí)現(xiàn)了阻塞插入。
在可串行化隔離級別,使用鎖住間隙去防止插入,從而避免了幻讀。
Next-Key Lock
很多時(shí)候需要鎖住多個(gè)間隙以及記錄本身,比如執(zhí)行“SELECT name FROM students WHERE id >= 1”,需要鎖住(1,3)、(3,5)、(6、7)以及1、3、5、7四條記錄本身:
間隙鎖和記錄鎖是兩種鎖結(jié)構(gòu),因此不能合并,如果為3個(gè)間隙分別加間隙鎖,4條記錄分別加記錄鎖,則會(huì)產(chǎn)生7條鎖記錄,很占用內(nèi)存,因此MySQL有一種鎖稱為Next-Key Lock,如果在小紅的記錄上面加Next-Key Lock,則會(huì)鎖住(1,3]這個(gè)前開后閉的區(qū)間,也就是鎖住了記錄本身+記錄之前的間隙,可以發(fā)現(xiàn),Next-Key Lock其實(shí)就是Gap Lock + Record Lock
。此時(shí)鎖結(jié)構(gòu)就可以簡化成為ID為1的記錄加上記錄鎖+后面連續(xù)的3個(gè)Next-Key Lock,由于Next-Key Lock類型相同并且連續(xù),可以將它們放入同一個(gè)鎖記錄,最后只有ID為1的記錄鎖+1個(gè)Next-Key Lock。
Next-Key Lock并沒有什么特別之處,只是對Record Lock + Gap Lock的一種簡化。
5. 舉一反三:并發(fā)問題之解
1. 臟寫(Dirty Write)
方案:事務(wù)寫記錄必須獲取排它鎖
原理:事務(wù)寫記錄之前獲取它的排它鎖,同時(shí)由于嚴(yán)格兩階段加鎖,在事務(wù)提交前都不會(huì)釋放鎖,因此完全避免了臟寫。
2. 臟讀(Dirty Read)
方案:事務(wù)寫記錄必須獲取排它鎖
原理:當(dāng)記錄被加上排它鎖后,是不允許再被加任何鎖的,因此任何事務(wù)都無法讀到其他事務(wù)寫入還未提交的數(shù)據(jù)。
3. 不可重復(fù)讀(Unrepeatable Read)
方案:事務(wù)讀記錄必須加鎖(S或X鎖均可)
原理:由于事務(wù)在讀記錄時(shí)已經(jīng)為記錄上鎖,因此其他事務(wù)無法再為這條記錄上排它鎖,因此根本無法修改這條記錄,也不會(huì)出現(xiàn)不可重復(fù)讀。
4. 幻讀(Phantom)
方案:間隙鎖
原理:間隙鎖阻塞了插入,因此也不會(huì)出現(xiàn)幻讀問題。
5. 讀偏差(Read Skew)
讀偏差需要再稍微解釋下,還是用上一篇提到的例子:比如X、Y兩個(gè)賬戶余額都為50,他們總和為100,事務(wù)A讀X余額為50,然后事務(wù)B從X轉(zhuǎn)賬50到Y(jié)然后提交,事務(wù)A在B提交后讀Y發(fā)現(xiàn)余額為100,那么它們總和變成了150,此時(shí)事務(wù)A讀到的數(shù)據(jù)違反業(yè)務(wù)一致性,為讀偏差。
可以發(fā)現(xiàn),讀偏差是由于業(yè)務(wù)一致性是由多條記錄的總狀態(tài)保證的,在事務(wù)A開啟并讀取了其中某一部分記錄后,事務(wù)B對A還沒有讀到的記錄進(jìn)行了修改并且B提交了,此時(shí)數(shù)據(jù)庫已經(jīng)進(jìn)入了新的一致狀態(tài),但是A在B提交后再去讀那部分記錄,讀到了B修改后的數(shù)據(jù),雖然此時(shí)數(shù)據(jù)庫事實(shí)上依舊處于一致狀態(tài),但是A卻發(fā)現(xiàn)多條記錄的總狀態(tài)不符合業(yè)務(wù)一致性,產(chǎn)生讀偏差
。
讀偏差的本質(zhì)是因?yàn)槭聞?wù)A有一部分是陳舊數(shù)據(jù),另一部分是新數(shù)據(jù),總狀態(tài)不一致。
方案:讀數(shù)據(jù)必須獲取鎖,寫數(shù)據(jù)必須加排它鎖
原理:由于事務(wù)在讀記錄時(shí)已經(jīng)加上了鎖,那么任何事務(wù)都不能再獲取排它鎖,也就不能更新這條已經(jīng)被讀過的數(shù)據(jù),那么對于事務(wù)自然不可能存在“陳舊數(shù)據(jù)”一說,任何被讀到的數(shù)據(jù),在它提交前都不可能被修改,因此讀到的都是最新數(shù)據(jù)。
6. 寫偏差(Write Skew)
上一篇有詳細(xì)講到寫偏差,這里就不多說,它與讀偏差本質(zhì)相同,都是因?yàn)樽x到的某一部分?jǐn)?shù)據(jù)成為了陳舊數(shù)據(jù),寫偏差使用陳舊數(shù)據(jù)作為寫前提,因此作出了錯(cuò)誤判斷,寫入了業(yè)務(wù)不一致的結(jié)果,因此解決寫偏差需要解決陳舊數(shù)據(jù)問題。
方案:讀數(shù)據(jù)必須獲取鎖,寫數(shù)據(jù)必須加排它鎖
原理:它與寫偏差的解決原理完全相同,都是因?yàn)榧渔i強(qiáng)制避免了事務(wù)讀取過的數(shù)據(jù)被修改,防止了陳舊數(shù)據(jù)的出現(xiàn)。
7. 丟失更新(Lost Updates)
丟失更新也在上一篇中有講到,大概就是事務(wù)A先讀X,對X進(jìn)行計(jì)算后再寫X,但是在寫X之前,已經(jīng)被事務(wù)B修改了X的值并提交了,而A不知道,將它認(rèn)為正確的X值寫入,覆蓋了事務(wù)B的值,此為丟失更新。
丟失更新的本質(zhì)也是基于陳舊數(shù)據(jù)做出修改決策,只不過陳舊記錄與被修改記錄為同一條記錄,這是和寫偏差的唯一區(qū)別。
方案:讀數(shù)據(jù)必須獲取鎖,寫數(shù)據(jù)必須加排它鎖
原理:它與避免讀、寫偏差完全相同的原理,避免記錄成為陳舊記錄。
可見,InnoDB中的可串行化隔離級別,基于鎖,避免了所有并發(fā)問題,是最安全的事務(wù)隔離級別,但是在業(yè)務(wù)開發(fā)中并不是每個(gè)并發(fā)問題我們都可能遇到,由于業(yè)務(wù)的獨(dú)特性,可能只會(huì)面臨某一些并發(fā)問題或者可以用其他方式去規(guī)避這些并發(fā)問題帶來的業(yè)務(wù)損害,而為了避免所有的并發(fā)問題去使用鎖,明顯是個(gè)收益很低的選擇,有時(shí)可以允許某些并發(fā)問題,減少鎖的使用,提高并發(fā)效率,下面會(huì)講到的MVCC就是個(gè)很好的替代品。
二、鎖的替代——使用MVCC提高并發(fā)度
可串行化雖然保證了事務(wù)的絕對安全,但是并發(fā)度很低,很多操作都需要排隊(duì)進(jìn)行,為了提高效率,SQL標(biāo)準(zhǔn)在隔離級別上進(jìn)行了妥協(xié),由此有了可重復(fù)讀、讀提交的隔離級別,它們都允許部分并發(fā)問題,這里先講可重復(fù)讀隔離級別。
SQL標(biāo)準(zhǔn)中,可重復(fù)讀僅僅需要完全避免臟寫、臟讀、不可重復(fù)讀三種異常,此時(shí)如果再用加鎖實(shí)現(xiàn),讀-寫排隊(duì)未免效率太低,于是MVCC誕生了。
MVCC全稱Multiple Version Concurrency Control,也就是多版本并發(fā)控制,重點(diǎn)在多版本,簡單來說,它為每個(gè)事務(wù)生成了一個(gè)快照,保證每個(gè)事務(wù)只能讀到自己的快照數(shù)據(jù),不論其他事務(wù)如何更新一條記錄,這個(gè)事務(wù)所讀到的數(shù)據(jù)都不會(huì)產(chǎn)生變化,也就是說,會(huì)為一條記錄保留多個(gè)版本,多個(gè)事務(wù)讀到的版本不同,MVCC代替了讀鎖,實(shí)現(xiàn)了讀-寫不阻塞
。
MVCC的意義只是替代讀鎖,寫依舊是加鎖的,這樣避免了臟寫,下面先講一下MVCC的實(shí)現(xiàn)思路,認(rèn)識MVCC如何避免并發(fā)問題,最后討論MVCC在并發(fā)中的局限性。
1. MVCC實(shí)現(xiàn)原理
版本鏈(Undo Log)
在MVCC中,每條記錄都有多個(gè)版本,串成了一個(gè)版本鏈,也就是說,記錄被UPDATE時(shí)并不是In Place Update,而是將記錄復(fù)制然后修改存一份到版本鏈,被DELET時(shí),也不是馬上從文件刪除,而是將記錄標(biāo)記為被刪除,它也是版本鏈的一環(huán)。
在InnoDB中每條記錄中都有2個(gè)隱藏列,1個(gè)是trx_id,一個(gè)是roll_pointer。
- trx_id代表這條記錄版本是被哪個(gè)事務(wù)創(chuàng)建的,數(shù)據(jù)庫有一個(gè)全局的事務(wù)ID分配器,它一定是遞增的,新的事務(wù)ID一定不會(huì)和舊的事務(wù)ID重復(fù)。
- roll_pointer是連接版本鏈的指針。
Read View
MVCC中最常聽到的概念就是快照,其實(shí)快照只是最終結(jié)果,而不是實(shí)現(xiàn)方式,快照 = 版本鏈 + Read View。
MVCC并不是將表中所有的記錄都為這個(gè)事務(wù)凍結(jié)了一份快照,而是在事務(wù)執(zhí)行第一條語句時(shí)時(shí)生成了一個(gè)叫做Read View的數(shù)據(jù)結(jié)構(gòu),注意,Read View是事務(wù)執(zhí)行語句時(shí)才會(huì)生成的,僅僅執(zhí)行start transaction是不會(huì)生成Read View的
。
Read View保存著以下信息:
Read View結(jié)合版本鏈?zhǔn)褂茫?dāng)事務(wù)讀取某條記錄時(shí),會(huì)根據(jù)此事務(wù)的Read View判斷此記錄的哪個(gè)版本是這個(gè)事務(wù)可見的:
- 如果記錄的trx_id與creator_trx_id相同,則代表這個(gè)版本是此事務(wù)創(chuàng)建的,可以讀取。
- 如果記錄的trx_id小于min_trx_id,代表這個(gè)版本是此事務(wù)生成Read View之前就已經(jīng)創(chuàng)建的,可以讀取。
- 如果記錄的trx_id大于等于max_trx_id,代表這個(gè)版本是此事務(wù)生成Read View之后開啟的事務(wù)創(chuàng)建的,一定不能被讀取。
- 如果記錄的trx_id處于min_trx_id與max_trx_id之間,則判斷trx_id是否在m_ids中,如果不在,則代表這個(gè)版本是此事務(wù)生成Read View時(shí)已經(jīng)提交的,可以讀取。
有了版本鏈和Read View,即使其他事務(wù)修改了記錄,先生成Read View的事務(wù)也不會(huì)讀到,只要Read View不改變,每次讀到的版本一定相同。MySQL中可重復(fù)讀和讀提交級別都基于MVCC,區(qū)別只是生成Read View的時(shí)機(jī)不同,可重復(fù)讀級別是在事務(wù)執(zhí)行第一個(gè)SQL時(shí)生成Read View,而讀提交級別是在事務(wù)每執(zhí)行一條SQL時(shí)都會(huì)重新生成Read View
。
2. MVCC的局限性
MVCC取代了讀鎖的位置,它不阻塞寫入雖然有提高效率的優(yōu)勢,但是同時(shí)也無法防止所有并發(fā)問題。
1. MVCC能避免幻讀嗎
事務(wù)是無法讀到Read View生成后別的事務(wù)產(chǎn)生的記錄版本,因此可以在不加間隙鎖的情況下也不會(huì)讀到別的事務(wù)的插入,那MVCC能避免幻讀嗎?
先說結(jié)論:MVCC不可以避免幻讀。
導(dǎo)致這個(gè)問題的根本原因是:InnoDB將Update、Insert、Delete都視為特殊操作,特殊操作對記錄進(jìn)行的是當(dāng)前讀(Current Read),也就是會(huì)讀取最新的記錄,也就是說Read View只對SELECT語句起作用。
如果users表中有id為1、2、3共3條記錄,事務(wù)A先讀,事務(wù)B插入一條記錄并提交,事務(wù)A更新被插入的記錄是可以成功的,因?yàn)閁PDATE是進(jìn)行當(dāng)前讀,更新時(shí)可以讀到id為4的記錄存在,因此可以成功更新,事務(wù)A成功更新id為4的記錄后,將在id為4的記錄版本鏈上新增一條事務(wù)A的版本,因此事務(wù)A再次SELECT,就可以名正言順地讀到這條記錄,符合Read View規(guī)則,但產(chǎn)生了幻讀。
如果要避免幻讀,可以使用MVCC+間隙鎖的方式。
2. 無法避免Read Skew與Write Skew
由于MVCC中讀-寫互不阻塞,因此事務(wù)讀取的快照可能已經(jīng)過期,讀到的可能已經(jīng)成為陳舊數(shù)據(jù),因此可能出現(xiàn)Read Skew與Write Skew。
3. 無法避免丟失更新
還是由于讀-寫不阻塞的特性:
R1(A) => R2(A) => W2(A) => W1(A)
事務(wù)1讀出的A值已經(jīng)過期,但是它不知道,還是根據(jù)舊的A值去更新A,最后覆蓋了事務(wù)2的寫入。
在Postgrel中,Repeatable Read級別就已經(jīng)避免了丟失更新,因?yàn)樗褂肕VCC+樂觀鎖,如果事務(wù)1去寫入A,存儲(chǔ)引擎檢測到A值已經(jīng)在事務(wù)1開啟后被別的事務(wù)修改過,則會(huì)報(bào)錯(cuò),阻止事務(wù)1的寫入。單純的MVCC并不能防止丟失更新,需要配合其他機(jī)制。
三、事務(wù)更佳實(shí)踐
在進(jìn)行業(yè)務(wù)開發(fā)時(shí)應(yīng)該先了解項(xiàng)目使用的數(shù)據(jù)庫的事務(wù)隔離級別以及其原理、表現(xiàn),然后根據(jù)事務(wù)實(shí)現(xiàn)原理去思考更好的編碼方式。
1. 避免死鎖
語句順序不同導(dǎo)致死鎖
這種情況大家一定很熟悉了:
因此建議在不同的業(yè)務(wù)中,盡量統(tǒng)一操作相同記錄語句的順序。
索引順序不同導(dǎo)致死鎖
鎖都是加在索引上的(這里最好先理解一下B+Tree索引),所以一條SQL如果涉及多個(gè)索引,會(huì)為每個(gè)索引加鎖,比如有一張users表(id,user_name,password),主鍵為id,在user_name上有一個(gè)唯一索引(Unique Index),以下語句:
UPDATE users SET user_name = 'j.huang@aftership.com' WHERE id = 1;
這條語句中涉及到了id與user_name兩個(gè)索引,InnoDB是索引組織表,主鍵是聚簇索引,因此記錄是存在主鍵聚簇索引結(jié)構(gòu)中的,那么這條SQL的加鎖順序?yàn)椋?/p>
- 為表加上IX鎖
- 為主鍵加上X鎖
- 為索引user_name加上X鎖
此時(shí)如果另一條事務(wù)執(zhí)行如下語句:
UPDATE users SET password = '123' WHERE user_name = 'j.huang@aftership.com';
則可能產(chǎn)生死鎖。
原因大家可以先思考一下。
這條語句的加鎖順序是:
- 找到user_name為'j.huang@aftership.com'的索引,加X鎖
- 為表加IX鎖
- 為主鍵加X鎖
他們都會(huì)對同一個(gè)主鍵索引加鎖和同一個(gè)二級索引,但是加鎖順序不同,因此可能造成死鎖,這種情況很難避免,MySQL中可以通過SHOW ENGINE INNODB STATUS
查看InnoDB的死鎖檢測情況。
2. 避免不必要的事務(wù)
其實(shí)很多業(yè)務(wù)場景并不需要事務(wù),比如說領(lǐng)取優(yōu)惠券,并不需要開啟一個(gè)Serializable級別的事務(wù)去SELECT優(yōu)惠券剩余數(shù)量,判斷是否有余量,再UPDATE領(lǐng)取優(yōu)惠券,完全可以一條語句解決:
UPDATE coupons SET balance = balance - 1 WHERE id = 1 and balance >= 1;
語句返回后判斷更新行數(shù),如果更新行數(shù)為1,則代表領(lǐng)取成功,更新行數(shù)為0,代表沒有符合條件的記錄,領(lǐng)取失敗。
(注意:這里只考慮領(lǐng)取優(yōu)惠券的場景,如果業(yè)務(wù)還需要將優(yōu)惠券寫入users表等其他一系列操作,就需要根據(jù)業(yè)務(wù)需求放入事務(wù))
3. 避免將不必要的SELECT放入事務(wù)
首先應(yīng)該理解將SELECT放入事務(wù)的意義是什么?
- 需要讀取事務(wù)自己的版本,則必須將SELECT放入事務(wù)
- 需要依賴SELECT結(jié)果作為其他語句的前提,此時(shí)不止要把SELECT放入事務(wù),還必須保證事務(wù)是Serializable級別的
如果不是以上兩個(gè)原因,則SELECT是沒有必要放入事務(wù)的,比如下單一件產(chǎn)品,如果只是SELECT它的product_name去寫入orders表,這種非強(qiáng)一致要求的數(shù)據(jù),沒有必要放入事務(wù),因?yàn)閜roduct_name即使被改變了,寫入order的product_name是1秒前的舊數(shù)據(jù),也是可以接受的。
4. 不要迷信事務(wù)
很多開發(fā)者誤以為將SELECT放入事務(wù),將結(jié)果作為判斷條件或者寫入條件是安全的,其實(shí)根據(jù)隔離級別不同,是不一定的,舉個(gè)例子:
- SELECT users表某個(gè)用戶等級信息,如果是鉆石會(huì)員,則為他3倍積分
- 將算出的積分UPDATE到user_scores表
將這兩條語句放入事務(wù)也不一定是安全的,這取決于事務(wù)的實(shí)現(xiàn),如果是InnoDB的Repeatable Read級別,那么這個(gè)事務(wù)是不安全的,因?yàn)镾ELECT讀到的是快照,在UPDATE之前,其他事務(wù)可能就已經(jīng)修改了user的等級信息,他可能已經(jīng)不滿足3倍積分條件,而此時(shí)再去UPDATE user_scores表,這個(gè)事務(wù)是個(gè)業(yè)務(wù)不安全的事務(wù)。
因此,要先了解事務(wù),再去使用,否則容易用錯(cuò)。