架構設計:系統存儲(8)——MySQL數據庫性能優化(4)

================================
(接上文《架構設計:系統存儲(7)——MySQL數據庫性能優化(3)》)web

4-三、InnoDB中的鎖

雖然鎖機制是InnoDB引擎中爲了保證事務性而天然存在的,在索引、表結構、配置參數必定的前提下,InnoDB引擎加鎖過程是同樣的,因此理論上來講也就不存在「鎖機制可以提高性能」這樣的說法。但若是技術人員不理解InnoDB中的鎖機制或者混亂、錯誤的索引定義和一樣混亂的SQL寫操做語句共同做用,那麼致使死鎖出現的可能性就越大,須要InnoDB進行死鎖檢測的狀況就越多,最終致使沒必要要的性能浪費甚至事務執行失敗。因此理解InnoDB引擎中的鎖機制能夠幫助咱們在高併發系統中儘量不讓鎖和死鎖成爲數據庫服務的一個性能瓶頸sql

4-3-一、InnoDB中的鎖類型

本文講解的鎖機制主要依據MySQL Version 5.6以及以前的版本(這是目前線上環境使用最多的版本),在MySQL Version 5.7以及最新的MySQL 8.0中InnoDB引擎的鎖類型發生了一些變化(後文會說起),但基本思路沒有變化。InnoDB引擎中的鎖類型按照獨佔形式能夠分爲共享鎖和排它鎖(還有意向性共享鎖和意向性排它鎖);按照鎖定數據的範圍能夠分爲行級鎖(其它引擎中還有頁級鎖的定義)、間隙鎖、間隙複合鎖??和表鎖;爲了保證鎖的粒度可以至上而下傳遞,InnoDB中還設計有不能被用戶干預的意向共享鎖和意向排它鎖。數據庫

  • 共享鎖(S鎖)

因爲InnoDB引擎支持事務,因此須要鎖機制在多個事務同時工做時保證每一個事務的ACID特性。共享鎖的特性是多個事務能夠同時爲某個資源加鎖後進行讀操做,而且這些事務間不會出現相互等待的現象。性能優化

  • 排它鎖(X鎖)

排它鎖又被稱爲獨佔鎖,一旦某個事務對資源加排它鎖,其它事務就不能再爲這個資源加共享鎖或者排它鎖了。一直要等待到當前的獨佔鎖從資源上解除後,才能繼續對資源進行操做。排它鎖只會影響其餘事務的加鎖操做,也就是說若是其它事務只是使用簡單的SELECT查詢語句檢索資源,就不會受到影響,由於這些SELECT查詢語句不會試圖爲資源加任何鎖,也就不會受資源上已有的排它鎖的影響。咱們能夠用一張表表示排它鎖和共享鎖的互斥關係:架構

鎖類型 共享鎖S 排它鎖X
共享鎖S 不互斥:多個共享鎖不會相互影響相互等待 互斥:若是某個資源要加共享鎖,則須要等待到資源上的排它鎖配解除後,才能進行這個操做
排它鎖X 互斥:若是資源要加排它鎖,則須要等待到資源上全部共享鎖都被解除後,才能進行這個操做 互斥:若是某個資源要加排它鎖,則須要等待到資源上的排它鎖配解除後,才能進行這個操做

排它鎖和共享鎖的互斥關係併發

  • 行級鎖(Record lock)

行級鎖是InnoDB引擎中對鎖的最小支持粒度,便是指這個鎖能夠鎖定數據表中某一個具體的數據行,鎖的類型能夠是排它鎖也能夠是共享鎖。例如讀者能夠在兩個獨立事務中同時使用如下語句查詢指定的行,可是兩個事務並不會相互等待:svg

# lock in share mode 是爲知足查詢條件的數據行加共享鎖 # 注意它和直接使用select語句的不一樣特性 select * from myuser where id = 6 lock in share mode;
  • 間隙鎖(GAP鎖)

間隙鎖只有在特定事務級別下才會使用,具體來講是「可重複讀」(Repeatable Read )這樣的事務級別,這也是InnoDB引擎默認的事務級別,它的大體解釋是不管在這個事務中執行多少次相同語句的當前讀操做,其每次讀取的記錄內容都是同樣的,並不受外部事務操做的影響。間隙鎖主要爲了防止多個事務在交叉工做的狀況下,特別是同時進行數據插入的狀況下出現幻讀。舉一個簡單的例子,事務A中的操做正在執行如下update語句的操做:高併發

...... # 事務A正在執行一個範圍內數據的更新操做 # 大意是說將用戶會員卡號序列大於10的全部記錄中user_name字段所有更新爲一個新的值 update myuser set user_name = '用戶11' where user_number >= 10; ......

其中user_number帶有一個索引(後續咱們將討論這個索引類型對間隙鎖策略的影響),這樣的檢索條件很顯然會涉及到一個範圍的數據都將被更新(例如user_number==十、1三、1五、1七、1九、21……),於此同時有另外一個事務B正在執行如下語句:性能

...... # 事務B正在執行一個插入操做 insert into myuser(.........,'user_number') values (.........,11); # 插入一個卡號爲11的新會員,而後提交事務B ......

若是InnoDB只鎖住user_number值爲10的非聚簇索引和相應的聚簇索引,顯然就會形成一個問題:在A事務處理過程當中,忽然多出了一條知足更新條件的記錄。事務A會很糾結的,很尷尬的。若是讀者是InnoDB引擎的開發者,您會怎麼作呢?正確的作法是爲知足事務A所執行檢索條件的整個範圍加鎖,這個鎖不是加在某個或某幾個具體的記錄上,由於那樣作仍是沒法限制相似插入「一個卡號爲11的新紀錄」這樣的狀況,而是加在到具體索引和下一個索引之間,告訴這個索引B+樹的其它使用者,包括這個索引在內的以後區域都不容許使用。這樣的鎖機制稱爲間隙鎖(GAP鎖)。優化

間隙鎖和行級鎖組合起來稱爲Next-Key Lock,實際上這兩種鎖通常狀況下都是組合工做的。

  • 表級鎖:沒有能夠檢索的索引,就沒法使用InnoDB特定的鎖。另外,索引失效InnoDB也會爲整個數據表加鎖。若是表級鎖的性質是排它鎖(實際上大多數狀況是這樣的鎖),那麼全部試圖爲這張數據表中任何資源加共享鎖或者排它鎖的事務都必須等待在數據表上的排它鎖被解除後,才能繼續工做。表級鎖能夠看做基於InnoDB引擎工做的數據表的最悲觀鎖,它是InnoDB引擎爲了保持事務特性的一場豪賭。例如咱們有以下的數據表結構:

uid(PK) varchar
user_name varchar
user_sex int

這張數據表中只有一個由uid字段構成的主索引。接着兩個事務同時執行如下語句:

begin;
select * from t_user where uid = 2 lock in share mode;
#都先不執行commit,以便觀察現象 #commit;

這裏的select查詢雖然使用的檢索依據是uid,可是設置檢索條件時uid的varchar類型卻被錯誤的使用成了int類型。那麼數據表將再也不使用索引進行檢索,轉而進行全表掃秒。這是一種典型的索引失效狀況,最終讀者觀察到的現象是,在執行以上同一查詢語句的兩個事務中,有一個返回了查詢結果,可是另一個一直爲等待狀態。以上的小例子也可讓讀者看到,科學管理索引在InnoDB引擎中是何等重要。本文後續部分將向讀者介紹表級鎖的實質結構。

  • 意向共享鎖(IS鎖)和意向排它鎖(IX鎖)

爲了在某一個具體索引上加共享鎖,事務須要首先爲涉及到的數據表加意向共享鎖(IS鎖);爲了在某一個具體因此上加排它鎖,事務須要首先爲涉及到的數據表加意向排它鎖(IX鎖)。這樣InnoDB能夠總體把握在併發的若干個事務中,讓哪些事務優先執行更能產生好的執行效果。意向共享鎖是InnoDB引擎自動控制的,開發人員沒法人工干預,也不須要干預。

4-3-二、加鎖過程實例

InnoDB引擎中的鎖機制基於索引才能工做。對數據進行鎖定時並非真的鎖定數據自己,而是對數據涉及的彙集索引和非彙集索引進行鎖定。在以前的文章中咱們已經介紹到,InnoDB引擎中的索引按照B+樹的結構進行組織,那麼加鎖的過程很明顯就是在對應的B+樹上進行加鎖位置檢索和進行標記的過程。而且InnoDB引擎中的非聚簇索引最終都要依靠聚簇索引才能找到具體的數據記錄位置,因此加鎖的過程都涉及到對聚簇索引進行操做。

SELECT關鍵字的查詢操做通常狀況下都不會涉及到鎖的問題(這種類型的讀操做稱爲快照讀),但並非全部的查詢操做都不涉及到鎖機制。只要SELECT屬於某種寫操做的前置子查詢/檢索或者開發人員顯式爲SELECT加鎖,這些SELECT語句就涉及到鎖機制——這種讀操做稱爲當前讀。而執行Update、Delete、Insert操做時,InnoDB會根據會根據操做中where檢索條件所涉及的一條或者多條數據加排它鎖。

爲了進一步詳細說明各類典型的加鎖過程,本小節爲讀者準備了幾個實例場景,並使用圖文混合的方式從索引邏輯層面上進行說明。後續的幾種實例場景都將以如下數據表和數據做爲講解依據:

CREATE TABLE `myuser` ( `Id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) NOT NULL DEFAULT '', `usersex` int(9) NOT NULL DEFAULT '0', `user_number` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`Id`), UNIQUE KEY `number_index` (`user_number`), KEY `name_index` (`user_name`) )

這張表中有三個索引,一個是以id字段爲依據的聚簇索引,一個是以user_name字段爲依據的非惟一鍵非聚簇索引,最後一個是以user_number字段爲依據的惟一鍵非聚簇索引。咱們將在實例場景中觀察惟一鍵索引和非惟一鍵索引在加鎖,特別是加GAP鎖的狀況的不一樣點。這張數據表中的數據狀況以下圖所示:

示例數據

4-3-2-一、 行鎖加鎖過程

首先咱們演示一個工做在InnoDB引擎下的數據表只加行鎖的狀況。

begin;
update myuser set user_name = '用戶11' where id = 10;
commit;

以上事務中只有一條更新操做,它直接使用聚簇索引做爲檢索條件。聚簇索引確定是一個惟一鍵索引,因此InnoDB得出的加鎖條件也就不須要考慮相似「insert into myuser(id,………) values(10,………)」這樣的字段重複狀況。由於若是有事務執行了這樣的語句,就會直接報錯退出。那麼最終的加鎖結果就是:只須要在聚簇索引上加X鎖。

這裏寫圖片描述
(額~~~你要問我爲何樹結構會是連續遍歷的?請重讀B+樹的介紹)

其它事務依然能夠對聚簇索引上的其它節點進行操做,例如使用update語句更新id爲14的數據:

begin;
update myuser set user_name = '用戶1414' where id = 14;
commit;

固然,因爲這樣的執行過程沒有在X鎖臨近的邊界加GAP鎖,因此開發人員也可使用insert語句插入一條id爲11的數據:

begin;
insert into myuser(id,user_name,usersex,user_number) values (11,'用戶1_1',1,'110110110');
commit;

4-3-2-二、間隙鎖加鎖過程

工做在InnoDB引擎下的數據表,更多的操做過程都涉及到加間隙鎖(GAP)的狀況,這是由於畢竟大多數狀況下咱們定義和使用的索引都不是惟一鍵索引,都在「可重複讀」的事務級別下存在「幻讀」風險。請看以下事務執行過程:

begin;
update myuser set usersex = 0 where user_name = '用戶8' commit;

這個事務操做過程當中的update語句,使用非惟一鍵非聚簇索引’name_index’進行檢索。InnoDB引擎進行分析後發現存在幻讀風險,例如可能有一個事務在同時執行如下操做:

begin;
insert into myuser(id,user_name,usersex,user_number) values (11,'用戶8',1,'110110110');
# 或者執行如下插入
# insert into myuser(id,user_name,usersex,user_number) values (11,'用戶88',1,'110110110');
commit;

因此InnoDB須要在X鎖臨近的位置加GAP鎖,避免幻讀:

這裏寫圖片描述

以上示意圖有一個注意點,在許多技術文章中對GAP鎖的講解都是以int字段類型爲基準,可是這裏講解所使用的類型是varchar。因此在加GAP鎖的時候,看似’用戶8’和’用戶9’這兩個索引節點沒有中間值了。可是字符串也是能夠排序的,因此’用戶8’和’用戶9’這兩個字符串之間其實是能夠放置不少中間值的,例如’用戶88’、’用戶888’、’用戶8888’等。

這就是爲何另外的事務執行相似」insert into myuser(id,user_name,usersex,user_number) values (11,’用戶88’,1,’110110110’);」這樣的語句,一樣會進入等待狀態:由於有GAP鎖進行獨佔控制。

4-3-2-三、表鎖加鎖過程

上文已經提到,索引一旦失效InnoDB也會爲整個數據表加鎖。那麼「爲整個數據表加鎖」這個動做怎麼理解呢?不少技術文章在這裏通常都歸納爲一句話「在XXX數據表上加鎖」。要弄清楚表鎖的加載位置,咱們就須要進行實踐驗證。首先,爲了更好的查看InnoDB引擎的工做狀態和加鎖狀態,咱們須要打開InnoDB引擎的監控功能:

# 使用如下語句開啓鎖監控
set GLOBAL innodb_status_output=ON;
set GLOBAL innodb_status_output_locks=ON;

接下來咱們就可使用myuser數據表中沒有鍵立索引的「usersex」字段進行加鎖驗證:

begin;
update myuser set user_name = '用戶1414' where usersex = 1;
# 先不忙使用commit,以便觀察鎖狀態 #commit;

在執行以上事務以前,myuser數據表中最新的記錄狀況以下圖所示:

這裏寫圖片描述

能夠看到myuser數據表中一共有13條記錄,其中知足「usersex = 1」的數據一共有9條記錄。那麼按照InnoDB引擎行鎖機制來講,就應該只有這9條記錄被鎖定,那麼是否如此呢?咱們經過執行InnoDB引擎的狀態監控功能來進行驗證:

show engine innodb status;

# 如下是執行結果(省略了一部分不相關信息)
=====================================
2016-10-06 22:22:49 2f74 INNODB MONITOR OUTPUT
=====================================
.......
------------
TRANSACTIONS
------------
Trx id counter 268113
Purge done for trx's n:o < 268113 undo n:o < 0 state: running but idle
History list length 640
LIST OF TRANSACTIONS FOR EACH SESSION:
......

---TRANSACTION 268103, ACTIVE 21 sec
2 lock struct(s), heap size 360, 14 row lock(s), undo log entries 9
MySQL thread id 5, OS thread handle 0x1a3c, query id 311 localhost 127.0.0.1 root cleaning up
TABLE LOCK table `qiang`.`myuser` trx id 268103 lock mode IX
RECORD LOCKS space id 1014 page no 3 n bits 152 index `PRIMARY` of table `qiang`.`myuser` trx id 268103 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 79 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 000000041723; asc      #;;
 2: len 7; hex 2c000001e423fd; asc ,    # ;;
 3: len 8; hex e794a8e688b73130; asc       10;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80018a92; asc     ;;

Record lock, heap no 80 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 8000000e; asc     ;;
 1: len 6; hex 000000041721; asc      !;;
 2: len 7; hex 2b000001db176a; asc +     j;;
 3: len 8; hex e794a8e688b73134; asc       14;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80022866; asc   (f;;

Record lock, heap no 81 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000012; asc     ;;
 1: len 6; hex 00000004171f; asc       ;;
 2: len 7; hex 2a000001da17b2; asc *      ;;
 3: len 8; hex e794a8e688b73138; asc       18;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 8002c63a; asc    :;;

Record lock, heap no 82 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000016; asc     ;;
 1: len 6; hex 00000004171d; asc       ;;
 2: len 7; hex 290000024d0237; asc )   M 7;;
 3: len 8; hex e794a8e688b73232; asc       22;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80035c3c; asc   \<;;

Record lock, heap no 86 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 41000002580110; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 80002b67; asc   +g;;

...... 這裏爲節約篇幅,省略了6條行鎖記錄......

Record lock, heap no 93 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000008; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 410000025802b4; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 80015b38; asc   [8;;

Record lock, heap no 94 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000009; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 410000025802f0; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 8001869f; asc     ;;
......

經過以上日誌咱們觀察到的比較重要狀況是,編號爲268103的事務擁有兩個鎖結構(2 lock struct(s)),其中一個鎖結構是意向性排它鎖IX,這個鎖結構一共鎖定了一條記錄(這條記錄並非myuser數據表中的一條記錄);另一個鎖結構是排它鎖(X),這個鎖結構加載在主鍵索引上(「page no 3 n bits 152 index ‘PRIMARY’ of table ‘qiang’.’myuser’」),而且鎖定了13條記錄。這13條記錄就是myuser數據表中的全部數據記錄,並不是咱們最早預計的9條記錄。

這就是表鎖在鎖定規律上的具體表現:由於不能基於索引檢索查詢條件,因此就只能基於彙集索引進行全表掃描。由於不能肯定彙集索引上哪些Page中數據知足檢索條件,因此只能用排它鎖一邊鎖定數據一邊進行檢索。由於要知足事務的ACID特性,因此在事務完成執行(或錯誤回滾)前都不能解除鎖定:

這裏寫圖片描述

因爲咱們一直討論的InnoDB引擎默認的事務級別是「可重複度」(Repeatable Read),因此爲了不幻讀,InnoDB還會在每個排它性行鎖周圍都加上間隙鎖(GAP)。那麼在這個事務級別下表鎖最終的邏輯表現就以下圖所示:

這裏寫圖片描述

是的,沒有索引能夠提供檢索依據的數據表正在進行一場豪賭!這仍是隻有13條數據的狀況下,那麼試想一下若是數據表中有10,000,000條記錄呢?這不只形成資源的浪費,更重要的是表鎖是形成死鎖的重要緣由,並且由此引起的InnoDB自動解鎖代價很是昂貴(後文會詳細講到)。

4-3-三、死鎖

一旦構成死鎖,InnoDB會盡量的幫助開發者解除死鎖。其作法是自動終止一些事務的運行從而釋放鎖定狀態。在上一小節咱們示範的多個加鎖場景,它們雖然都構成鎖等待,可是都沒有構成死鎖。那麼本文就要首先說明一下,什麼樣的狀況才構成死鎖。

4-3-3-一、什麼是死鎖

兩個或者多個事務相互等待對方已鎖定的資源,而彼此都不爲協助對方達成操做目而主動釋放已鎖定的資源,這樣的狀況就稱爲死鎖。請區分正常的鎖等待和死鎖的區別,例如如下示意圖中的鎖等待並不構成死鎖:

這裏寫圖片描述

上圖中的狀況只能稱爲鎖資源等待,這是由於當A事務完成處理後就會釋放所佔據的資源上的鎖,這樣B事務就能夠繼續進行處理。而且在這個過程當中沒有任何因素阻止A事務完成,也沒有任何因素阻止B事務在隨後的操做中獲取鎖。可是,如下示意圖中的兩個事務就在相互等待對方已鎖定的資源,這就稱爲死鎖:

這裏寫圖片描述

上圖中A事務已爲id1和id2這兩個索引項加鎖,當它準備爲id4這個索引加鎖時,卻發現id4已經被事務B加鎖,因而事務A進行等待過程。恰巧的是,B事務在爲id四、id5加鎖後,正在等待爲id2這個索引項加鎖。因而最後形成的結果就是事務A和事務B相互等待對方釋放資源。注意,因爲須要保證事務的ACID特性,因此A事務已經鎖定的索引id一、id2在事務A的等待過程當中,是不會被釋放的;一樣事務B已經鎖定的索引id四、id5在等待過程當中也不會被釋放。很明顯若是沒有外部干預,這個互相等待的過程將一直持續下去。這就是一個典型的死鎖現象。在實際應用場景中,每每會由超過兩個事務共同構成死鎖現象,甚至會出現強制終止某一個等待的事務後依然不能解除死鎖的複雜狀況。

4-3-3-二、死鎖出現的緣由

死鎖形成的根本緣由和上層MySQL服務和下層InnoDB引擎的協調方式有關:在上層MySQL服務和下層InnoDB引擎配合進行Update、Delete和Insert操做時, 對知足條件的索引加X鎖的操做是逐步進行的

當InnoDB進行update、delete或者insert操做時,若是有多條記錄知足操做要求,那麼InnoDB引擎會鎖定一條記錄(其實是相應的索引)而後再對這條記錄進行處理,完成後再鎖定下一條記錄進行處理。這樣依次循環直到全部知足條件的數據被處理完,最後再統一釋放事務中的全部鎖。若是這個過程當中某個將要鎖定的記錄已經被其它事務搶先鎖定,則本事務就進入等待狀態,一直等待到鎖定的資源被釋放爲止。

這裏寫圖片描述

要知道在正式的生成環境中,可能會同時有多個事務對某一個數據表上同一個範圍內的數據進行加鎖(加X鎖後進行寫操做)操做。而InnoDB引擎和MySQL服務的交互採用的這種方式極可能使這些事務各自持有某些記錄的行鎖,但又不構成讓事務繼續執行下去的條件。那爲何說在生產環境下,多數死鎖狀態的出現是由於表鎖致使的呢?

  • 首先,表鎖自己並不會致使死鎖,它只是InnoDB中的一種機制。可是表鎖會一次鎖定數據表中的全部彙集索引項。這就增長了表鎖所在事務須要等待前序事務執行完畢才能繼續執行的概率。並且這種等待狀態還極可能在一個事務中出現屢次——由於有多個事務在同時執行嘛。在這個過程當中因爲表鎖逐漸佔據了聚簇索引上絕大多數的索引項,因此這又增長了和其它正在執行的事務搶佔鎖定資源的,最終增長了死鎖發生的概率。

  • 因爲須要進行表鎖定的事務,須要將數據表中的全部彙集索引所有鎖定後(若是在默認的事務級別下還要加GAP鎖),才能完成事務的執行過程,因此這會致使後序事務所有進入等待狀態。而InnoDB引擎根本沒法預知表鎖所在事務是否佔據了後續資源須要使用的索引項。這就與以前的提到的狀況同樣,增長了死鎖發生的概率。

相關文章
相關標籤/搜索