如何優(yōu)雅地回答 MySQL 的事務(wù)隔離級別和鎖的機(jī)制?
案例背景
MySQL 隔離級別
案例分析
案例解答
死鎖產(chǎn)生的四個(gè)必要條件:
案例背景
MySQL 的事務(wù)隔離級別(Isolation Level),是指:當(dāng)多個(gè)線程操作數(shù)據(jù)庫時(shí),數(shù)據(jù)庫要負(fù)責(zé)隔離操作,來保證各個(gè)線程在獲取數(shù)據(jù)時(shí)的準(zhǔn)確性。它分為四個(gè)不同的層次,按隔離水平高低排序,讀未提交 < 讀已提交 < 可重復(fù)度 < 串行化。
MySQL 隔離級別
讀未提交(Read uncommitted):隔離級別最低、隔離度最弱,臟讀、不可重復(fù)讀、幻讀三種現(xiàn)象都可能發(fā)生。所以它基本是理論上的存在,實(shí)際項(xiàng)目中沒有人用,但性能最高。
讀已提交(Read committed):它保證了事務(wù)不出現(xiàn)中間狀態(tài)的數(shù)據(jù),所有數(shù)據(jù)都是已提交且更新的,解決了臟讀的問題。但讀已提交級別依舊很低,它允許事務(wù)間可并發(fā)修改數(shù)據(jù),所以不保證再次讀取時(shí)能得到同樣的數(shù)據(jù),也就是還會存在不可重復(fù)讀、幻讀的可能。
可重復(fù)讀(Repeatable reads):MySQL InnoDB 引擎的默認(rèn)隔離級別,保證同一個(gè)事務(wù)中多次讀取數(shù)據(jù)的一致性,解決臟讀和不可重復(fù)讀,但仍然存在幻讀的可能。
可串行化(Serializable):選擇“可串行化”意味著讀取數(shù)據(jù)時(shí),需要獲取共享讀鎖;更新數(shù)據(jù)時(shí),需要獲取排他寫鎖;如果 SQL 使用 WHERE 語句,還會獲取區(qū)間鎖。換句話說,事務(wù) A 操作數(shù)據(jù)庫時(shí),事務(wù) B 只能排隊(duì)等待,因此性能也最低。
至于數(shù)據(jù)庫鎖,分為悲觀鎖和樂觀鎖,“悲觀鎖”認(rèn)為數(shù)據(jù)出現(xiàn)沖突的可能性很大,“樂觀鎖”認(rèn)為數(shù)據(jù)出現(xiàn)沖突的可能性不大。那悲觀鎖和樂觀鎖在基于 MySQL 數(shù)據(jù)庫的應(yīng)用開發(fā)中,是如何實(shí)現(xiàn)的呢?
悲觀鎖一般利用 SELECT … FOR UPDATE 類似的語句,對數(shù)據(jù)加鎖,避免其他事務(wù)意外修改數(shù)據(jù)。
樂觀鎖利用 CAS 機(jī)制,并不會對數(shù)據(jù)加鎖,而是通過對比數(shù)據(jù)的時(shí)間戳或者版本號,實(shí)現(xiàn)版本判斷。
案例分析
如果面試官想深挖候選人對數(shù)據(jù)庫內(nèi)部機(jī)制的掌握程度,切入點(diǎn)一般是 MySQL 的事務(wù)和鎖機(jī)制。接下來,我就從初中級研發(fā)工程師的角度出發(fā),從概念到實(shí)踐,帶你掌握“MySQL 事務(wù)和鎖機(jī)制”的高頻考點(diǎn):
舉例說明什么是臟讀、不可重復(fù)度和幻讀(三者雖然基礎(chǔ),但很多同學(xué)容易弄混)?
MySQL 是怎么解決臟讀、不可重復(fù)讀,和幻讀問題的?
你怎么理解死鎖?
……
案例解答
怎么理解臟讀、不可重復(fù)讀和幻讀?
臟讀:讀到了未提交事務(wù)的數(shù)據(jù)。
事務(wù)并發(fā)時(shí)的“臟讀”現(xiàn)象
假設(shè)有 A 和 B 兩個(gè)事務(wù),在并發(fā)情況下,事務(wù) A 先開始讀取商品數(shù)據(jù)表中的數(shù)據(jù),然后再執(zhí)行更新操作,如果此時(shí)事務(wù) A 還沒有提交更新操作,但恰好事務(wù) B 開始,然后也需要讀取商品數(shù)據(jù),此時(shí)事務(wù) B 查詢得到的是剛才事務(wù) A 更新后的數(shù)據(jù)。
如果接下來事務(wù) A 觸發(fā)了回滾,那么事務(wù) B 剛才讀到的數(shù)據(jù)就是過時(shí)的數(shù)據(jù),這種現(xiàn)象就是臟讀。
“臟讀”面試關(guān)注點(diǎn):
臟讀對應(yīng)的隔離級別是“讀未提交”,只有該隔離級別才會出現(xiàn)臟讀。
臟讀的解決辦法是升級事務(wù)隔離級別,比如“讀已提交”。
不可重復(fù)讀: 事務(wù) A 先讀取一條數(shù)據(jù),然后執(zhí)行邏輯的過程中,事務(wù) B 更新了這條數(shù)據(jù),事務(wù) A 再讀取時(shí),發(fā)現(xiàn)數(shù)據(jù)不匹配,這個(gè)現(xiàn)象就是“不可重復(fù)讀”。

事務(wù)并發(fā)時(shí)的“不可重復(fù)讀”現(xiàn)象
“不可重復(fù)讀”面試關(guān)注點(diǎn):
簡單理解是兩次讀取的數(shù)據(jù)中間被修改,對應(yīng)的隔離級別是“讀未提交”或“讀已提交”。
不可重復(fù)讀的解決辦法就是升級事務(wù)隔離級別,比如“可重復(fù)度”。
幻讀: 在一個(gè)事務(wù)內(nèi),同一條查詢語句在不同時(shí)間段執(zhí)行,得到不同的結(jié)果集。

事務(wù)并發(fā)時(shí)的“幻讀”現(xiàn)象
事務(wù) A 讀了一次商品表,得到最后的 ID 是 3,事務(wù) B 也同樣讀了一次,得到最后 ID 也是 3。接下來事務(wù) A 先插入了一行,然后讀了一下最新的 ID 是 4,剛好是前面 ID 3 加上 1,然后事務(wù) B 也插入了一行,接著讀了一下最新的 ID 發(fā)現(xiàn)是 5,而不是 3 加 1。
這時(shí),你發(fā)現(xiàn)在使用 ID 做判斷或做關(guān)鍵數(shù)據(jù)時(shí),就會出現(xiàn)問題,這種現(xiàn)象就像是讓事務(wù) B 產(chǎn)生了幻覺一樣,讀取到了一個(gè)意想不到的數(shù)據(jù),所以叫幻讀。當(dāng)然,不僅僅是新增,刪除、修改數(shù)據(jù)也會發(fā)生類似的情況。
“幻讀”面試關(guān)注點(diǎn):
要想解決幻讀不能升級事務(wù)隔離級別到“可串行化”,那樣數(shù)據(jù)庫也失去了并發(fā)處理能力。
行鎖解決不了幻讀,因?yàn)榧词规i住所有記錄,還是阻止不了插入新數(shù)據(jù)。
解決幻讀的辦法是鎖住記錄之間的“間隙”,為此 MySQL InnoDB 引入了新的鎖,叫間隙鎖(Gap Lock),所以在面試中,你也要掌握間隙鎖,以及間隙鎖與行鎖結(jié)合的 next-key lock 鎖。
怎么理解死鎖
除了事務(wù)隔離級別,很多同學(xué)在面試時(shí),經(jīng)常會被面試官直奔主題地問:“談?wù)勀銓λ梨i的理解”。要回答這樣開放的問題,你就要在腦海中梳理出系統(tǒng)化的回答思路:死鎖是如何產(chǎn)生的,如何避免死鎖。
死鎖一般發(fā)生在多線程(兩個(gè)或兩個(gè)以上)執(zhí)行的過程中。因?yàn)闋帄Z資源造成線程之間相互等待,這種情況就產(chǎn)生了死鎖。我在 06 講也提到了死鎖,但是并沒有講它產(chǎn)生的原因以及怎么避免,所以接下來我們就來了解這部分內(nèi)容。

線程死鎖
比如你有資源 1 和 2,以及線程 A 和 B,當(dāng)線程 A 在已經(jīng)獲取到資源 1 的情況下,期望獲取線程 B 持有的資源 2。與此同時(shí),線程 B 在已經(jīng)獲取到資源 2 的情況下,期望獲取現(xiàn)場 A 持有的資源 1。
那么線程 A 和線程 B 就處理了相互等待的死鎖狀態(tài),在沒有外力干預(yù)的情況下,線程 A 和線程 B 就會一直處于相互等待的狀態(tài),從而不能處理其他的請求。
死鎖產(chǎn)生的四個(gè)必要條件:
互斥:多個(gè)線程不能同時(shí)使用一個(gè)資源。比如線程 A 已經(jīng)持有的資源,不能再同時(shí)被線程 B 持有。如果線程 B 請求獲取線程 A 已經(jīng)占有的資源,那線程 B 只能等待這個(gè)資源被線程 A 釋放。

持有并等待:當(dāng)線程 A 已經(jīng)持有了資源 1,又提出申請資源 2,但是資源 2 已經(jīng)被線程 C 占用,所以線程 A 就會處于等待狀態(tài),但它在等待資源 2 的同時(shí)并不會釋放自己已經(jīng)獲取的資源 1。

不可剝奪:線程 A 獲取到資源 1 之后,在自己使用完之前不能被其他線程(比如線程 B)搶占使用。如果線程 B 也想使用資源 1,只能在線程 A 使用完后,主動釋放后再獲取。

循環(huán)等待:發(fā)生死鎖時(shí),必然會存在一個(gè)線程,也就是資源的環(huán)形鏈。比如線程 A 已經(jīng)獲取了資源 1,但同時(shí)又請求獲取資源 2。線程 B 已經(jīng)獲取了資源 2,但同時(shí)又請求獲取資源 1,這就會形成一個(gè)線程和資源請求等待的環(huán)形圖。
死鎖只有同時(shí)滿足互斥、持有并等待、不可剝奪、循環(huán)等待時(shí)才會發(fā)生。并發(fā)場景下一旦死鎖,一般沒有特別好的方法,很多時(shí)候只能重啟應(yīng)用。因此,最好是規(guī)避死鎖,那么具體怎么做呢?答案是:至少破壞其中一個(gè)條件(互斥必須滿足,你可以從其他三個(gè)條件出發(fā))。
持有并等待:我們可以一次性申請所有的資源,這樣就不存在等待了。
不可剝奪:占用部分資源的線程進(jìn)一步申請其他資源時(shí),如果申請不到,可以主動釋放它占有的資源,這樣不可剝奪這個(gè)條件就破壞掉了。
循環(huán)等待:可以靠按序申請資源來預(yù)防,也就是所謂的資源有序分配原則,讓資源的申請和使用有線性順序,申請的時(shí)候可以先申請資源序號小的,再申請資源序號大的,這樣的線性化操作就自然就不存在循環(huán)了。
— 【 THE END 】— 本公眾號全部博文已整理成一個(gè)目錄,請?jiān)诠娞柪锘貜?fù)「m」獲取! 最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
