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

轉自http://blog.csdn.net/yinwenjie/article/details/5273506

================================
(接上文《架構設計:系統存儲(7)——MySQL數據庫性能優化(3)》)
4-3、InnoDB中的鎖

雖然鎖機制是InnoDB引擎中爲了保證事務性而自然存在的,在索引、表結構、配置參數一定的前提下,InnoDB引擎加鎖過程是一樣的,所以理論上來說也就不存在“鎖機制能夠提升性能”這樣的說法。但如果技術人員不理解InnoDB中的鎖機制或者混亂、錯誤的索引定義和同樣混亂的SQL寫操作語句共同作用,那麼導致死鎖出現的可能性就越大,需要InnoDB進行死鎖檢測的情況就越多,最終導致不必要的性能浪費甚至事務執行失敗。所以理解InnoDB引擎中的鎖機制可以幫助我們在高併發系統中儘可能不讓鎖和死鎖成爲數據庫服務的一個性能瓶頸。
4-3-1、InnoDB中的鎖類型

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

共享鎖(S鎖)

由於InnoDB引擎支持事務,所以需要鎖機制在多個事務同時工作時保證每個事務的ACID特性。共享鎖的特性是多個事務可以同時爲某個資源加鎖後進行讀操作,並且這些事務間不會出現相互等待的現象。

排它鎖(X鎖)

排它鎖又被稱爲獨佔鎖,一旦某個事務對資源加排它鎖,其它事務就不能再爲這個資源加共享鎖或者排它鎖了。一直要等待到當前的獨佔鎖從資源上解除後,才能繼續對資源進行操作。排它鎖只會影響其他事務的加鎖操作,也就是說如果其它事務只是使用簡單的SELECT查詢語句檢索資源,就不會受到影響,因爲這些SELECT查詢語句不會試圖爲資源加任何鎖,也就不會受資源上已有的排它鎖的影響。我們可以用一張表表示排它鎖和共享鎖的互斥關係:
這裏寫圖片描述
排它鎖和共享鎖的互斥關係

行級鎖(Record lock)

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

# 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==10、13、15、17、19、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-2、加鎖過程實例

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-1、 行鎖加鎖過程

首先我們演示一個工作在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-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-3、表鎖加鎖過程

上文已經提到,索引一旦失效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-3、死鎖

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

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

這裏寫圖片描述

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

這裏寫圖片描述

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

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

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

這裏寫圖片描述

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

首先,表鎖本身並不會導致死鎖,它只是InnoDB中的一種機制。但是表鎖會一次鎖定數據表中的所有聚集索引項。這就增加了表鎖所在事務需要等待前序事務執行完畢才能繼續執行的機率。而且這種等待狀態還很可能在一個事務中出現多次——因爲有多個事務在同時執行嘛。在這個過程中由於表鎖逐漸佔據了聚簇索引上絕大多數的索引項,所以這又增加了和其它正在執行的事務搶佔鎖定資源的,最終增加了死鎖發生的機率。

由於需要進行表鎖定的事務,需要將數據表中的所有聚集索引全部鎖定後(如果在默認的事務級別下還要加GAP鎖),才能完成事務的執行過程,所以這會導致後序事務全部進入等待狀態。而InnoDB引擎根本無法預知表鎖所在事務是否佔據了後續資源需要使用的索引項。這就與之前的提到的情況一樣,增加了死鎖發生的機率。

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