1. 程式人生 > >MySQL資料庫的鎖詳解【轉】

MySQL資料庫的鎖詳解【轉】

當然在我們的資料庫中也有鎖用來控制資源的併發訪問,這也是資料庫和檔案系統的區別之一。

為什麼要懂資料庫鎖?

通常來說對於一般的開發人員,在使用資料庫的時候一般懂點 DQL(select),DML(insert,update,delete)就夠了。

小明是一個剛剛畢業在網際網路公司工作的 Java 開發工程師,平常的工作就是完成 PM 的需求。

當然在完成需求的同時肯定逃脫不了 Spring,Spring MVC,Mybatis 的那一套框架,所以一般來說 SQL 還是自己手寫,遇到比較複雜的 SQL 會從網上去百度一下。

對於一些比較重要操作,比如交易啊這些,小明會用 Spring 的事務來對資料庫的事務進行管理,由於資料量比較小目前還涉及不到分散式事務。

前幾個月小明過得都還風調雨順,直到有一天,小明接了一個需求,商家有個配置項,叫優惠配置項,可以配置買一送一,買一送二等等規則。

當然這些配置是批量傳輸給後端的,這樣就有個問題每個規則都得去匹配,他到底是刪除還是新增還是修改,這樣後端邏輯就比較麻煩。

聰明的小明想到了一個辦法,直接刪除這個商家的配置,然後全部新增進去。小明馬上開發完畢,成功上線。

開始上線沒什麼毛病,但是日誌經常會出現一些 mysql-insert-deadlock 異常。

由於小明經驗比較淺,對於這型別的問題第一次遇見,於是去問了他們組的老司機大紅。

大紅一看見這個問題,然後看了他的程式碼之後,輸出了幾個命令看了幾個日誌,馬上定位了問題,告訴了小明:這是因為 delete 的時候會加間隙鎖。

但是間隙鎖之間卻可以相容,但是插入新的資料的時候就會因為插入意向鎖會被間隙鎖阻塞,導致雙方資源被互佔,導致死鎖。

小明聽了之後似懂非懂,由於大紅的事情比較多,不方便一直麻煩大紅,所以決定自己下來自己想。

下班過後,小明回想大紅說的話,什麼是間隙鎖,什麼是插入意向鎖,看來作為開發者對資料庫不應該只會寫 SQL 啊,不然遇到一些疑難雜症完全沒法解決啊。想完,於是小明就踏上了學習 MySQL 鎖這條不歸之路。

什麼是 InnoDB?

MySQL 體系架構

小明沒有著急去了解鎖這方面的知識,他首先先了解了下 MySQL 體系架構:

 

為什麼開發人員必須要了解資料庫鎖?

 

可以發現 MySQL 由連線池元件、管理服務和工具元件、SQL 介面元件、查詢分析器元件、優化器元件、 緩衝元件、外掛式儲存引擎、物理檔案組成。

小明發現在 MySQL 中儲存引擎是以外掛的方式提供的,在 MySQL 中有多種儲存引擎,每個儲存引擎都有自己的特點。

隨後小明在命令列中打出了:

show engines G; 

一看原來有這麼多種引擎。又打出了下面的命令,檢視當前資料庫預設的引擎:

show variables like '%storage_engine%'; 

 

為什麼開發人員必須要了解資料庫鎖?

 

小明恍然大悟:原來自己的資料庫是使用的 InnoDB,依稀記得自己在上學的時候好像聽說過有個引擎叫 MyIsAM,小明想這兩個有啥不同呢?

馬上查找了一下資料:

 

為什麼開發人員必須要了解資料庫鎖?

 

小明大概瞭解了一下 InnoDB 和 MyIsAM 的區別,由於使用的是 InnoDB,小明就沒有過多的糾結這一塊。

事務的隔離性

小明在研究鎖之前,又回想到之前上學的時候教過的資料庫事務隔離性,其實鎖在資料庫中其功能之一也是用來實現事務隔離性。而事務的隔離性其實是用來解決髒讀,不可重複讀,幻讀幾類問題。

髒讀

一個事務讀取到另一個事務未提交的更新資料。什麼意思呢?

 

為什麼開發人員必須要了解資料庫鎖?

 

在事務 A,B 中,事務 A 在時間點 2,4 分別對 user 表中 id = 1 的資料進行了查詢。

但是事務 B 在時間點 3 進行了修改,導致了事務 A 在 4 中的查詢出的結果其實是事務 B 修改後的。這樣就破壞了資料庫中的隔離性。

不可重複讀

在同一個事務中,多次讀取同一資料返回的結果不同,不可重複讀和髒讀不同的是這裡讀取的是已經提交過後的資料。

 

為什麼開發人員必須要了解資料庫鎖?

 

在事務 B 中提交的操作在事務 A 第二次查詢之前,但是依然讀到了事務 B 的更新結果,也破壞了事務的隔離性。

幻讀

一個事務讀到另一個事務已提交的 insert 資料。

 

為什麼開發人員必須要了解資料庫鎖?

 

在事務 A 中查詢了兩次 id 大於 1 的,在第一次 id 大於 1 查詢結果中沒有資料,但是由於事務 B 插入了一條 id = 2 的資料,導致事務 A 第二次查詢時能查到事務 B 中插入的資料。

事務中的隔離性:

 

為什麼開發人員必須要了解資料庫鎖?

 

小明注意到在收集資料的過程中,有資料寫到 InnoDB 和其他資料庫有點不同,InnoDB 的可重複讀其實就能解決幻讀了,小明心想:這 InnoDB 還挺牛逼的,我得好好看看到底是怎麼個原理。

InnoDB 鎖型別

小明首先了解了 MySQL 中常見的鎖型別有哪些:

S or X

在 InnoDB 中實現了兩個標準的行級鎖,可以簡單的看為兩個讀寫鎖:

S 共享鎖:又叫讀鎖,其他事務可以繼續加共享鎖,但是不能繼續加排他鎖。

X 排他鎖:又叫寫鎖,一旦加了寫鎖之後,其他事務就不能加鎖了。

 

為什麼開發人員必須要了解資料庫鎖?

 

相容性:是指事務 A 獲得一個某行某種鎖之後,事務 B 同樣的在這個行上嘗試獲取某種鎖,如果能立即獲取,則稱鎖相容,反之叫衝突。

縱軸是代表已有的鎖,橫軸是代表嘗試獲取的鎖。

意向鎖

意向鎖在 InnoDB 中是表級鎖,和它的名字一樣它是用來表達一個事務想要獲取什麼。

意向鎖分為:

  • 意向共享鎖:表達一個事務想要獲取一張表中某幾行的共享鎖。
  • 意向排他鎖:表達一個事務想要獲取一張表中某幾行的排他鎖。

這個鎖有什麼用呢?為什麼需要這個鎖呢?首先說一下如果沒有這個鎖,要給這個表加上表鎖,一般的做法是去遍歷每一行看看它是否有行鎖,這樣的話效率太低。

而我們有意向鎖,只需要判斷是否有意向鎖即可,不需要再去一行行的去掃描。

在 InnoDB 中由於支援的是行級的鎖,因此 InnboDB 鎖的相容性可以擴充套件如下:

 

為什麼開發人員必須要了解資料庫鎖?

 

自增長鎖

自增長鎖是一種特殊的表鎖機制,提升併發插入效能。

對於這個鎖有幾個特點:

  • 在 SQL 執行完就釋放鎖,並不是事務執行完。
  • 對於 insert...select 大資料量插入會影響插入效能,因為會阻塞另外一個事務執行。
  • 自增演算法可以配置。

在 MySQL 5.1.2 版本之後,有了很多優化,可以根據不同的模式來調整自增加鎖的方式。

小明看到了這裡打開了自己的 MySQL 發現是 5.7 之後,便輸入了下面的語句,獲取到當前鎖的模式:

mysql> show variables like 'innodb_autoinc_lock_mode'; 
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 2 |
+--------------------------+-------+
1 row in set (0.01 sec)

在 MySQL 中 innodbautoinclock_mode 有 3 種配置模式 0、1、2,分別對應:

傳統模式:也就是我們最上面的使用表鎖。

連續模式:對於插入的時候可以確定行數的使用互斥量,對於不能確定行數的使用表鎖的模式。

交錯模式:所有的都使用互斥量,為什麼叫交錯模式呢,有可能在批量插入時自增值不是連續的,當然一般來說如果不看重自增值連續一般選擇這個模式,效能是最好的。

InnoDB 鎖演算法

小明已經瞭解到了在 InnoDB 中有哪些鎖型別,但是如何去使用這些鎖,還是得靠鎖演算法。

記錄鎖(Record-Lock)

記錄鎖是鎖住記錄的,這裡要說明的是這裡鎖住的是索引記錄,而不是我們真正的資料記錄:

如果鎖的是非主鍵索引,會在自己的索引上面加鎖之後然後再去主鍵上面加鎖鎖住。

如果沒有表上沒有索引(包括沒有主鍵),則會使用隱藏的主鍵索引進行加鎖。

如果要鎖的列沒有索引,則會進行全表記錄加鎖。

間隙鎖

間隙鎖顧名思義鎖間隙,不鎖記錄。鎖間隙的意思就是鎖定某一個範圍,間隙鎖又叫 gap 鎖,其不會阻塞其他的 gap 鎖,但是會阻塞插入間隙鎖,這也是用來防止幻讀的關鍵。

為什麼開發人員必須要了解資料庫鎖?

next-key 鎖

這個鎖本質是記錄鎖加上 gap 鎖。在 RR 隔離級別下(InnoDB 預設),InnoDB 對於行的掃描鎖定都是使用此演算法,但是如果查詢掃描中有唯一索引會退化成只使用記錄鎖。

為什麼呢? 因為唯一索引能確定行數,而其他索引不能確定行數,有可能在其他事務中會再次新增這個索引的資料造成幻讀。

這裡也說明了為什麼 MySQL 可以在 RR 級別下解決幻讀。

插入意向鎖

插入意向鎖 MySQL 官方對其的解釋:

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

可以看出插入意向鎖是在插入的時候產生的,在多個事務同時寫入不同資料至同一索引間隙的時候,並不需要等待其他事務完成,不會發生鎖等待。

假設有一個記錄索引包含鍵值 4 和 7,不同的事務分別插入 5 和 6,每個事務都會產生一個加在 4-7 之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為資料行並不衝突。

這裡要說明的是如果有間隙鎖了,插入意向鎖會被阻塞。

MVCC

MVCC,多版本併發控制技術。在 InnoDB 中,在每一行記錄的後面增加兩個隱藏列,記錄建立版本號和刪除版本號。通過版本號和行鎖,從而提高資料庫系統併發效能。

在 MVCC 中,對於讀操作可以分為兩種讀:

  • 快照讀:讀取的歷史資料,簡單的 select 語句,不加鎖,MVCC 實現可重複讀,使用的是 MVCC 機制讀取 undo 中的已經提交的資料。所以它的讀取是非阻塞的。
  • 當前讀:需要加鎖的語句,update,insert,delete,select...for update 等等都是當前讀。

在 RR 隔離級別下的快照讀,不是以 begin 事務開始的時間點作為 snapshot 建立時間點,而是以第一條 select 語句的時間點作為 snapshot 建立的時間點。以後的 select 都會讀取當前時間點的快照值。

在 RC 隔離級別下每次快照讀均會建立新的快照。

具體的原理是通過每行會有兩個隱藏的欄位一個是用來記錄當前事務,一個是用來記錄回滾的指向 Undolog。利用 Undolog 就可以讀取到之前的快照,不需要單獨開闢空間記錄。

加鎖分析

小明到這裡,已經學習很多 MySQL 鎖有關的基礎知識,所以決定自己建立一個表搞下實驗。

首先建立了一個簡單的使用者表:

CREATE TABLE `user` ( 
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL,
`comment` varchar(11) CHARACTER SET utf8 DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

然後插入了幾條實驗資料:

insert user select 20,333,333; 
insert user select 25,555,555;
insert user select 20,999,999;

資料庫事務隔離選擇了 RR。

實驗 1

小明開啟了兩個事務,進行了實驗 1,如下圖:

 

為什麼開發人員必須要了解資料庫鎖?

 

小明開啟了兩個事務並輸入了上面的語句,發現事務 B 居然出現了超時,小明看了一下自己明明是對 name = 555 這一行進行的加鎖,為什麼我想插入 name = 556 給我阻塞了。

於是小明開啟命令列輸入:

select * from information_schema.INNODB_LOCKS 

發現在事務 A 中給 555 加了 next-key 鎖,事務 B 插入的時候會首先進行插入意向鎖的插入。

於是得出下面結果:

 

為什麼開發人員必須要了解資料庫鎖?

 

可以看見事務 B 由於間隙鎖和插入意向鎖的衝突,導致了阻塞。

實驗 2

小明發現上面查詢條件用的是普通的非唯一索引,於是小明就試了一下主鍵索引:

 

為什麼開發人員必須要了解資料庫鎖?

 

居然發現事務 B 並沒有發生阻塞,哎這個是咋回事呢,小明有點疑惑,按照實驗 1 的套路應該會被阻塞啊,因為 25-30 之間會有間隙鎖。

於是小明又祭出了命令列,發現只加了 X 記錄鎖。原來是因為唯一索引會降級記錄鎖。

這麼做的理由是:非唯一索引加 next-key 鎖由於不能確定明確的行數有可能其他事務在你查詢的過程中,再次新增這個索引的資料,導致隔離性遭到破壞,也就是幻讀。

唯一索引由於明確了唯一的資料行,所以不需要新增間隙鎖解決幻讀。

 

為什麼開發人員必須要了解資料庫鎖?

 

實驗 3

上面測試了主鍵索引,非唯一索引,這裡還有個欄位是沒有索引,如果對其加鎖會出現什麼呢?

 

為什麼開發人員必須要了解資料庫鎖?

 

小明一看哎喲我去,這個咋回事呢,咋不管是用實驗 1 非間隙鎖範圍的資料,還是用間隙鎖裡面的資料都不行,難道是加了表鎖嗎?

的確,如果用沒有索引的資料,其會對所有聚簇索引上都加上 next-key 鎖。

 

為什麼開發人員必須要了解資料庫鎖?

 

所以大家平常開發的時候如果對查詢條件沒有索引的,一定進行一致性讀,也就是加鎖讀,會導致全表加上索引,會導致其他事務全部阻塞,資料庫基本會處於不可用狀態。

回到事故

死鎖

小明做完實驗之後總算是瞭解清楚了加鎖的一些基本套路,但是之前線上出現的死鎖又是什麼東西呢?

死鎖是指兩個或兩個以上的事務在執行過程中,因爭奪資源而造成的一種互相等待的現象。說明有等待才會有死鎖,解決死鎖可以通過去掉等待,比如回滾事務。

解決死鎖的兩個辦法:

  • 等待超時:當某一個事務等待超時之後回滾該事務,另外一個事務就可以執行了。

但是這樣做效率較低,會出現等待時間,還有個問題是如果這個事務所佔的權重較大,已經更新了很多資料了,但是被回滾了,就會導致資源浪費。

  • 等待圖(wait-for-graph):等待圖用來描述事務之間的等待關係,當這個圖如果出現迴路如下:

 

為什麼開發人員必須要了解資料庫鎖?

 

事務就出現回滾,通常來說 InnoDB 會選擇回滾權重較小的事務,也就是 undo 較小的事務。

線上問題

小明到這裡,基本需要的基本功都有了,於是在自己的本地表中開始復現這個問題:

 

為什麼開發人員必須要了解資料庫鎖?

 

可以看見事務 A 出現被回滾了,而事務 B 成功執行。具體每個時間點發生了什麼呢?

時間點 2:事務 A 刪除 name = '777' 的資料,需要對 777 這個索引加上 next-key 鎖,但是其不存在。

所以只對 555-999 之間加間隙鎖,同理事務 B 也對 555-999 之間加間隙鎖。間隙鎖之間是相容的。

時間點 3:事務 A,執行 insert 操作,首先插入意向鎖,但是 555-999 之間有間隙鎖。

由於插入意向鎖和間隙鎖衝突,事務 A 阻塞,等待事務 B 釋放間隙鎖。事務 B 同理,等待事務 A 釋放間隙鎖。於是出現了 A->B,B->A 迴路等待。

時間點 4:事務管理器選擇回滾事務 A,事務 B 插入操作執行成功。

 

為什麼開發人員必須要了解資料庫鎖?

 

修復 Bug

這個問題總算是被小明找到了,就是因為間隙鎖,現在需要解決這個問題。

這個問題的原因是出現了間隙鎖,那就來去掉它吧:

  • 方案一:隔離級別降級為 RC,在 RC 級別下不會加入間隙鎖,所以就不會出現毛病了,但是在 RC 級別下會出現幻讀,可提交讀都破壞隔離性的毛病,所以這個方案不行。
  • 方案二:隔離級別升級為可序列化,小明經過測試後發現不會出現這個問題,但是在可序列化級別下,效能會較低,會出現較多的鎖等待,同樣的也不考慮。
  • 方案三:修改程式碼邏輯,不要直接刪,改成每個資料由業務邏輯去判斷哪些是更新,哪些是刪除,那些是新增,這個工作量稍大,小明寫這個直接刪除的邏輯就是為了不做這些複雜的事的,所以這個方案先不考慮。
  • 方案四:較少的修改程式碼邏輯,在刪除之前,可以通過快照查詢(不加鎖),如果查詢沒有結果,則直接插入;如果有通過主鍵進行刪除,在之前第三節實驗 2 中,通過唯一索引會降級為記錄鎖,所以不存在間隙鎖。

經過考慮小明選擇了第四種,馬上進行了修復,然後上線觀察驗證,發現現在已經不會出現這個 Bug 了,這下小明總算能睡個安穩覺了。

如何防止死鎖

小明通過基礎的學習和平常的經驗總結了如下幾點:

  • 以固定的順序訪問表和行。交叉訪問更容易造成事務等待迴路。
  • 儘量避免大事務,佔有的資源鎖越多,越容易出現死鎖。建議拆成小事務。
  • 降低隔離級別。如果業務允許(上面也分析了,某些業務並不能允許),將隔離級別調低也是較好的選擇,比如將隔離級別從 RR 調整為 RC,可以避免掉很多因為 gap 鎖造成的死鎖。
  • 為表新增合理的索引。防止沒有索引出現表鎖,出現死鎖的概率會突增。

最後

由於篇幅有限很多東西並不能介紹全,如果感興趣的同學可以閱讀《MySQL 技術內幕-InnoDB 引擎》第 6 章,以及何大師的 MySQL 加鎖處理分析。

 

轉自

為什麼開發人員必須要了解資料庫鎖? https://www.toutiao.com/a6604986432278757901/?tt_from=mobile_qq&utm_campaign=client_share&timestamp=1541639531&app=news_article&utm_source=mobile_qq&iid=26112390770&utm_medium=toutiao_ios&group_id=6604986432278757901