在上一篇文章中,我跟你介紹了 MySQL 的全局鎖和表級鎖,今天咱們就來說講 MySQL的行鎖。mysql
MySQL 的行鎖是在引擎層由各個引擎本身實現的。但並非全部的引擎都支持行鎖,好比 MyISAM 引擎就不支持行鎖。不支持行鎖意味着併發控制只能使用表鎖,對於這種引
擎的表,同一張表上任什麼時候刻只能有一個更新在執行,這就會影響到業務併發度。InnoDB是支持行鎖的,這也是 MyISAM 被 InnoDB 替代的重要緣由之一。sql
咱們今天就主要來聊聊 InnoDB 的行鎖,以及如何經過減小鎖衝突來提高業務併發度。數據庫
顧名思義,行鎖就是針對數據表中行記錄的鎖。這很好理解,好比事務 A 更新了一行,而這時候事務 B 也要更新同一行,則必須等事務 A 的操做完成後才能進行更新。bash
固然,數據庫中還有一些沒那麼一目瞭然的概念和設計,這些概念若是理解和使用不當,容易致使程序出現非預期行爲,好比兩階段鎖。服務器
我先給你舉個例子。在下面的操做序列中,事務 B 的 update 語句執行時會是什麼現象呢?假設字段 id 是表 t 的主鍵。併發
這個問題的結論取決於事務 A 在執行完兩條 update 語句後,持有哪些鎖,以及在何時釋放。你能夠驗證一下:實際上事務 B 的 update 語句會被阻塞,直到事務 A 執行
commit 以後,事務 B 才能繼續執行。性能
知道了這個答案,你必定知道了事務 A 持有的兩個記錄的行鎖,都是在 commit 的時候才釋放的。優化
也就是說,在 InnoDB 事務中,行鎖是在須要的時候才加上的,但並非不須要了就馬上釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。spa
知道了這個設定,對咱們使用事務有什麼幫助呢?那就是,若是你的事務中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放。我給你舉個例子。線程
假設你負責實現一個電影票在線交易業務,顧客 A 要在影院 B 購買電影票。咱們簡化一點,這個業務須要涉及到如下操做:
1. 從顧客 A 帳戶餘額中扣除電影票價; 2. 給影院 B 的帳戶餘額增長這張電影票價; 3. 記錄一條交易日誌。
也就是說,要完成這個交易,咱們須要 update 兩條記錄,並 insert 一條記錄。固然,爲了保證交易的原子性,咱們要把這三個操做放在一個事務中。那麼,你會怎樣安排這三個
語句在事務中的順序呢?
一、咱們要把這三個操做放在一個事務中你會怎樣安排這三個語句在事務中的順序呢?
試想若是同時有另一個顧客 C 要在影院 B 買票,那麼這兩個事務衝突的部分就是語句 2了。由於它們要更新同一個影院帳戶的餘額,須要修改同一行數據。
根據兩階段鎖協議,不論你怎樣安排語句順序,全部的操做須要的行鎖都是在事務提交的時候才釋放的。因此,若是你把語句 2 安排在最後,好比按照 三、一、2 這樣的順序,
那麼影院帳戶餘額這一行的鎖時間就最少。這就最大程度地減小了事務之間的鎖等待,提高了併發度。
二、最大程度地減小了事務之間的鎖等待,提高了
好了,如今因爲你的正確設計,影院餘額這一行的行鎖在一個事務中不會停留很長時間。可是,這並無徹底解決你的困擾。
若是這個影院作活動,能夠低價預售一年內全部的電影票,並且這個活動只作一天。因而在活動時間開始的時候,你的 MySQL 就掛了。你登上服務器一看,CPU 消耗接近
100%,但整個數據庫每秒就執行不到 100 個事務。這是什麼緣由呢?這裏,我就要說到死鎖和死鎖檢測了。
當併發系統中不一樣線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會致使這幾個線程都進入無限等待的狀態,稱爲死鎖。這裏我用數據庫中的行鎖舉個例子。
這時候,事務 A 在等待事務 B 釋放 id=2 的行鎖,而事務 B 在等待事務 A 釋放 id=1 的行鎖。 事務 A 和事務 B 在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖
之後,有兩種策略:
一種策略是,直接進入等待,直到超時。這個超時時間能夠經過參數innodb_lock_wait_timeout 來設置。
另外一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其餘事務得以繼續執行。將參數 innodb_deadlock_detect 設置爲 on,表示開啓這個邏輯。
一、InnoDB 中,innodb_lock_wait_timeout 的默認值是?
在 InnoDB 中,innodb_lock_wait_timeout 的默認值是 50s
二、innodb_lock_wait_timeout 設置大小的影響
在 InnoDB 中,innodb_lock_wait_timeout 的默認值是 50s,意味着若是採用第一個策略,當出現死鎖之後,第一個被鎖住的線程要過 50s 纔會超時退出,而後其餘線程纔有可
能繼續執行。對於在線服務來講,這個等待時間每每是沒法接受的。
可是,咱們又不可能直接把這個時間設置成一個很小的值,好比 1s。這樣當出現死鎖的時候,確實很快就能夠解開,但若是不是死鎖,而是簡單的鎖等待呢?因此,超時時間設置
過短的話,會出現不少誤傷。
因此,正常狀況下咱們仍是要採用第二種策略,即:主動死鎖檢測,並且innodb_deadlock_detect 的默認值自己就是 on。主動死鎖檢測在發生死鎖的時候,是
可以快速發現並進行處理的,可是它也是有額外負擔的。
你能夠想象一下這個過程:每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,也就是死鎖。
一、第二種策略存在的問題
每一個新來的被堵住的線程,都要判斷會不會因爲本身的加入致使了死鎖,這是一個時間復
雜度是 O(n) 的操做。假設有 1000 個併發線程要同時更新同一行,那麼死鎖檢測操做就
是 100 萬這個量級的。雖然最終檢測的結果是沒有死鎖,可是這期間要消耗大量的 CPU
資源。所以,你就會看到 CPU 利用率很高,可是每秒卻執行不了幾個事務。
根據上面的分析,咱們來討論一下,怎麼解決由這種熱點行更新致使的性能問題呢?問題
的癥結在於,死鎖檢測要耗費大量的 CPU 資源
二、一種頭痛醫頭的方法,就是若是你能確保這個業務必定不會出現死鎖,能夠臨時把死鎖檢測關掉
一種頭痛醫頭的方法,就是若是你能確保這個業務必定不會出現死鎖,能夠臨時把死鎖檢測關掉。可是這種操做自己帶有必定的風險,由於業務設計的時候通常不會把死鎖當作一
個嚴重錯誤,畢竟出現死鎖了,就回滾,而後經過業務重試通常就沒問題了,這是業務無損的。而關掉死鎖檢測意味着可能會出現大量的超時,這是業務有損的。
三、另外一個思路是控制併發度
另外一個思路是控制併發度。根據上面的分析,你會發現若是併發可以控制住,好比同一行同時最多隻有 10 個線程在更新,那麼死鎖檢測的成本很低,就不會出現這個問題。一個
直接的想法就是,在客戶端作併發控制。可是,你會很快發現這個方法不太可行,由於客戶端不少。我見過一個應用,有 600 個客戶端,這樣即便每一個客戶端控制到只有 5 個併發
線程,彙總到數據庫服務端之後,峯值併發數也可能要達到 3000。
所以,這個併發控制要作在
一、數據庫服務端。
二、若是你有中間件,能夠考慮在中間件實現;
三、若是你的團隊有能修改 MySQL 源碼的人,也能夠作在 MySQL 裏面。
基本思路就是,對於相同行的更新,在進入引擎以前排隊。這樣在 InnoDB 內部就不會有大量的死鎖檢測工做了
四、若是團隊裏暫時沒有數據庫方面的專家,不能實現這樣的方案,能不能從設計上優化這個問題呢?
你能夠考慮經過將一行改爲邏輯上的多行來減小鎖衝突。仍是以影院帳戶爲例,能夠考慮放在多條記錄上,好比 10 個記錄,影院的帳戶總額等於這 10 個記錄的值的總和。這樣每
次要給影院帳戶加金額的時候,隨機選其中一條記錄來加。這樣每次衝突機率變成原來的1/10,能夠減小鎖等待個數,也就減小了死鎖檢測的 CPU 消耗。
這個方案看上去是無損的,但其實這類方案須要根據業務邏輯作詳細設計。若是帳戶餘額可能會減小,好比退票邏輯,那麼這時候就須要考慮當一部分行記錄變成 0 的時候,代碼
要有特殊處理。
今天,我和你介紹了 MySQL 的行鎖,涉及了兩階段鎖協議、死鎖和死鎖檢測這兩大部份內容。
其中,我以兩階段協議爲起點,和你一塊兒討論了在開發的時候如何安排正確的事務語句。這裏的原則 / 我給你的建議是:若是你的事務中須要鎖多個行,要把最可能形成鎖衝突、
最可能影響併發度的鎖的申請時機儘可能日後放。
可是,調整語句順序並不能徹底避免死鎖。因此咱們引入了死鎖和死鎖檢測的概念,以及提供了三個方案,來減小死鎖對數據庫的影響。減小死鎖的主要方向,就是控制訪問相同
資源的併發事務量。
最後,我給你留下一個問題吧。若是你要刪除一個表裏面的前 10000 行數據,有如下三種方法能夠作到:
你會選擇哪種方法呢?爲何呢?
你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期我給你留的問題是:當備庫用–single-transaction 作邏輯備份的時候,若是從主庫的binlog 傳來一個 DDL 語句會怎麼樣?
假設這個 DDL 是針對表 t1 的, 這裏我把備份過程當中幾個關鍵的語句列出來:
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT; /* other tables */ Q3:SAVEPOINT sp; /* 時刻 1 */ Q4:show create table `t1`; /* 時刻 2 */ Q5:SELECT * FROM `t1`; /* 時刻 3 */ Q6:ROLLBACK TO SAVEPOINT sp; /* 時刻 4 */ /* other tables */
在備份開始的時候,爲了確保 RR(可重複讀)隔離級別,再設置一次 RR 隔離級別 (Q1);啓動事務,這裏用 WITH CONSISTENT SNAPSHOT 確保這個語句執行完就能夠獲得一個
一致性視圖(Q2);設置一個保存點,這個很重要(Q3);
show create 是爲了拿到表結構 (Q4),而後正式導數據 (Q5),回滾到 SAVEPOINTsp,在這裏的做用是釋放 t1 的 MDL 鎖 (Q6)。固然這部分屬於「超綱」,上文正文裏面都沒提到。
DDL 從主庫傳過來的時間按照效果不一樣,我打了四個時刻。題目設定爲小表,咱們假定到達後,若是開始執行,則很快可以執行完成。
1. 若是在 Q4 語句執行以前到達,現象:沒有影響,備份拿到的是 DDL 後的表結構。2. 若是在「時刻 2」到達,則表結構被改過,Q5 執行的時候,報 Table definition haschanged, please retry transaction,現象:mysqldump 終止;3. 若是在「時刻 2」和「時刻 3」之間到達,mysqldump 佔着 t1 的 MDL 讀鎖,binlog被阻塞,現象:主從延遲,直到 Q6 執行完成。4. 從「時刻 4」開始,mysqldump 釋放了 MDL 讀鎖,現象:沒有影響,備份拿到的是DDL 前的表結構。