炸裂:MySQL死鎖是什麼,如何解決?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


炸裂:MySQL死鎖是什麼,如何解決?

尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找
此文的公衆號版本 炸裂:MySQL死鎖是什麼,如何解決?

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線企業如 字節、得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團、螞蟻、得物的面試資格,遇到很多很重要的面試題:

MySQL死鎖什麼時候發生,如何解決?

如何解決MySQL中的死鎖問題?

最近有小夥伴在面試字節,都到了這個的面試題。

小夥伴沒回答好,支支吾吾的說了幾句,面試官不滿意,面試掛了。

所以,尼恩給大家做一下系統化、體系化的梳理,幫大家展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。

當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V175版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到文末公號【技術自由圈】獲取

1 什麼是mysql死鎖?

死鎖是指兩個或多個事務在執行過程中,因爭奪鎖資源而造成的相互等待的現象,若無外力干涉它們都將無法繼續執行。

通俗來說,就是兩個或多個事務在等待對方釋放鎖,從而造成僵持不下,使得整個系統陷入停滯狀態。

2 從操作的粒度進行的mysql鎖的分類

從操作的粒度可分爲表級鎖、行級鎖和頁級鎖。

表級鎖:

每次操作鎖住整張表鎖定粒度大,發生鎖衝突的概率最高,併發度最低

應用在MyISAM、InnoDB、BDB 等存儲引擎中。

表鎖的特點:

  • 開銷小,加鎖快
  • 不會出現死鎖
  • 鎖定粒度大,發生鎖衝突的概率最高,併發度最低

行級鎖:

每次操作鎖住一行數據鎖定粒度最小,發生鎖衝突的概率最低,併發度最高

應用在InnoDB 存儲引擎中。

行鎖的特點:

  • 開銷大,加鎖慢
  • 會出現死鎖
  • 鎖定粒度小,發生鎖衝突的概率最低,併發度最高

頁級鎖:

每次鎖定相鄰的一組記錄,鎖定粒度界於表鎖和行鎖之間,開銷和加鎖時間界於表鎖和行鎖之間,併發度一般。

頁鎖的特點:

  • 開銷和加鎖時間介於表鎖和行鎖之間
  • 會出現死鎖
  • 鎖定粒度介於表鎖和行鎖之間,併發度一般

3 從操作的類型進行的mysql鎖的分類

從操作的類型可分爲讀鎖和寫鎖。

讀鎖(S鎖)

讀鎖(S鎖):共享鎖,針對同一份數據,多個讀操作可以同時進行而不會互相影響。

S鎖:事務A對記錄添加了S鎖,可以對記錄進行讀操作,不能做修改,其他事務可以對該記錄追加S鎖,但是不能追加X鎖,要追加X鎖,需要等記錄的S鎖全部釋放。

寫鎖(X鎖)

寫鎖(X鎖):排他鎖,當前寫操作沒有完成前,它會阻斷其他寫鎖和讀鎖

X鎖:事務A對記錄添加了X鎖,可以對記錄進行讀和修改操作,其他事務不能對記錄做讀和修改操作。

意向鎖

  • IS: 意向共享鎖,表級鎖,已加S鎖的表,肯定會有IS鎖,反過來,有IS鎖的表,不一定會有S鎖

  • IX: 意向排它鎖,表級鎖,已加X鎖的表,肯定會有IX鎖,反過來,有IX鎖的表,不一定會有X鎖

4 從操作的性能進行的mysql鎖的分類

從操作的性能可分爲樂觀鎖和悲觀鎖。

  • 樂觀鎖:一般的實現方式是對記錄數據版本進行比對,在數據更新提交的時候纔會進行衝突檢測,如果發現衝突了,則提示錯誤信息。
  • 悲觀鎖:在對一條數據修改的時候,爲了避免同時被其他人修改,在修改數據之前先鎖定,再修改的控制方式。共享鎖和排他鎖是悲觀鎖的不同實現,但都屬於悲觀鎖範疇。

5 InnoDB存儲引擎三種行鎖模式

InnoDB引擎行鎖是通過對索引數據頁上的記錄加鎖實現的,主要實現算法有 3 種:Record Lock、Gap Lock 和 Next-key Lock,也就是InnoDB的三種行鎖模式。

  • RecordLock鎖(行鎖):鎖定單個行記錄的鎖。(RecordLock鎖 是記錄鎖,RC、RR隔離級別都支持)
  • GapLock鎖:間隙鎖,鎖定索引記錄間隙(不包括記錄本身),確保索引記錄的間隙不變。(GapLock是範圍鎖,RR隔離級別支持。RC隔離級別不支持)
  • Next-key Lock 鎖(臨鍵鎖):記錄鎖和間隙鎖組合,同時鎖住數據,並且鎖住數據前後範圍。(記錄鎖+範圍鎖,RR隔離級別支持。RC隔離級別不支持)

5.1 記錄鎖(Record Locks)

(1)記錄鎖, 僅僅鎖住索引記錄的一行,在單條索引記錄上加鎖。
(2)record lock鎖住的永遠是索引,而非記錄本身,即使該表上沒有任何索引,那麼innodb會在後臺創建一個隱藏的聚集主鍵索引,那麼鎖住的就是這個隱藏的聚集主鍵索引。

所以說當一條sql沒有走任何索引時,那麼將會在每一條聚合索引後面加X鎖,這個類似於表鎖,但原理上和表鎖應該是完全不同的。

5.2 間隙鎖(Gap Locks)

(1)區間鎖, 僅僅鎖住一個索引區間(開區間,不包括雙端端點)。
(2)在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或者之後加鎖,並不包括該索引記錄本身。

(3)間隙鎖可用於防止幻讀,保證索引間的不會被插入數據

比如在 100、10000中,間隙鎖的可能值有 (∞, 100),(100, 10000),(10000, ∞),

5.3 行鎖:臨鍵鎖(Next-Key Locks)

(1)record lock + gap lock, 左開右閉區間。

(2)默認情況下,innodb使用next-key locks來鎖定記錄。select … for update
(3)但當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級爲Record Lock,即僅鎖住索引本身,不是範圍。
(4)Next-Key Lock在不同的場景中會退化:

在這裏插入圖片描述

比如在 100、10000中,臨鍵鎖(Next-Key Locks)的可能有 (∞, 100],(100, 10000] , 40歲老架構師尼恩提示,這裏的關鍵是左開右閉

具體的講解,請參見《尼恩Java面試寶典》配套視頻。

6 事務隔離級別和鎖的關係

6.1 數據庫事務的隔離級別

先來回顧一下,數據庫事務的隔離級別,目前數據庫事務的隔離級別一共有 4 種,由低到高分別爲:

事務的四個隔離級別:

  • 未提交讀(READ UNCOMMITTED):所有事務都可以看到其他事務未提交的修改。一般很少使用;
  • 讀已提交(READ COMMITTED):Oracle默認隔離級別,事務之間只能看到彼此已提交的變更修改;
  • 可重複讀(REPEATABLE READ):MySQL默認隔離級別,同一事務中的多次查詢會看到相同的數據行;可以解決不可重複讀,但可能出現幻讀;
  • 可串行化(SERIALIZABLE):最高的隔離級別,事務串行的執行,前一個事務執行完,後面的事務會執行。讀取每條數據都會加鎖,會導致大量的超時和鎖爭用問題;

圖片

數據庫一般默認的隔離級別爲 讀已提交 RC ,比如 Oracle,

也有一些數據的默認隔離級別爲 可重複讀 RR,比如 Mysql。

"可重複讀"(Repeatable Read)這個級別確保了對同一字段的多次讀取結果是一致的,除非數據是被本身事務自己所修改。

RR它能夠防止髒讀、不可重複讀,但可能會遇到幻讀的情況。

參考 文章:

Mysql如何實現RR級隔離時,不會幻讀?

MySQL默認的Repeatable Read隔離級別,被改成了RC , 具體請參考 《尼恩Java 面試寶典》 MYSQL 專題:

在這裏插入圖片描述

一般而言,數據庫的讀已提交(READ COMMITTED)能夠滿足業務絕大部分場景了。

6.2 事務隔離級別和鎖的關係

  1. 事務隔離級別是SQL92定製的標準,相當於事務併發控制的整體解決方案,本質上是對鎖和MVCC使用的封裝,隱藏了底層細節。

  2. 鎖是數據庫實現併發控制的基礎,事務隔離性是採用鎖來實現,對相應操作加不同的鎖,就可以防止其他事務同時對數據進行讀寫操作。

  3. 對用戶來講,首先選擇使用隔離級別,當選用的隔離級別不能解決併發問題或需求時,纔有必要在開發中手動的設置鎖。

    MySQL 默認隔離級別:可重複讀, 一般建議改爲 RC 讀已提交

    Oracle、SQLServer默認隔離級別:讀已提交

7 死鎖產生原因和解決方案

InnoDB與MyISAM的最大不同有兩點

  • 支持事務
  • 採用行鎖

行級鎖和表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些問題 ,比如 死鎖。

7.1 查看Innodb行鎖爭用情況

通過show status like 'innodb_row_lock_%'; 命令可以查詢MySQL整體的鎖狀態,如下:

鎖性能表現

  • Innodb_row_lock_current_waits:當前正在阻塞等待鎖的事務數量。
  • Innodb_row_lock_timeMySQL啓動到現在,所有事務總共阻塞等待的總時長。
  • Innodb_row_lock_time_avg:平均每次事務阻塞等待鎖時,其平均阻塞時長。
  • Innodb_row_lock_time_maxMySQL啓動至今,最長的一次阻塞時間。
  • Innodb_row_lock_waitsMySQL啓動到現在,所有事務總共阻塞等待的總次數。

7.2 詳細介紹 死鎖的概念

什麼是死鎖DeadLock? :

是指兩個或兩個以上的進程在執行過程中, 因爭奪資源而造成的一種互相等待的現象,

若無外力作用,它們都將無法推進下去.

此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

一個的形象舉例:

假設有兩個事務 A 和 B,它們同時試圖獲取對方持有的資源,但又都在等待對方釋放資源,導致了僵持不下的局面。

舉個例子,假設有兩個人 A 和 B,他們同時想要通過一扇門進入一個房間,但這扇門只能由一人單獨打開。

在這裏插入圖片描述

現在,尷尬了 :

  • A 想要進入房間1,但門被 B 擋住了,所以 A 無法進入,於是 A 抓住了 B 的右手,不讓 B 打開房間2的門。

  • B 想要進入房間2,但門被 A 擋住了,所以 B 無法進入,於是 B 抓住了 A 的左手,不讓 A 打開房間1的門。

現在的情況是:

  • A 等待着 B 放開 B 的左手,以便自己能打開門進入房間,

    B 等待着 A 放開A的右手,以便自己能夠進入房間。

這就形成了死鎖,因爲兩個人都在 對方放開資源,而對方又不願意放開自己所持有的資源,導致了相互等待,最終無法繼續執行下去。

7.3 表級鎖死鎖

a)產生原因:
  • 用戶A先訪問表1(鎖住了表1),然後再訪問表2;

  • 用戶B先訪問表2(鎖住了表2),然後再企圖訪問表1;

    用戶A和用戶B加鎖的順序如下:

  • 用戶A--》表1(表鎖)--》表2(表鎖)

  • 用戶B--》表2(表鎖)--》表1(表鎖)

    這時

  • 用戶A由於用戶B已經鎖住表2,它必須等待用戶B釋放表2才能繼續

  • 同樣用戶B要等用戶A釋放表1才能繼續

這就死鎖就產生了。 如下圖所示:

在這裏插入圖片描述

b)解決方案:

這種死鎖比較常見,是由於程序的BUG產生的,除了調整的程序的邏輯沒有其它的辦法。

仔細分析程序的邏輯,對於數據庫的多表操作時,儘量按照相同的順序進行處理,

儘量避免同時鎖定兩個資源,如操作A和B兩張表時,總是按先A後B的順序處理, 必須同時鎖定兩個資源時,要保證在任何時刻都應該按照相同的順序來鎖定資源。

7.4 行級鎖死鎖

a) 產生原因1:

如果在事務中執行了一條沒有索引條件的查詢,引發全表掃描,行鎖 膨脹 爲表鎖( 或者等價於
表級鎖),

多個這樣的 鎖表事務 執行後,就很容易產生死鎖和阻塞,最終應用系統會越來越慢,發生阻塞或
死鎖。

解決方案1:

SQL語句中不要使用太複雜的關聯多表的查詢;

使用explain“執行計劃"對SQL語句進行分析,對於有全表掃描和全表鎖定的SQL語句,建立相應的索引進行優化。

b) 產生原因2:

兩個事務分別想拿到對方持有的鎖,互相等待,於是產生死鎖。

c) 產生原因3:每個事務只有一個SQL,但是有些情況還是會發生死鎖.

假設有下面的一個表 t1

create table t1(
    id int(32) not null,
    name varchar(50) not null,
    reg_time int(32) not null,
    city varchar(50) ,
    primary key (`name`),
    index index_name(`name`),
    index index_reg_time(`reg_time`)
);

事務1, 假設有下面的一個 session1 會話

update t1 set city = "香港"  where name="aaa"

首先, session1 從name 非聚族索引索引出發 , 讀到的 [aaa, 1], [aaa, 4] , 會加name索引上的記錄[aaa, 1], [aaa, 4] 兩個 記錄的X鎖,

然後,session1 會加聚簇索引上的記錄X鎖, 聚簇索引上 加鎖順序爲先[1] 記錄, 後[4] 記錄

在這裏插入圖片描述

事務2 假設有下面的一個 session2 會話

Select * from t1  where reg_time>=1000 for update

session2 從reg_time 非聚族索引索引出發 , 讀到的 [1000, 4], [1100, 3] , [1200,1], [1300, 2] ,

首先,session2 會加reg_time索引上的記錄 [1000, 4], [1100, 3] , [1200,1], [1300, 2] 四個 記錄的X鎖,

然後,session2 而且會加聚簇索引上的記錄X鎖, 聚簇索引上 加鎖順序爲 [4] 、[3] 、[2] 、[1] ,其中 先[4] 記錄, 後[1] 記錄

在這裏插入圖片描述

session2 加聚簇索引上的記錄X鎖時,發現跟session1的加鎖順序正好相反,

兩個Session恰好都持有了第一把鎖,請求加第二 把鎖,死鎖就發生了。

解決方案: 如上面的原因2和原因3, 對索引加鎖順序的不一致很可能會導致死鎖,所以如果可以,

儘量以相同的順序來訪問索引記錄和表

在程序以批量方式處理數據的時候,如果事先對數據排序,保證每個線程按固定的順序來處理記錄,也可以大大降低出現死鎖的可能;

通過統一的鎖定順序,可以有效地避免不同事務之間的鎖定順序不一致導致的死鎖問題。

具體的講解,請參見《尼恩Java面試寶典》配套視頻。

8: InnoDB預防死鎖策略

InnoDB引擎內部(或者說是所有的數據庫內部),有多種鎖類型:事務鎖(行鎖表鎖),Mutex(保護內部的共享變量操作)、RWLock(又稱之爲Latch,保護內部的頁面讀取與修改)。

InnoDB每個頁面爲16K,讀取一個頁面時,需要對頁面加S鎖(共享鎖),更新一個頁面時,需要對頁面加上X鎖(排他鎖)。

任何情況下,操作一個頁面,都會對頁面加鎖,頁面鎖加上之後,頁面內存儲的索引記錄纔不會被併發修改。

因此,爲了修改一條記錄,InnoDB內部如何處理:

  • 根據給定的查詢條件,找到對應的記錄所在頁面;

  • 對頁面加上X鎖(RWLock),然後在頁面內尋找滿足條件的記錄;

  • 在持有頁面鎖的情況下,對滿足條件的記錄加事務鎖(行鎖:根據記錄是否滿足查詢條件,記錄是否已經被刪除,分別對應於上面提到的3種加鎖策略之一);

相對於事務鎖,頁面鎖是一個短期持有的鎖,而事務鎖(行鎖、表鎖)是長期持有的鎖

InnoDB預防死鎖策略

因此,爲了防止頁面鎖與事務鎖之間產生死鎖,InnoDB做了死鎖預防的策略:

  • 持有事務鎖(行鎖、表鎖),可以等待獲取頁面鎖
  • 但反之,持有頁面鎖,不能等待持有事務鎖。

根據死鎖預防策略,在持有頁面鎖,加行鎖的時候,如果行鎖需要等待,則釋放頁面鎖,然後等待行鎖。

此時,行鎖獲取沒有任何鎖保護,因此加上行鎖之後,記錄可能已經被併發修改。因此,此時要重新加回頁面鎖,重新判斷記錄的狀態,重新在頁面鎖的保護下,對記錄加鎖。

如果此時記錄未被併發修改,那麼第二次加鎖能夠很快完成,因爲已經持有了相同模式的鎖。但是,如果記錄已經被併發修改,那麼,就有可能導致死鎖問題。

在數據庫系統中,死鎖的檢測和解決通常是通過鎖管理器(Lock Manager)來實現的。

  • 當一個事務請求某個數據頁的鎖時,鎖管理器會檢查當前鎖的狀態以及其他事務是否持有或等待相同的鎖。
  • 如果存在潛在的死鎖風險,系統會通過死鎖檢測算法來檢測並解決死鎖。其中,常用的死鎖檢測算法包括等待圖(Wait-for graph)算法和超時算法。

在數據庫系統的實現中,鎖管理器會維護一個鎖表(Lock Table),用於記錄當前數據頁的鎖狀態以及事務之間的關係。

當一個事務請求鎖時,鎖管理器會根據鎖定順序來判斷是否存在死鎖風險,並根據具體情況採取相應的措施,比如阻塞等待或者回滾事務。

在數據庫系統的源代碼級別,鎖管理器通常是數據庫引擎的一部分,具體實現方式會根據不同的數據庫系統而有所不同。例如,MySQL、PostgreSQL、Oracle等數據庫系統都有自己的鎖管理器實現,通常會涉及到併發控制、事務管理等核心模塊的代碼。

總之,在MySQL 5.5.5及以上版本中,MySQL的默認存儲引擎是InnoDB。該存儲引擎使用的是行級鎖,在某種情況下會產生死鎖問題,所以InnoDB存儲引擎採用了一種叫作等待圖(wait-for graph)的方法來自動檢測死鎖,如果發現死鎖,就會自動回滾一個事務

9 : 死鎖案例分析

爲了幫助大家瞭解mysql的死鎖,下面有三個和死鎖有關的案例:

9.1 案例一:拆借款

需求:投資人將投資的錢,拆成幾份隨機分配給借款人。

第一個版本的業務邏輯:投資人投資後,將金額隨機分爲幾份,然後隨機從借款人表裏面選幾個,然後通過一條條select for update 去更新借款人表裏面的餘額等。

例如兩個用戶同時投資,

  • A用戶金額隨機分爲2份,分給借款人小明,小亮,

  • B用戶金額隨機分爲2份,分給借款人小亮,小明

由於加鎖的順序不一樣,死鎖當然很快就出現了。

如果改進呢? 對於這個問題的改進很簡單,直接把所有分配到的借款人直接一次鎖住就行了。

Select * from xxx where id in (xx,xx,xx) for update

在in裏面的列表值mysql是會自動從小到大排序,加鎖也是一條條從小到大加的鎖。

例如(以下會話id爲主鍵):

Session1:


mysql> select * from t3 where id in (8,9) for update;
+----+--------+------+---------------------+
| id | course | name | ctime               |
+----+--------+------+---------------------+
|  8 | WA     | f    | 2016-03-02 11:36:30 |
|  9 | JX     | f    | 2016-03-01 11:36:30 |
+----+--------+------+---------------------+
rows in set (0.04 sec)

Session2:


select * from t3 where id in (10,8,6) for update;
鎖等待中……

其實這個時候id=10這條記錄沒有被鎖住的,但id=6的記錄已經被鎖住了,鎖的等待在id=8(被Session1 鎖住)的這裏

Session3:

mysql> select * from t3 where id=6 for update;
鎖等待中


可以看到id=6被Session2 鎖住,鎖等待中

Session4:

mysql> select * from t3 where id=10 for update;
+----+--------+------+---------------------+
| id | course | name | ctime               |
+----+--------+------+---------------------+
| 10 | JB     | g    | 2016-03-10 11:45:05 |
+----+--------+------+---------------------+
row in set (0.00 sec)

在其它session中id=6是加不了鎖的,但是id=10是可以加上鎖的, 說明id=10這條記錄沒有被Session2鎖住 。

9.2 案例二:有則插入無則更新

在開發中,經常會做這類的判斷需求:

  • 根據字段值查詢(有索引),如果不存在,則插入;
  • 否則更新。
以id爲主鍵爲例,目前還沒有id=22的行

Session1:
select * from t3 where id=22 for update;
Empty set (0.00 sec)

session2:
select * from t3 where id=23  for update;
Empty set (0.00 sec)

Session1:
insert into t3 values(22,'ac','a',now());
鎖等待中……

Session2:
insert into t3 values(23,'bc','b',now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  • 當對存在的行 進行鎖的時候(主鍵),mysql就只有行鎖。

  • 當對未存在的行進行鎖的時候(即使條件爲主鍵),mysql是會鎖住一段範圍(有gap鎖),也就是間隙鎖(Gap Locks)

間隙鎖(Gap Locks) 鎖住的範圍爲:(無窮小或小於表中鎖住id的最大值,無窮大或大於表中鎖住id的最小值)

  • 如果表中目前有已有的id爲(11 , 12),那麼就鎖住(12,無窮大)

  • 如果表中目前已有的id爲(11 , 30),那麼就鎖住(11,30)

對於這種死鎖的解決辦法是:

insert into t3(xx,xx) on duplicate key update xx='XX';

用mysql特有的語法來解決此問題。因爲insert語句對於主鍵來說,插入的行不管有沒有存在,都會只有行鎖

9.3 案例三 死鎖日誌分析

數據準備

--創建表 t2
create table t2(
  id int primary key,
  name varchar(50),
  age int
);
--插入數據
insert into t2 values(1,'lisi',11),(2,'zhangsan',22),(3,'wangwu',33);

數據庫隔離級別查看

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+

查看加鎖信息

-- information_schema.innodb_trx: 當前出現的鎖
select * from information_schema.innodb_locks;
-- information_schema.innodb_trx: 當前運行的所有事務
select * from information_schema.innodb_trx;
-- information_schema.innodb_lock_waits: 鎖等待的對應關係
select * from information_schema.innodb_lock_waits;

查看InnoDB狀態 ( 包含最近的死鎖日誌信息 )

show engine innodb status;

案例分析

這裏我們進行細緻的分析,兩個事物每執行一條SQL,

可以查看下innodb鎖狀態及鎖等待信息以及當前innodb事務列表信息,

最後可以通過 show engine innodb status 查看最近的死鎖日誌信息.

場景1:

  1. 事務1, 執行begin開始事務執行一條SQL,查詢 id=1 的數據
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t2 where id = 1 for update;
+----+------+------+
| id | name | age |
+----+------+------+
|  1 | lisi |  11 |
+----+------+------+
1 row in set (0.00 sec)

分析加鎖過程:

  1. 事務1進行首先申請IX鎖 (意向排它鎖,因爲是for update)
  2. 然後申請X鎖進行查詢是否存在 id = 1 的記錄
  3. 存在該記錄,因爲id字段是唯一索引,所以添加的是 Record Lock
  1. 查看 information_schema.innodb_trx表,發現存在事務1 的信息
select trx_id '事務id',trx_state '事務狀態',
       trx_started '事務開始時間',trx_weight '事務權重',
       trx_mysql_thread_id '事務線程ID',
       trx_tables_locked '事務擁有多少個鎖',
       trx_lock_memory_bytes '事務鎖住的內存大小',
       trx_rows_locked '事務鎖住的行數',
       trx_rows_modified '事務更改的行數'
  from information_schema.innodb_trx;
  1. 執行事務2的 delete語句, 刪除成功,因爲id=3的數據並沒有被加鎖
mysql> delete from t2 where id = 3; -- 刪除成功
  1. 事務1對 id=3 的記錄進行修改操作,發生阻塞. 因爲id=3的數據的X鎖已經被事務2拿到,其他事務的操
    作只能被阻塞.
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t2 where id = 1 for update;
+----+------+------+
| id | name | age |
+----+------+------+
|  1 | lisi |  11 |
+----+------+------+
1 row in set (0.00 sec)
mysql> update t2 set name = 'aaa' where id = 3;
-- 阻塞
  1. 查看當前鎖信息
-- 查看當前鎖信息
select
lock_id '鎖ID',
 lock_trx_id '擁有鎖的事務ID',
 lock_mode '鎖模式',
 lock_type '鎖類型' ,
 lock_table '被鎖的索引',
 lock_space '被鎖的表空間號',
 lock_page '被鎖的頁號',
 lock_rec '被鎖的記錄號',
 lock_data '被鎖的數據'
from information_schema.innodb_locks;

lock_rec=4 表示是對唯一索引進行的加鎖. lock_mode= X 表示這裏加的是X鎖.

-- 查看鎖等待的對應關係
select requesting_trx_id '請求鎖的事務ID',
       requested_lock_id '請求鎖的鎖ID',
       blocking_trx_id '當前擁有鎖的事務ID',
       blocking_lock_id '當前擁有鎖的鎖ID'
   from information_schema.innodb_lock_waits;
  1. 事務2 執行刪除操作,刪除 id = 1的數據成功.
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from t2 where id = 3;
Query OK, 1 row affected (0.00 sec)

mysql> delete from t2 where id = 1;
Query OK, 1 row affected (0.00 sec)
  1. 但是事務1已經檢測到了死鎖的發生
mysql> update t2 set name = 'aaa' where id = 3;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

--事務1 commit,更新操作失敗
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_dead;
-- ERROR 1146 (42S02): Table 'test_lock.test_dead' doesn't exist

mysql> select * from t2;
+----+----------+------+
| id | name     | age  |
+----+----------+------+
|  1 | lisi     |   11 |
|  2 | zhangsan |   22 |
+----+----------+------+
2 rows in set (0.00 sec)

-- 事務2 commit ,刪除操作成功
mysql> commit;

mysql> select * from t2;
+----+----------+------+
| id | name     | age  |
+----+----------+------+
|  1 | lisi     |   11 |
|  2 | zhangsan |   22 |
+----+----------+------+
2 rows in set (0.00 sec)
  1. 查看死鎖日誌
  • ACTIVE 309秒 sec : 表示事務活動時間

  • starting index read : 表示讀取索引

  • tables in use 1: 表示有一張表被使用了

  • LOCK WAIT 3 lock struct(s): 表示該事務的鎖鏈表的長度爲3,每個鏈表節點代表該事務持有的一個鎖結構,包括表鎖,記錄鎖以及 autoinc 鎖等.

  • heap size 1136 : 爲事務分配的鎖堆內存大小

  • 3 row lock(s): 表示當前事務持有的行鎖個數/gap鎖的個數

    LATEST DETECTED DEADLOCK
    ------------------------
    2022-04-04 06:22:01 0x7fa66b39d700
    *** (1) TRANSACTION: 事務1
    TRANSACTION 16472, ACTIVE 309 sec starting index read
    -- 事務編號 16472,活躍秒數 309,starting index read 表示事務狀態爲根據索引讀取數據.
    
    mysql tables in use 1, locked 1  
    -- 表示有一張表被使用了 ,locked 1 表示表上有一個表鎖,對於DML語句爲LOCK_IX
    
    LOCK WAIT 3 lock struct(s), heap size 1136, 3 row lock(s)
    MySQL thread id 20, OS thread handle 140352739985152, query id 837 localhost root updating
    
    update t2 set name = 'aaa' where id = 3 
    --當前正在等待鎖的SQL語句.
    
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 248 page no 3 n bits 72 index PRIMARY of table `test_lock`.`t2` trx id 16472 lock_mode X locks rec but not gap waiting
    Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 32
     0: len 4; hex 80000003; asc     ;;
     1: len 6; hex 000000004059; asc     @Y;;
     2: len 7; hex 4100000193256b; asc A    %k;;
     3: len 6; hex 77616e677775; asc wangwu;;
     4: len 4; hex 80000021; asc    !;;
    
    *** (2) TRANSACTION:
    TRANSACTION 16473, ACTIVE 300 sec starting index read
    mysql tables in use 1, locked 1
    3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
    MySQL thread id 19, OS thread handle 140352740251392, query id 838 localhost root updating
    delete from t2 where id = 1
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 248 page no 3 n bits 72 index PRIMARY of table `test_lock`.`t2` trx id 16473 lock_mode X locks rec but not gap
    Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 32
     0: len 4; hex 80000003; asc     ;;
     1: len 6; hex 000000004059; asc     @Y;;
     2: len 7; hex 4100000193256b; asc A    %k;;
     3: len 6; hex 77616e677775; asc wangwu;;
     4: len 4; hex 80000021; asc    !;;
    
    *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 248 page no 3 n bits 72 index PRIMARY of table `test_lock`.`t2` trx id 16473 lock_mode X locks rec but not gap waiting
    Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 80000001; asc     ;;
     1: len 6; hex 00000000403d; asc     @=;;
     2: len 7; hex b0000001240110; asc     $  ;;
     3: len 4; hex 6c697369; asc lisi;;
     4: len 4; hex 8000000b; asc     ;
    

    具體的演示過程,請參見《尼恩Java面試寶典》配套視頻。

10 : 死鎖產生的前提和建議

前提

  • 互斥:不能共享

  • 持有並等待:當前事務保持至少一個資源,同時在等待獲取其他資源。

  • **不可剝奪 **:已獲得的資源不能被強制釋放,只能由獲取該資源的事務主動釋放。

  • 循環等待:系統中若干事務之間形成了一個循環等待資源的鏈。

    死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。

    那麼對應的解決死鎖問題的關鍵就是:讓不同的session加鎖有次序

建議:

  • 一致性排序

    對索引加鎖順序的不一致很可能會導致死鎖, 所以如果可以, 儘量以相同的順序來訪問索引記錄和表.

    在程序以批量方式處理數據的時候, 如果事先對數據排序, 保證每個線程按固定的順序來處理記錄, 也可以大大降低出現死鎖的可能.

  • 間隙鎖

    往往是程序中導致死鎖的真兇, 由於默認情況下 MySQL 的隔離級別是 RR(Repeatable Read,可重複讀),所以如果能確定幻讀和不可重複讀對應用的影響不大, 可以考慮將隔離級別改成 RC, 可以避免 Gap 鎖導致的死鎖.

  • 爲表添加合理的索引, 如果不走索引將會爲表的每一行記錄加鎖, 死鎖的概率就會大大增大.

  • 避免大事務, 儘量將大事務拆成多個小事務來處理.

    因爲大事務佔用資源多, 耗時長, 與其他事務衝突的概率也會變高.

  • 避免在同一時間點運行多個對同一表進行讀寫的腳本, 特別注意加鎖且操作數據量比較大的語句.

  • 超時和重試機制設置鎖等待超時參數

    innodb_lock_wait_timeout,在併發訪問比較高的情況下,如果大量事務因無法立即獲得所需的鎖而掛起,會佔用大量計算機資源,造成嚴重性能問題,甚至拖跨數據庫。

    我們通過設置合適的鎖等待超時閾值,可以避免這種情況發生。

11: 線上發生了死鎖,應該如何具體操作?

數據庫的死鎖是指不同的事務在獲取資源時相互等待,導致無法繼續執行的情況。

MySQL中可能發生死鎖的情況包括事務同時更新多個表、事務嵌套、索引順序不一致以及不同事務同時更新相同的索引等。

雖然數據庫有死鎖的預防策略,以及自動的處理措施。 但是,在線上很多場景下, 數據的的死鎖預防策略和回滾策略 , 通常達不到預期的效果。

如果線上發生了死鎖,我們應該採取以下步驟進行處理:

11.1 監控死鎖

通過數據庫的監控工具或命令查看是否存在死鎖情況,瞭解死鎖的具體情況,包括死鎖的事務和死鎖的資源。

step1: 查看當前正在等待鎖的事務

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

運行以上SQL語句,可以查看當前正在等待鎖的事務列表。

根據返回結果,可以分析哪些事務在等待哪些鎖,以及等待鎖的具體類型。

step2:查看當前持有的鎖信息

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

運行以上SQL語句,可以查看當前數據庫中的鎖信息。

通過分析返回結果,可以瞭解哪些鎖正在被持有,以及鎖的持有者和鎖的類型。

step3:查看當前的死鎖信息

SHOW ENGINE INNODB STATUS;

運行以上SQL語句,可以顯示當前的InnoDB存儲引擎的狀態信息。

其中包括死鎖檢測結果。如果存在死鎖,可以通過分析該信息來解決死鎖問題。

具體的演示過程,請參見《尼恩Java面試寶典》配套視頻。

11.2 終止死鎖事務

一旦發現死鎖,需要找到造成死鎖的事務,並選擇其中一個事務終止。可以根據事務的執行時間、影響行數、優先級等因素進行終止決策。

可以採取以下方法來解決死鎖問題:

  • 回滾事務:

    使用以下命令回滾某個事務以解除死鎖:

    ROLLBACK;
    
    
  • 殺死進程:

    使用以下命令查找引起死鎖的進程:

    SHOW PROCESSLIST;
    
    

    找到引起死鎖的進程ID後,使用以下命令殺死該進程:

    KILL <process_id>;
    
    

具體的演示過程,請參見《尼恩Java面試寶典》配套視頻。

11.3 重試事務

終止死鎖事務後,需要重新執行被終止的事務。

重試事務 之前,需要調整事務順序

這可能需要一些邏輯處理,例如對數據進行回滾或者重新執行一些操作。

11.4 防止死鎖再次發生

通過數據庫的日誌和監控信息,分析死鎖的原因。

可以根據死鎖原因對數據庫的設計和代碼進行優化,以儘量減少死鎖的發生。

根據分析結果,針對性地進行數據庫結構調整、索引優化、事務隔離級別調整等措施,以降低死鎖的概率。

說在最後:有問題找老架構取經

MySQL死鎖是什麼,如何解決? 如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走。

在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來改簡歷、做幫扶。

遇到職業難題,找老架構取經, 可以省去太多的折騰,省去太多的彎路。

尼恩指導了大量的小夥伴上岸,前段時間,剛指導一個40歲+被裁小夥伴,拿到了一個年薪100W的offer。

狠狠卷,實現 “offer自由” 很容易的, 前段時間一個武漢的跟着尼恩捲了2年的小夥伴, 在極度嚴寒/痛苦被裁的環境下, offer拿到手軟, 實現真正的 “offer自由” 。

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章