相關命令:
- show engines; 查看數據庫支持的引擎
- show variables like '%storage_engine%'; 查看數據庫默認的引擎
- select @@global.tx_isolation; 查詢數據庫的隔離級別
- show variables like 'innodb_autoinc_lock_mode'; 獲取到當前自增長鎖的模式
- select * from information_schema.INNODB_LOCKS; 獲取到當前加鎖情況
- show InnoDB status; 查看最近死鎖的日志。
InnoDB 鎖類型
S or X (共享鎖&排他鎖)
在 InnoDB 中實現了兩個標准的行級鎖,可以簡單的看為兩個讀寫鎖:
-
S 共享鎖:又叫讀鎖,其他事務可以繼續加共享鎖,但是不能繼續加排他鎖。
-
X 排他鎖:又叫寫鎖,一旦加了寫鎖之后,其他事務就不能加鎖了。
IS or IX (共享&排他)意向鎖
意向鎖在 InnoDB 中是表級鎖,意向鎖分為:
-
意向共享鎖:表達一個事務想要獲取一張表中某幾行的共享鎖。
-
意向排他鎖:表達一個事務想要獲取一張表中某幾行的排他鎖。
這個鎖有什么用呢?為什么需要這個鎖呢?
首先說一下如果沒有這個鎖,要給這個表加上表鎖,一般的做法是去遍歷每一行看看它是否有行鎖,這樣的話效率太低。而我們有意向鎖,只需要判斷是否有意向鎖即可,不需要再去一行行的去掃描。
兼容性:是指事務 A 獲得一個某行某種鎖之后,事務 B 同樣的在這個行上嘗試獲取某種鎖,如果能立即獲取,則稱鎖兼容,反之叫沖突。
縱軸是代表已有的鎖,橫軸是代表嘗試獲取的鎖。
自增長鎖
自增長鎖是一種特殊的表鎖機制,提升並發插入性能。對於這個鎖有幾個特點:
-
在 SQL 執行完就釋放鎖,並不是事務執行完。
-
對於 insert...select 大數據量插入會影響插入性能,因為會阻塞另外一個事務執行。
-
自增算法可以配置。
在 MySQL 中 innodb_auto_inclock_mode 有 3 種配置模式 0、1、2,分別對應:
-
傳統模式:使用表鎖。
-
連續模式:對於插入的時候可以確定行數的使用互斥量,對於不能確定行數的使用表鎖的模式。
-
交錯模式:所有的都使用互斥量,為什么叫交錯模式呢,有可能在批量插入時自增值不是連續的,當然一般來說如果不看重自增值連續一般選擇這個模式,性能是最好的。
InnoDB 鎖算法
記錄鎖(Record-Lock)
記錄鎖是鎖住記錄的,鎖住的是索引記錄,而不是我們真正的數據記錄:
-
如果鎖的是非主鍵索引,會在自己的索引上面加鎖之后然后再去主鍵上面加鎖鎖住。
-
如果表上沒有索引(包括沒有主鍵),則會使用隱藏的主鍵索引進行加鎖。
-
如果要鎖的沒有索引,則會進行全表記錄加鎖。
間隙鎖
間隙鎖顧名思義鎖間隙,不鎖記錄。鎖間隙的意思就是鎖定某一個范圍,間隙鎖又叫 gap 鎖,其不會阻塞其他的 gap 鎖,但是會阻塞插入間隙鎖,這也是用來防止幻讀的關鍵。
next-key 鎖
這個鎖本質是記錄鎖加上 gap 鎖。在 RR 隔離級別下(InnoDB 默認),InnoDB 對於行的掃描鎖定都是使用此算法,但是如果查詢掃描中有唯一索引會退化成只使用記錄鎖。
因為唯一索引能確定行數,而其他索引不能確定行數,需要使用間隙鎖防止其他事務中再次添加這個索引的數據造成幻讀。RR 隔離級別下,InnoDB 使用 Next-Key Lock 算法避免了幻讀。
插入意向鎖
插入意向鎖是在插入的時候產生的,在多個事務同時寫入不同數據至同一索引間隙的時候,並不需要等待其他事務完成,不會發生鎖等待。
假設有一個記錄索引包含鍵值 4 和 7,不同的事務分別插入 5 和 6,每個事務都會產生一個加在 4-7 之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為數據行並不沖突。
這里要說明的是如果有間隙鎖了,插入意向鎖會被阻塞。
MVCC
MVCC 參考:https://www.cnblogs.com/wade-luffy/p/8686883.html
MVCC,多版本並發控制技術。在 InnoDB 中,在每一行記錄的后面增加兩個隱藏列,記錄創建版本號和刪除版本號。通過版本號和行鎖,從而提高數據庫系統並發性能。
在 MVCC 中,對於讀操作可以分為兩種讀:
-
快照讀:讀取的歷史數據,簡單的 select 語句,不加鎖,MVCC 實現可重復讀,使用的是 MVCC 機制讀取 undo 中的已經提交的數據。所以它的讀取是非阻塞的。
-
當前讀:需要加鎖的語句,update,insert,delete,select...for update 等等都是當前讀。
在 RR 隔離級別下的快照讀,不是以 begin 事務開始的時間點作為 snapshot 建立時間點,而是以第一條 select 語句的時間點作為 snapshot 建立的時間點。以后的 select 都會讀取當前時間點的快照值。
在 RC 隔離級別下總是讀取被鎖定行最新的快照數據。
具體的原理是通過每行會有兩個隱藏的字段一個是用來記錄當前事務,一個是用來記錄回滾的指向 Undolog。利用 Undolog 就可以讀取到之前的快照,不需要單獨開辟空間記錄。
死鎖檢測
死鎖是指兩個或兩個以上的事務在執行過程中,因爭奪資源而造成的一種互相等待的現象。說明有等待才會有死鎖,解決死鎖可以通過去掉等待,比如回滾事務。
解決死鎖的兩個辦法:
-
等待超時:當某一個事務等待超時之后回滾該事務,另外一個事務就可以執行了。
但是這樣做效率較低,會出現等待時間,還有個問題是如果這個事務所占的權重較大,已經更新了很多數據了,但是被回滾了,就會導致資源浪費。
-
等待圖(wait-for-graph):等待圖用來描述事務之間的等待關系,當這個圖如果出現回路如下:事務就出現回滾,通常來說 InnoDB 會選擇回滾權重較小的事務,也就是 undo 較小的事務。
如何防止死鎖:
-
以固定的順序訪問表和行。交叉訪問更容易造成事務等待回路。
-
盡量避免大事務,占有的資源鎖越多,越容易出現死鎖。建議拆成小事務。
-
降低隔離級別。如果業務允許(上面也分析了,某些業務並不能允許),將隔離級別調低也是較好的選擇,比如將隔離級別從 RR 調整為 RC,可以避免掉很多因為 gap 鎖造成的死鎖。
-
為表添加合理的索引。防止沒有索引出現表鎖,出現死鎖的概率會突增。
實例分析
數據庫事務隔離選擇了 RR。
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;
demo 1 select 普通索引 鎖競爭
發現在事務 A 中給 555 加了 next-key 鎖,事務 B 插入的時候會首先進行插入意向鎖的插入。
事務 B 由於間隙鎖和插入意向鎖的沖突,導致了阻塞。
demo 2 select 唯一索引
上面查詢條件用的是普通的非唯一索引,現在試下主鍵索引:
非唯一索引加 next-key 鎖由於不能確定明確的行數有可能其他事務在你查詢的過程中,再次添加這個索引的數據,導致隔離性遭到破壞,也就是幻讀。
唯一索引由於明確了唯一的數據行,所以不需要添加間隙鎖解決幻讀。
demo 3 select 沒有索引 鎖競爭
上面測試了主鍵索引,非唯一索引,再試下一個沒有索引
如果用沒有索引的數據,其會對所有聚簇索引上都加上 next-key 鎖。
平常開發的時候如果對查詢條件沒有索引的,一定進行一致性讀,也就是加鎖讀,會導致全表加上索引,會導致其他事務全部阻塞,數據庫基本會處於不可用狀態。
demo 4 delete 死鎖
時間點 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 插入操作執行成功。
間隙鎖帶來的死鎖問題解決思路
方案一:隔離級別降級為 RC,在 RC 級別下不會加入間隙鎖,所以就不會出現毛病了,但是在 RC 級別下會出現幻讀,所以這個方案不行。
方案二:隔離級別升級為可序列化,不會出現這個問題,但是在可序列化級別下,性能會較低,會出現較多的鎖等待,同樣的也不考慮。
方案三:修改代碼邏輯,不要直接刪(因為刪除會加next-key鎖),改成每個數據由業務邏輯去判斷哪些是更新,哪些是刪除,那些是添加(減少鎖時間),這個工作量稍大,所以這個方案可選。
方案四:較少的修改代碼邏輯,在刪除之前,可以通過快照查詢(不加鎖),如果查詢沒有結果,則直接插入;如果有通過主鍵進行刪除,通過唯一索引會降級為記錄鎖,所以不存在間隙鎖。