<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳解事務、隔離級別、悲觀鎖和樂觀鎖

          共 4928字,需瀏覽 10分鐘

           ·

          2021-05-06 17:31

          今天,我們來聊數(shù)據(jù)庫事務ACID、隔離級別、悲觀鎖和樂觀鎖。無論是在工作中,還是在筆試面試中,數(shù)據(jù)庫相關的問題,總是繞不開,不會的話,很容易歇菜,你懂的。

          數(shù)據(jù)庫事務場景

          在銀行系統(tǒng)中,數(shù)據(jù)庫事務是必須的。在電商系統(tǒng)中,也是如此。

          來看下A給B匯款100元的例子,可以看到,A賬戶扣款100元,此時如果進程崩潰或者機器掉電,那么這100元就沒有加到B的賬戶中,自然會導致用戶的強烈投訴:


          如果先給B賬戶加錢,然后給A賬戶扣錢,會怎樣呢?可以看到,此時如果進程崩潰或者機器掉電,銀行白白給B加了100元,而沒有扣減A的100元,只怕銀行會虧得沒褲子穿:

                

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

          如果要在應用層處理這些異常問題,將極為困難,甚至幾乎不可能。做過軟件開發(fā)的朋友應該知道,很多時候,如果異常問題處理得不妥當,將要投入大量時間分析和補救,且不一定能補救回來。

          所以,有必要引入數(shù)據(jù)庫事務。所謂事務,就是一組SQL操作,它們不可分割,不能被打斷,要么都成功,要么都失敗。具體地說,就是要滿足ACID性質。

          引入事務之后,應用層再也不用擔心上述異常了,因為數(shù)據(jù)庫已經(jīng)為我們處理得很好了。很多書籍把ACID放在一起敘述,我認為有點扯,因為他們并不正交。在我看來,C是AID的最終目的。下面,我們來看下ACID性質。



          Atomicity(原子性)

          古希臘哲學家德謨克利特認為,原子是構成世界萬物的單元,且不可分割:
          所以,原子性這個詞的含義就是不可分割。以上述的步驟一和步驟二為例,它們是一個整體,不可分割,要么同時成功,要么同時失敗。
          那么具體怎樣去實現(xiàn)原子性呢?有興趣的朋友可以了解下undo log, 在此不展開敘述。我們不是DBA, 不需要精通數(shù)據(jù)庫的眾多具體細節(jié),但是,至少要知道大概的原理和可行性,這可以為我們解決類似問題提供思路和參考。

          Consistency(一致性)

          一致性是我們最終的目的,籠統(tǒng)地說,一致性就是要確保數(shù)據(jù)是正確無誤的。所謂valid data, 其實就是正確無誤的data:
          原子性沒法完全保證一致性,因為在多個事務操作數(shù)據(jù)庫時,還需要涉及到隔離性。

          Isolation(隔離性)

          隔離性,就是要隔離不同事務,隔離性是本文的重點,我們會針對不同的隔離級別進行介紹,先來看一眼:
          需要強調的是,每種存儲引擎的實現(xiàn)不盡一致,在可重復讀隔離級別下,有的朋友在進行驗證時,并未出現(xiàn)所謂的幻讀,這是因為:
          • InnoDB通過MVCC部分地解決了幻讀問題:a. 針對select不會有幻讀;b. 針對select for update會有幻讀。

          • 其它很多數(shù)據(jù)庫引擎,還是存在幻讀問題。
          關于InnoDB是否存在幻讀問題,我們將在本文的實驗部分進行驗證。

          Durability(持久性)

          持久性的意思是,一旦事務提交,它對數(shù)據(jù)庫的變更是永久性的。實際上,事務提交后,最后不一定會落地到數(shù)據(jù)庫中(比如落地時機器斷電了),那怎么保證一定要落地成功呢?
          這就涉及到redo log了,我們也不需要具體知道redo log的細節(jié),但是,我們從邏輯上可以縷清:redo log要記錄什么?redo log為什么能保證持久性?
          很多時候,就是這樣,對于不太相關的東西,可以不精通,但至少要了解大概邏輯和思路。這樣才能說服自己,才不會有一種玄乎其玄的感覺。

          接下來,我們看這個問題:客戶端A的事務,是否應該看到客戶端B的事務所作的修改?這就涉及到數(shù)據(jù)庫事務的隔離級別。
          在本文中,如下圖示都是基于我的實際驗證。建議有興趣的朋友一起動手,感受一下。
          說明:事務A和事務B位于兩個不同的終端窗口,對應兩個不同的進程,在改變隔離級別時,僅改A的隔離級別來進行驗證。

          1.讀未提交

          我們來看看讀未提交的場景:
          可見,設置讀未提交后,事務B在未提交時,事務A讀出了a=10,  這是臟數(shù)據(jù)(B事務被回滾了),這就是所謂的“臟讀”。

          2.讀已提交

          我們來看看讀已提交的場景
          可見,設置讀已提交后,事務B在未提交時,事務A讀出了a=0, 在事務B提交后,又讀出了a=10,  出現(xiàn)了“不可重復讀”。

          3. 可重復讀

          我們來看看可重復讀的場景
          可以看到,看事務A內,讀取的值具有前后不變的特點,這就是“可重復讀”。只有當事務A提交后,才能讀出a=10. 在MySql中,默認的隔離級別就是可重復讀。

          接下來,我們看一個魔幻現(xiàn)象:
          在B事務提交后,A事務執(zhí)行select ... where a = 100時,發(fā)現(xiàn)還是無記錄,可見此時并未產(chǎn)生“幻讀”。但是,如果用select for update, 則出現(xiàn)了“幻讀”現(xiàn)象。
          可見,在InnoDB可重復讀的隔離級別中,并未完全解決“幻讀”問題,而是解決了讀數(shù)據(jù)情況下的“幻讀”問題,而對于修改的操作依然存在“幻讀”問題。

          4.串行化

          我們來看看串行化的場景
          可以看到,即使對于讀操作,也會加鎖,一個事務要等待另一個事務完成。串行化是完全的隔離級別,會導致大量超時和鎖競爭問題,在高并發(fā)場景中,較少用到串行化。在SQLite中,默認的隔離級別就是串行化。
               

          丟失更新問題

          有了這些隔離級別,就萬事大吉了嗎? 當然不是。以MySql為例,在默認隔離級別下,會有丟失更新的問題。
          領導A給你加了30元的雞腿,領導B給你加了40元的雞腿,最終結果發(fā)現(xiàn),只有40元雞腿,顯然,這是不合理的:
          怎么解決這種問題呢?可以考慮引入悲觀鎖或樂觀鎖。

          悲觀鎖

          所謂悲觀鎖,就是持悲觀態(tài)度,認為一定會有沖突,所以提前加強保護。悲觀鎖可以用select for update來實現(xiàn),之前項目中就經(jīng)常這樣玩,但后來重構了代碼,統(tǒng)一優(yōu)化成了分布式鎖。
          使用分布式鎖, 代碼示意如下(如下使用方法有問題):
          func proc() {money := queryMoneyFromDb()  begin lock     begin transaction         money += req.Money         setToDb(money)     end transaction  end lock}
          上述代碼的使用是有問題的,想一下為什么?
          當兩個進程都讀取money=0后,進程A獲取鎖,并且執(zhí)行完畢后,money=30,然后進程B獲取鎖,執(zhí)行完畢后,顯然可知,最后的結果是money=40,仍然存在丟失更新的問題。
          曾經(jīng)在項目中,就出現(xiàn)過這種錯誤,導致了低概率的金額不匹配,比較難發(fā)現(xiàn)問題,最后還是通過對賬發(fā)現(xiàn)了,然后查出上述錯誤的用法。
          正確使用悲觀鎖代碼示意如下:
          func proc() {    begin lock      begin transaction        money := queryMoneyFromDb()        money += req.Money        setToDb(money)      end transaction    end lock}

          樂觀鎖

          所謂樂觀鎖,就是抱有很樂觀的態(tài)度,也就是假定不會存在數(shù)據(jù)沖突(即使有沖突也不怕,樂觀得很)。具體實現(xiàn)時,可以在數(shù)據(jù)上打一個version標記,基于version進行控制,代碼示意如下:
          func proc() {   begin transaction      select * from T where user_id = 123456  // 假設查到的version為100      update T set money = xxx, version = version + 1 where user_id = 123456 and version = 100;   end transaction}
          分析一下:進程A和進程B都讀到了version=100的數(shù)據(jù),進程A在加完30元后,同時讓version變成了101;此時進程B去執(zhí)行,突然發(fā)現(xiàn)不滿足where version=100這個條件,所以更新失敗,這是合理的,符合預期,寧可執(zhí)行失敗,也不能產(chǎn)生數(shù)據(jù)錯誤。
          這里有一個極為微妙的問題:在MySql可重復讀隔離級別下,當進程A的update執(zhí)行成功并且提交事務后,version變?yōu)榱?01, 但是在進程B看來,version還是100(可重復讀),  為什么B在執(zhí)行update的時候,在where version=100條件下又無法真正執(zhí)行update呢?
          要注意,可重復讀是針對select而言的,而不是select for update或者update之類的操作,當A進程事務提交后,B進程事務看到的情況如下:
          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)
          可見,對B事務而言,用select看,看不到B事務的更新,這滿足事務的可重復讀。但是,當使用select for update時,能看到B事務的更新。
          所以,當B事務使用update嘗試更新where  version=100的記錄時,發(fā)現(xiàn)更新失敗,這是我們期望的結果,寧可執(zhí)行失敗,也不能產(chǎn)生數(shù)據(jù)錯誤。針對這種失敗,可以采用多次重試。

          至于悲觀鎖和樂觀鎖的選擇,還是要依賴于具體業(yè)務。數(shù)據(jù)的一致性如此重要,可千萬別把用戶的錢給算錯了。
          對于頻繁寫沖突的業(yè)務,用樂觀鎖肯定是不太好的,重試操作會增加各種開銷,此時可以考慮使用悲觀鎖。對于寫沖突較少發(fā)生的場景,那樂觀鎖就非常適合了。

          ·················· END ··················

          點擊關注公眾號,免費領學習資料

          你好,我是濤哥,CSDN排名第一。
          自學計算機,畢業(yè)后就職華為騰訊。
          從事軟件開發(fā),期待與你一起成長。
          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  嫩苞又嫩又紧AV无码 | 亚洲AV无码秘 翔田 | 亚洲91成人 | 五月天啪啪网 | 污污的啪啪网站 |