數據庫9種鎖、3種讀、4種隔離級別一次性串聯起來,用15張圖呈現背后數據庫事務背后的并發原理

      網友投稿 820 2025-04-01

      前段時間開發時,正好遇到了2個進程同時更新一行記錄時引發的bug,雖然問題最終解決了,但自己對背后的運行邏輯仍舊一頭霧水。事后嘗試簡單翻了下各種博客資料,還有《高性能mysql》那本書時,發現大部分是將一堆八股文概念堆砌在一起,很少完整串聯過這堆概念。

      于是我重新完整學習了這些概念和底層原理, 通過一個轉賬問題的場景,將這些概念全部關聯起來。

      將下面這些數據庫的概念單獨拿出來時,相信很多人都有了解或者記憶過,但是將這些概念全部串聯在一起時,可能就會很混亂。

      我這里舉個例子:

      排他鎖、共享鎖

      行鎖、表鎖、意向鎖、間隙鎖、next-key鎖

      悲觀鎖、樂觀鎖

      兩階段鎖協議

      LCBB鎖并發控制協議、MVCC多版本控制協議

      臟讀、不可重復讀、幻讀

      RU\RC\RR\SE隔離級別

      然后自己問自己一個問題:

      這一堆鎖的關聯關系究竟是什么?

      各隔離級別究竟是怎么用各種鎖+MVCC來解決事務讀問題的?

      首先,我們完全不考慮數據庫引擎、隔離級別設置之類的,就當作你用一個超簡陋的兒科級別數據庫來存放和更新數據。

      假設你的商城服務正好在同時執行如下的2種事情

      張三給窮光蛋李四轉賬100元。

      李四嘗試下單購買100元的衣服

      李四在最開始余額只有0元錢。

      注意因為是同時執行,在沒有做任何保護的情況下,就可能會出現下圖這樣的情況

      可以看到李四明明沒有錢,卻扣費了,變成了很奇怪的-100元。

      Q: 那這個有問題的讀過程叫什么?

      A: 這個過程就叫做臟讀。 即更新回退的時,另一個事務讀到了臟數據,判斷失誤,導致做了錯誤的處理。

      根本原因是2個事務都是先查后扣,卻沒有提前保護的形式

      Q: 在不修改數據庫隔離級別的情況下, 我們可以如何用sql語句手動解決這個臟讀?

      A: 那很顯然就是加鎖對事務過程做提前保護, 不讓B去判斷和扣費。

      sql語句里有個 ”for update“ 語法, 會手動鎖住李四那一行,在調用commit后釋放

      具體見下面綠色的標注部分:

      Q: 剛才看到”鎖住李四這一行“, 那么這個就叫行級鎖。

      什么情況下會變成鎖住整個表?

      A:

      name ='李四’這句話, 如果name是索引列的話,就會加行鎖

      如果不是索引列, 就會變成表鎖。

      換言之, 行鎖的本質是在索引節點上加鎖

      如果無法在索引節點上加鎖,那就會直接變成整張表的鎖,代價就會很大。

      另外表鎖也可以單獨用lock table的語法手動加鎖

      Q: 如果一個事務A申請了行鎖,鎖住某一行, 另一個事務B申請了表鎖,那B會被阻塞嗎?

      A:

      B事務既然申請表鎖,說明可能會用到A中的每一行。

      B申請的流程可以是下面這樣:

      判斷表是否已被其他事務用表鎖鎖表

      判斷表中的每一行是否已被行鎖鎖住。

      但2這一步也太耗時了。

      因此A申請行鎖前,會優先申請一個意向鎖,再申請行鎖。

      然后B申請時,第2步改成判斷意向鎖即可,有意向鎖就阻塞。

      簡單點說, 意向鎖就是行鎖操作用來阻塞表鎖用的。 但行鎖和行鎖之間不會互相阻塞,除非行有沖突。

      剛才看到的for update會限制其他并行事務的所有讀寫操作,而且是2個事務上都加了”for update“。

      那么這個鎖就叫做”排他鎖“, 屬于非常強勢的鎖, 相當于其他讀寫操作馬上全部攔住了。

      這里使用排他鎖來解決臟讀的原因是因為后面有查詢余額+扣余額的代碼,寫這段代碼的人必須做提前保護,以避免自己讀到一個可能被修改的數據,導致判斷和修改失誤。

      和排他鎖對應的是“共享鎖”,也就是熟知的讀寫鎖。

      可以讓多個事務同時讀,但是不允許修改 。

      手動加共享鎖的方式:把for update改成 lock in share mode即可

      Q: 那么什么時候使用共享鎖比排他鎖要好呢?

      A:

      可以看下面的例子:

      可以看到沒有查自身+更新自身的操作, 僅僅是查+更新其他表,表之間也互不關聯,對余額的實時性也不是要求太高。

      如果都加排他鎖,各種select操作就會很慢。

      但如果不加共享鎖, T6這邊刪除時,就可能產生冗余數據,所以還是得加鎖。

      Q: 那我加的共享鎖(S鎖)和排他鎖(X)什么時候釋放呢?是每次執行完update馬上釋放嗎?

      A:

      這里就涉及了“兩階段鎖”協議。

      加鎖階段:在該階段可以進行加鎖操作。在對任何數據進行讀操作之前要申請并獲得S鎖(共享鎖,其它事務可以繼續加共享鎖,但不能加排它鎖),在進行寫操作之前要申請并獲得X鎖(排它鎖,其它事務不能再獲得任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。

      解鎖階段:當事務釋放了一個封鎖以后,事務進入解鎖階段,在該階段只能進行解鎖操作不能再進行加鎖操作。

      說人話, 就是在事務中需要加鎖時再加鎖, 直到commit完一次性解鎖。

      為什么要兩階段鎖,看到的一句話是

      若并發執行的所有事務均遵守兩段鎖協議,則對這些事務的任何并發調度策略都是可串行化的。

      Q: 兩階段鎖協議可以避免死鎖嗎?

      A:

      不能避免,但是可以通過死鎖檢測算法進行事務解除。

      重新回到張三李四轉賬+下單的場景上來。

      for update這種鎖,其實也是一種“悲觀鎖” ,加鎖解鎖比較耗時, 默認經常發生競爭。

      將數據庫9種鎖、3種讀、4種隔離級別一次性串聯起來,用15張圖呈現背后數據庫事務背后的并發原理

      但如果我的轉賬和下單過程要求非常快,每次只有幾毫秒,那加悲觀鎖成本就太大了

      這時候就可以手動使用樂觀鎖, 需要你自己在余額表里增加version列,增加后如下所示:

      這樣就不需要特地加鎖了,每次循環判斷即可,前提是沖突發生概率比較低,阻塞時間比較短。

      剛才一個小小的臟讀,就已經解決了下面3個問題

      排他鎖和共享鎖的區別:前者是拒絕所有讀寫 , 后者是允許并發讀拒絕寫

      行鎖和表鎖的區別: 前者是對單行加鎖 , 后者是對整表加鎖, 區別是 是否涉及索引

      悲觀鎖和樂觀鎖的區別: 前者主動用數據庫自帶的鎖, 后者自己添加version版本號

      外加一個兩階段鎖協議

      繼續回到臟讀問題, 前面我們學習的所有概念,都是和數據庫自身隔離級別無關,使用數據庫的鎖語法或者version版本號來避免。

      但數據庫發展這么強大,怎么可能需要我們頻繁自己寫這種復雜邏輯,于是數據庫誕生了隔離級別設置。

      前面會發生臟讀的隔離級別, 叫做RU(read uncommited)

      即RU級別時, 我可以在別的事務沒完全commit好時就讀到數據。

      Q: 先來個小問題,RU級別沒有任何鎖,對嗎?

      A:

      錯誤, RU級別做update等增刪改操作時,仍然會默認在事務更新操作中增加排他鎖,避免update沖突。

      切記臟讀的發生原因,是查詢+更新+回滾時沒加鎖導致其他查詢操作出現失誤判斷。

      即查詢這塊可能讀到沒提交的數據,導致錯誤,而不是更新的并發問題。

      Q: 當我們的數據庫被設置成RC級別(Read commited)時, 可以解決臟讀, 那么背后是怎么解決的呢?

      A:

      業界有兩種方式

      LBCC基于鎖的并發控制(Lock-Based Concurrency Control))

      MVCC基于多版本的并發控制協議(Multi-Version Concurrency Control)

      LBCC其實就是類似前面手動用悲觀鎖的方式, 事務操作中查詢時默認試圖加鎖,因此就可能被update的排他鎖阻塞住,避免了臟讀。

      但代價就是效率很低。很多場景下,select的次數是遠大于update的。

      所以InnoDb 基于樂觀鎖的概念, 想了一個MVCC,自己在事務的背后實現了一套類似樂觀鎖的機制來處理這種情況。 確保了盡可能不在讀操作上加鎖, 排他鎖只對更新操作生效。

      Q: MVCC究竟是怎么做的呢?

      A:

      簡單來說,就是默認給每個數據行加了一個版本號列TRX_ID和回滾版本鏈ROLL_BT,具體可以看《高性能mysql》書里的這段描述:

      簡而言之

      查的時候,只查當前事務之前的記錄,或者回滾版本比當前大的已刪記錄。

      增的時候,加新版本的記錄

      刪的時候,把老記錄標記上回滾版本

      改的時候,本質上是加新記錄, 同時把老記錄標上回滾版本

      Q: MVCC機制下, 什么是快照讀,什么是當前讀?

      A:

      快照讀:對于select讀操作,統一默認不加鎖,使用歷史版本數據。

      當前讀:對于insert、update、delete操作,仍然需要加X鎖,因為涉及了數據變更,必須使用最新數據進行修改

      Q: 那么回到剛才的臟讀問題, MVCC究竟是怎么在讀不加鎖的情況下, 解決臟讀的?

      A:

      首先,每次select都不用任何鎖, 每次都是快照讀,不會阻塞,因此會變成下面這樣:

      總結這個圖,就是

      每次讀時,會生成一個readView,用來記錄當前還沒提交的事務版本號。

      根據自己事務的版本號version,去尋找小于自己當前版本且不在readView集合中的記錄。

      這樣的話就保證了讀的數據必須是已經完成提交的,是不是很簡單?

      Q: 如果事務B中不做余額判斷,支持直接賒賬+扣費, 那是不是會導致先扣費,然后回滾成0這樣的情況?

      A:

      不會。

      上面提過, MVCC中更新操作都是“當前讀”,仍然需要加X鎖, 且因為涉及了數據變更,必須使用最新數據版本進行修改

      換言之, update等操作, 還是會加鎖,且用最新版本更新,避免了臟更新的問題,如下:

      Q: 上面這個過程有什么隱患

      A:

      如果1個事務中連續讀2次余額,可能有“不可重復讀”的風險,即前后讀的數據發生了不一致

      如下所示

      因此RC隔離級別無法解決 “不可重復讀的問題”

      Q: RR(可重復讀,Repeat Read)的隔離級別又是怎么解決上面這個問題的?

      A:

      本質上就是readView生成時的區別

      上面RC不可重復讀的圖中可以看到,每次讀時,都取了最新的readView。 這可能導致事務A提交后, 事務B觀察到的readView集合發生了變化。

      因此RR機制改變了readView的生成方式, 每次讀時只使用事務B最開始拿到的那個readView,這樣永遠就只取老的數據了。

      Q: 那讀問題中的幻讀又是什么?

      A:

      剛才的”不可重復讀“,是一個事務中查詢2次結果,發現值對不上。

      而”幻讀“,是指一個事務中查詢2批結果,發現這2批數量對不上,就好象發生了幻覺。

      就像下圖所示展示:

      Q: RR隔離級別中的MVCC機制可以解決上面的問題嗎?

      A:

      可以解決。

      通過查詢的快照讀,能夠保證只查詢到同一批數據。

      Q: 那如果像下面這樣, 事務A連續做兩次更新呢,單純靠MVCC能避免更新操作的幻讀么?

      A:

      如果只依靠MVCC,那就無法避免了, 因為update操作是”當前讀“,每次取最新版本做更新, 這會導致update中的讀操作出現幻讀,前后更新的記錄數量不一樣了。

      Q: 那數據庫怎么處理這種2次updete中間做insert的幻讀情況呢?

      A:

      之前有了解到, update過程仍然會加鎖,

      RR級別會啟用一個叫”間隙鎖“(Gap鎖)的玩意,專門來防這樣情況。

      即調用 update xxx where name ='李四’時, 不僅僅在李四的行上加鎖, 更會在中間所有行的間隙、左右邊界的兩邊,加上一個gap間隙鎖,就像下面這個圖一樣:

      可以看到,訂單D的插入過程被update過程的間隙鎖攔住了,于是無法插入,置到事務結束才會釋放。

      因此事務中兩次update之間的幻讀是可以避免的,也能。

      Q: 那行鎖、間隙鎖、next-key鎖是什么區別?

      A:

      行鎖就是單個行(單個索引節點)加鎖

      間隙鎖就是在行(索引節點之間)加鎖

      next-key就是“行鎖+間隙鎖”,一起使用。

      Q: 如果name這個字段不是索引,而是普通字段,那間隙鎖會怎么加?

      A:

      那就會給整個表的所有間隙都加上鎖!

      因為數據庫無法確認到底是哪個范圍,所以干脆全加上。

      這就會導致整表鎖住,性能很差。

      Q: 那是不是只要name是索引,就不會給整個表全加間隙鎖了?

      A:

      不對, 如果where條件寫的有問題,不符合最左匹配原則,那也會導致索引失效, 以至于給整個表加鎖。

      Q: 剛才看到說RR可以解決2次select之間的幻讀, 也能解決2次update之間的幻讀, 那為什么很多資料里,仍然說RR不能解決幻讀?

      A:

      這個問題我也是翻了好多資料, 終于找到了一個合理的解釋。

      看下面這個場景:

      發現什么區別沒, 事務B的insert操作,發生在了事務A的update之前。因此事務B的insert操作沒有被間隙鎖阻塞。

      而update用的是當前讀, 于是更新的數量和 最初select的數量匹配不上了。

      Mysql官方給出的幻讀解釋是:只要在一個事務中,第二次select多出了row就算幻讀,所以這個場景下,算出現幻讀了。

      這也就是下面這個圖的來源:

      Q: 那串行化serializable隔離級別,為什么就能避免幻讀了?

      A:

      Se級別時,會從MVCC并發控制退化為基于鎖的并發控制(LCBB)。

      不區別快照讀和當前讀

      所有的讀操作都是當前讀,讀加讀鎖(S鎖),寫加寫鎖(X鎖)。在該隔離級別下,讀寫沖突,因此并發性能急劇下降,在MySQL/InnoDB中不建議使用。

      這就是我們文章最開頭手動加鎖的那個過程了。

      EI企業智能 MySQL 可信智能計算服務 TICS 數據庫 智能數據

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:去除Excel圖表分類軸上的空白日期
      下一篇:《Spark Streaming實時流式大數據處理實戰》 ——2.2 Spark運行模式
      相關文章
      亚洲精品无码av中文字幕| 亚洲av永久无码| 久久精品亚洲乱码伦伦中文| 亚洲国产综合人成综合网站| 激情无码亚洲一区二区三区| 亚洲色偷偷色噜噜狠狠99| 亚洲av无码片在线观看| 亚洲国产成人va在线观看网址| 亚洲视频免费在线看| 亚洲精品熟女国产| 亚洲欧洲综合在线| 亚洲成人高清在线观看| 亚洲同性男gay网站在线观看| 亚洲成a人片7777| 亚洲国产日韩在线成人蜜芽 | 久久久久亚洲av无码专区导航| 亚洲V无码一区二区三区四区观看 亚洲αv久久久噜噜噜噜噜 | 精品国产成人亚洲午夜福利| 久久精品国产亚洲AV蜜臀色欲| 亚洲va在线va天堂成人| 一本天堂ⅴ无码亚洲道久久| 亚洲色一区二区三区四区| 亚洲熟妇无码AV| 久久久久久亚洲精品无码| 日韩精品亚洲专区在线观看| 亚洲精品A在线观看| 久久精品亚洲男人的天堂| 精品国产亚洲一区二区三区| 国产亚洲精品国产| 亚洲色四在线视频观看| 亚洲色图校园春色| 亚洲乱码在线卡一卡二卡新区| 亚洲日韩精品无码专区加勒比| 亚洲精品美女久久久久久久| 成人亚洲网站www在线观看| 久久精品亚洲男人的天堂| 亚洲av中文无码乱人伦在线播放| 无码乱人伦一区二区亚洲| 亚洲春黄在线观看| 亚洲日本中文字幕天天更新| 亚洲成A人片77777国产|