今天,我們來(lái)聊數(shù)據(jù)庫(kù)事務(wù)ACID、隔離級(jí)別、悲觀鎖和樂(lè)觀鎖。無(wú)論是在工作中,還是在筆試面試中,數(shù)據(jù)庫(kù)相關(guān)的問(wèn)題,總是繞不開(kāi),不會(huì)的話,很容易歇菜,你懂的。數(shù)據(jù)庫(kù)事務(wù)場(chǎng)景
在銀行系統(tǒng)中,數(shù)據(jù)庫(kù)事務(wù)是必須的。在電商系統(tǒng)中,也是如此。
來(lái)看下A給B匯款100元的例子,可以看到,A賬戶扣款100元,此時(shí)如果進(jìn)程崩潰或者機(jī)器掉電,那么這100元就沒(méi)有加到B的賬戶中,自然會(huì)導(dǎo)致用戶的強(qiáng)烈投訴:

如果先給B賬戶加錢(qián),然后給A賬戶扣錢(qián),會(huì)怎樣呢?可以看到,此時(shí)如果進(jìn)程崩潰或者機(jī)器掉電,銀行白白給B加了100元,而沒(méi)有扣減A的100元,只怕銀行會(huì)虧得沒(méi)褲子穿:

墨菲定律說(shuō):凡是會(huì)出錯(cuò)的事,一定會(huì)出錯(cuò)。 而且,一旦發(fā)生,將造成較大危害。所以,在軟件設(shè)計(jì)上,有必要考慮這種異常。進(jìn)程崩潰,機(jī)房掉電,網(wǎng)絡(luò)抖動(dòng),硬件損壞,都應(yīng)該被視為常態(tài),都應(yīng)該被考慮到。

如果要在應(yīng)用層處理這些異常問(wèn)題,將極為困難,甚至幾乎不可能。做過(guò)軟件開(kāi)發(fā)的朋友應(yīng)該知道,很多時(shí)候,如果異常問(wèn)題處理得不妥當(dāng),將要投入大量時(shí)間分析和補(bǔ)救,且不一定能補(bǔ)救回來(lái)。
所以,有必要引入數(shù)據(jù)庫(kù)事務(wù)。所謂事務(wù),就是一組SQL操作,它們不可分割,不能被打斷,要么都成功,要么都失敗。具體地說(shuō),就是要滿足ACID性質(zhì)。
引入事務(wù)之后,應(yīng)用層再也不用擔(dān)心上述異常了,因?yàn)閿?shù)據(jù)庫(kù)已經(jīng)為我們處理得很好了。很多書(shū)籍把ACID放在一起敘述,我認(rèn)為有點(diǎn)扯,因?yàn)樗麄儾⒉徽弧T谖铱磥?lái),C是AID的最終目的。下面,我們來(lái)看下ACID性質(zhì)。
Atomicity(原子性)
古希臘哲學(xué)家德謨克利特認(rèn)為,原子是構(gòu)成世界萬(wàn)物的單元,且不可分割:
所以,原子性這個(gè)詞的含義就是不可分割。以上述的步驟一和步驟二為例,它們是一個(gè)整體,不可分割,要么同時(shí)成功,要么同時(shí)失敗。那么具體怎樣去實(shí)現(xiàn)原子性呢?有興趣的朋友可以了解下undo log, 在此不展開(kāi)敘述。我們不是DBA, 不需要精通數(shù)據(jù)庫(kù)的眾多具體細(xì)節(jié),但是,至少要知道大概的原理和可行性,這可以為我們解決類(lèi)似問(wèn)題提供思路和參考。Consistency(一致性)
一致性是我們最終的目的,籠統(tǒng)地說(shuō),一致性就是要確保數(shù)據(jù)是正確無(wú)誤的。所謂valid data, 其實(shí)就是正確無(wú)誤的data:原子性沒(méi)法完全保證一致性,因?yàn)樵诙鄠€(gè)事務(wù)操作數(shù)據(jù)庫(kù)時(shí),還需要涉及到隔離性。
Isolation(隔離性)
隔離性,就是要隔離不同事務(wù),隔離性是本文的重點(diǎn),我們會(huì)針對(duì)不同的隔離級(jí)別進(jìn)行介紹,先來(lái)看一眼:
需要強(qiáng)調(diào)的是,每種存儲(chǔ)引擎的實(shí)現(xiàn)不盡一致,在可重復(fù)讀隔離級(jí)別下,有的朋友在進(jìn)行驗(yàn)證時(shí),并未出現(xiàn)所謂的幻讀,這是因?yàn)椋?/span>關(guān)于InnoDB是否存在幻讀問(wèn)題,我們將在本文的實(shí)驗(yàn)部分進(jìn)行驗(yàn)證。
Durability(持久性)
持久性的意思是,一旦事務(wù)提交,它對(duì)數(shù)據(jù)庫(kù)的變更是永久性的。實(shí)際上,事務(wù)提交后,最后不一定會(huì)落地到數(shù)據(jù)庫(kù)中(比如落地時(shí)機(jī)器斷電了),那怎么保證一定要落地成功呢?
這就涉及到redo log了,我們也不需要具體知道redo log的細(xì)節(jié),但是,我們從邏輯上可以縷清:redo log要記錄什么?redo log為什么能保證持久性?很多時(shí)候,就是這樣,對(duì)于不太相關(guān)的東西,可以不精通,但至少要了解大概邏輯和思路。這樣才能說(shuō)服自己,才不會(huì)有一種玄乎其玄的感覺(jué)。接下來(lái),我們看這個(gè)問(wèn)題:客戶端A的事務(wù),是否應(yīng)該看到客戶端B的事務(wù)所作的修改?這就涉及到數(shù)據(jù)庫(kù)事務(wù)的隔離級(jí)別。
在本文中,如下圖示都是基于我的實(shí)際驗(yàn)證。建議有興趣的朋友一起動(dòng)手,感受一下。說(shuō)明:事務(wù)A和事務(wù)B位于兩個(gè)不同的終端窗口,對(duì)應(yīng)兩個(gè)不同的進(jìn)程,在改變隔離級(jí)別時(shí),僅改A的隔離級(jí)別來(lái)進(jìn)行驗(yàn)證。1.讀未提交
我們來(lái)看看讀未提交的場(chǎng)景:
可見(jiàn),設(shè)置讀未提交后,事務(wù)B在未提交時(shí),事務(wù)A讀出了a=10, 這是臟數(shù)據(jù)(B事務(wù)被回滾了),這就是所謂的“臟讀”。
2.讀已提交
我們來(lái)看看讀已提交的場(chǎng)景:可見(jiàn),設(shè)置讀已提交后,事務(wù)B在未提交時(shí),事務(wù)A讀出了a=0, 在事務(wù)B提交后,又讀出了a=10, 出現(xiàn)了“不可重復(fù)讀”。3. 可重復(fù)讀
我們來(lái)看看可重復(fù)讀的場(chǎng)景:可以看到,看事務(wù)A內(nèi),讀取的值具有前后不變的特點(diǎn),這就是“可重復(fù)讀”。只有當(dāng)事務(wù)A提交后,才能讀出a=10. 在MySql中,默認(rèn)的隔離級(jí)別就是可重復(fù)讀。接下來(lái),我們看一個(gè)魔幻現(xiàn)象:在B事務(wù)提交后,A事務(wù)執(zhí)行select ... where a = 100時(shí),發(fā)現(xiàn)還是無(wú)記錄,可見(jiàn)此時(shí)并未產(chǎn)生“幻讀”。但是,如果用select for update, 則出現(xiàn)了“幻讀”現(xiàn)象。可見(jiàn),在InnoDB可重復(fù)讀的隔離級(jí)別中,并未完全解決“幻讀”問(wèn)題,而是解決了讀數(shù)據(jù)情況下的“幻讀”問(wèn)題,而對(duì)于修改的操作依然存在“幻讀”問(wèn)題。4.串行化
可以看到,即使對(duì)于讀操作,也會(huì)加鎖,一個(gè)事務(wù)要等待另一個(gè)事務(wù)完成。串行化是完全的隔離級(jí)別,會(huì)導(dǎo)致大量超時(shí)和鎖競(jìng)爭(zhēng)問(wèn)題,在高并發(fā)場(chǎng)景中,較少用到串行化。在SQLite中,默認(rèn)的隔離級(jí)別就是串行化。丟失更新問(wèn)題
有了這些隔離級(jí)別,就萬(wàn)事大吉了嗎? 當(dāng)然不是。以MySql為例,在默認(rèn)隔離級(jí)別下,會(huì)有丟失更新的問(wèn)題。領(lǐng)導(dǎo)A給你加了30元的雞腿,領(lǐng)導(dǎo)B給你加了40元的雞腿,最終結(jié)果發(fā)現(xiàn),只有40元雞腿,顯然,這是不合理的:怎么解決這種問(wèn)題呢?可以考慮引入悲觀鎖或樂(lè)觀鎖。悲觀鎖
所謂悲觀鎖,就是持悲觀態(tài)度,認(rèn)為一定會(huì)有沖突,所以提前加強(qiáng)保護(hù)。悲觀鎖可以用select for update來(lái)實(shí)現(xiàn),之前項(xiàng)目中就經(jīng)常這樣玩,但后來(lái)重構(gòu)了代碼,統(tǒng)一優(yōu)化成了分布式鎖。使用分布式鎖, 代碼示意如下(如下使用方法有問(wèn)題):func proc() {money := queryMoneyFromDb() begin lock begin transaction money += req.Money setToDb(money) end transaction end lock}
上述代碼的使用是有問(wèn)題的,想一下為什么?當(dāng)兩個(gè)進(jìn)程都讀取money=0后,進(jìn)程A獲取鎖,并且執(zhí)行完畢后,money=30,然后進(jìn)程B獲取鎖,執(zhí)行完畢后,顯然可知,最后的結(jié)果是money=40,仍然存在丟失更新的問(wèn)題。曾經(jīng)在項(xiàng)目中,就出現(xiàn)過(guò)這種錯(cuò)誤,導(dǎo)致了低概率的金額不匹配,比較難發(fā)現(xiàn)問(wèn)題,最后還是通過(guò)對(duì)賬發(fā)現(xiàn)了,然后查出上述錯(cuò)誤的用法。func proc() { begin lock begin transaction money := queryMoneyFromDb() money += req.Money setToDb(money) end transaction end lock}
樂(lè)觀鎖
所謂樂(lè)觀鎖,就是抱有很樂(lè)觀的態(tài)度,也就是假定不會(huì)存在數(shù)據(jù)沖突(即使有沖突也不怕,樂(lè)觀得很)。具體實(shí)現(xiàn)時(shí),可以在數(shù)據(jù)上打一個(gè)version標(biāo)記,基于version進(jìn)行控制,代碼示意如下:func proc() { begin transaction select * from T where user_id = 123456 update T set money = xxx, version = version + 1 where user_id = 123456 and version = 100; end transaction}
分析一下:進(jìn)程A和進(jìn)程B都讀到了version=100的數(shù)據(jù),進(jìn)程A在加完30元后,同時(shí)讓version變成了101;此時(shí)進(jìn)程B去執(zhí)行,突然發(fā)現(xiàn)不滿足where version=100這個(gè)條件,所以更新失敗,這是合理的,符合預(yù)期,寧可執(zhí)行失敗,也不能產(chǎn)生數(shù)據(jù)錯(cuò)誤。
這里有一個(gè)極為微妙的問(wèn)題:在MySql可重復(fù)讀隔離級(jí)別下,當(dāng)進(jìn)程A的update執(zhí)行成功并且提交事務(wù)后,version變?yōu)榱?01, 但是在進(jìn)程B看來(lái),version還是100(可重復(fù)讀), 為什么B在執(zhí)行update的時(shí)候,在where version=100條件下又無(wú)法真正執(zhí)行update呢?
要注意,可重復(fù)讀是針對(duì)select而言的,而不是select for update或者update之類(lèi)的操作,當(dāng)A進(jìn)程事務(wù)提交后,B進(jìn)程事務(wù)看到的情況如下:mysql> select * from user;+----+-------+---------+| id | money | version |+----+-------+---------+| 1 | 0 | 100 |+----+-------+---------+1 row in set (0.00 sec)
mysql> select * from user for update;+----+-------+---------+| id | money | version |+----+-------+---------+| 1 | 30 | 101 |+----+-------+---------+1 row in set (0.25 sec)
mysql> select * from user;+----+-------+---------+| id | money | version |+----+-------+---------+| 1 | 0 | 100 |+----+-------+---------+1 row in set (0.00 sec)
可見(jiàn),對(duì)B事務(wù)而言,用select看,看不到B事務(wù)的更新,這滿足事務(wù)的可重復(fù)讀。但是,當(dāng)使用select for update時(shí),能看到B事務(wù)的更新。所以,當(dāng)B事務(wù)使用update嘗試更新where version=100的記錄時(shí),發(fā)現(xiàn)更新失敗,這是我們期望的結(jié)果,寧可執(zhí)行失敗,也不能產(chǎn)生數(shù)據(jù)錯(cuò)誤。針對(duì)這種失敗,可以采用多次重試。
至于悲觀鎖和樂(lè)觀鎖的選擇,還是要依賴(lài)于具體業(yè)務(wù)。數(shù)據(jù)的一致性如此重要,可千萬(wàn)別把用戶的錢(qián)給算錯(cuò)了。對(duì)于頻繁寫(xiě)沖突的業(yè)務(wù),用樂(lè)觀鎖肯定是不太好的,重試操作會(huì)增加各種開(kāi)銷(xiāo),此時(shí)可以考慮使用悲觀鎖。對(duì)于寫(xiě)沖突較少發(fā)生的場(chǎng)景,那樂(lè)觀鎖就非常適合了。·················· END ··················
點(diǎn)擊關(guān)注公眾號(hào),免費(fèi)領(lǐng)學(xué)習(xí)資料
自學(xué)計(jì)算機(jī),畢業(yè)后就職華為騰訊。從事軟件開(kāi)發(fā),期待與你一起成長(zhǎng)。點(diǎn)“贊”和“在看”哦