一張圖徹底搞懂 MySQL 的鎖機制[更新一]
導(dǎo)語
鎖是計算機協(xié)調(diào)多個進程或線程并發(fā)訪問某一資源的機制。
在數(shù)據(jù)庫中,除傳統(tǒng)的計算資源(如 CPU、RAM、I/O 等)的爭用以外,數(shù)據(jù)也是一種供許多用戶共享的資源。如何保證數(shù)據(jù)并發(fā)訪問的一致性、有效性是所有數(shù)據(jù)庫必須解決的一 個問題,鎖沖突也是影響數(shù)據(jù)庫并發(fā)訪問性能的一個重要因素。
從這個角度來說,鎖對數(shù)據(jù)庫而言顯得尤其重要,也更加復(fù)雜。本章我們著重討論 MySQL 鎖機制 的特點,常見的鎖問題,以及解決 MySQL 鎖問題的一些方法或建議。
Mysql 用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。這些鎖統(tǒng)稱為悲觀鎖(Pessimistic Lock)。
題綱

相關(guān)書籍推薦

MySQL鎖分類
相對其他數(shù)據(jù)庫而言,MySQL 的鎖機制比較簡單,其最 顯著的特點是不同的存儲引擎支持不同的鎖機制。
比如,MyISAM 和 MEMORY 存儲引擎采用的是表級鎖(table-level locking);
BDB 存儲引擎采用的是頁面鎖(page-level locking),但也支持表級鎖;
InnoDB 存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。
1.表級鎖:開銷小,加鎖快;不會出現(xiàn)死鎖;鎖定粒度大,發(fā)生鎖沖突的概率最高,并發(fā)度最低。
2.行級鎖:開銷大,加鎖慢;會出現(xiàn)死鎖;鎖定粒度最小,發(fā)生鎖沖突的概率最低,并發(fā)度也最高。
3.頁面鎖:開銷和加鎖時間界于表鎖和行鎖之間;會出現(xiàn)死鎖;鎖定粒度界于表鎖和行鎖之間,并發(fā)度一般
從上述特點可見,很難籠統(tǒng)地說哪種鎖更好,只能就具體應(yīng)用的特點來說哪種鎖更合適!
僅從鎖的角度 來說:表級鎖更適合于以查詢?yōu)橹?,只有少量按索引條件更新數(shù)據(jù)的應(yīng)用,如 Web 應(yīng)用;而行級鎖則更適合于有大量按索引條件并發(fā)更新少量不同數(shù)據(jù),同時又有 并發(fā)查詢的應(yīng)用,如一些在線事務(wù)處理(OLTP)系統(tǒng)。
MyISAM 表鎖
MySQL 的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨占寫鎖(Table Write Lock)。
對 MyISAM 表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求; 對 MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作; MyISAM 表的讀操作與寫操作之間,以及寫操作之間是串行的!根據(jù)如表 20-2 所示的 例子可以知道,當一個線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。
MyISAM 存儲引擎的寫鎖阻塞讀例子:
當一個線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。

MyISAM 存儲引擎的讀鎖阻塞寫例子:
一個 session 使用 LOCK TABLE 命令給表 film_text 加了讀鎖,這個 session 可以查詢鎖定表中的記錄,但更新或訪問其他表都會提示錯誤;同時,另外一個 session 可以查詢表中的記錄,但更新就會出現(xiàn)鎖等待。
如何加表鎖
MyISAM 在執(zhí)行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖。
在執(zhí)行更新操作 (UPDATE、DELETE、INSERT 等)前,會自動給涉及的表加寫鎖,這個過程并不需要用戶干預(yù),因此,用戶一般不需要直接用 LOCK TABLE 命令給 MyISAM 表顯式加鎖。
在示例中,顯式加鎖基本上都是為了演示而已,并非必須如此。給 MyISAM 表顯示加鎖,一般是為了在一定程度模擬事務(wù)操作,實現(xiàn)對某一時間點多個表的一致性讀取。例如, 有一個訂單表 orders,其中記錄有各訂單的總金額 total,同時還有一個訂單明細表 order_detail,其中記錄有各訂單每一產(chǎn)品的金額小計 subtotal,假設(shè)我們需要檢查這兩個表的金額合計是否相符,可能就需要執(zhí)行如下兩條 SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
這時,如果不先給兩個表加鎖,就可能產(chǎn)生錯誤的結(jié)果,因為第一條語句執(zhí)行過程中,order_detail 表可能已經(jīng)發(fā)生了改變。因此,正確的方法應(yīng)該是:
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
要特別說明以下兩點內(nèi)容:
上面的例子在 LOCK TABLES 時加了“l(fā)ocal”選項,其作用就是在滿足 MyISAM 表并發(fā)插入條件的情況下,允許其他用戶在表尾并發(fā)插入記錄,有關(guān) MyISAM 表的并發(fā)插入問題,在后面還會進一步介紹。
在用 LOCK TABLES 給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,并且 MySQL 不支持鎖升級。也就是說,在執(zhí)行 LOCK TABLES 后,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那么只能執(zhí)行查詢操作,而不能執(zhí)行更新操作。其實,在自動加鎖的 情況下也基本如此,MyISAM 總是一次獲得 SQL 語句所需要的全部鎖。這也正是 MyISAM 表不會出現(xiàn)死鎖(Deadlock Free)的原因。
當使用 LOCK TABLES 時,不僅需要一次鎖定用到的所有表,而且,同一個表在 SQL 語句中出現(xiàn)多少次,就要通過與 SQL 語句中相同的別名鎖定多少次,否則也會出錯!舉例說明如下。
對 actor 表獲得讀鎖:
mysql> lock table actor read;
Query OK, 0 rows affected (0.00 sec)
但是通過別名訪問會提示錯誤:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name
from actor a,actor b
where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom'
and a.last_name <> b.last_name;
需要對別名分別鎖定:
mysql> lock table actor as a read,actor as b read;
Query OK, 0 rows affected (0.00 sec)
按照別名的查詢可以正確執(zhí)行:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name
from actor a,actor b where a.first_name = b.first_name
and a.first_name = 'Lisa' and a.last_name = 'Tom'
and a.last_name <> b.last_name;
+————+———–+————+———–+
| first_name | last_name | first_name | last_name |
+————+———–+————+———–+
| Lisa | Tom | LISA | MONROE |
+————+———–+————+———–+
1 row in set (0.00 sec)
查詢表級鎖爭用情況
可以通過檢查 table_locks_waited 和 table_locks_immediate 狀態(tài)變量來分析系統(tǒng)上的表鎖定爭奪:
mysql> show status like 'table%';
Variable_name | Value
Table_locks_immediate | 2979
Table_locks_waited | 0
2 rows in set (0.00 sec))
如果 Table_locks_waited 的值比較高,則說明存在著較嚴重的表級鎖爭用情況。
并發(fā)插入(Concurrent Inserts)
上文提到過 MyISAM 表的讀和寫是串行的,但這是就總體而言的。在一定條件下,MyISAM 表也支持查詢和插入操作的并發(fā)進行。
MyISAM 存儲引擎有一個系統(tǒng)變量 concurrent_insert,專門用以控制其并發(fā)插入的行為,其值分別可以為 0、1 或 2。
當 concurrent_insert 設(shè)置為 0 時,不允許并發(fā)插入。
當 concurrent_insert 設(shè)置為 1 時,如果 MyISAM 表中沒有空洞(即表的中間沒有被刪除的行),MyISAM 允許在一個進程讀表的同時,另一個進程從表尾插入記錄。這也是 MySQL 的默認設(shè)置。
當 concurrent_insert 設(shè)置為 2 時,無論 MyISAM 表中有沒有空洞,都允許在表尾并發(fā)插入記錄。
在下面的例子中:
session_1 獲得了一個表的 READ LOCAL 鎖,該線程可以對表進行查詢操作,但不能對表進行更新操作;
其他的線程(session_2),雖然不能對表進行刪除和更新操作,但卻可以對該表進行并發(fā)插入操作,這里假設(shè)該表中間不存在空洞。
MyISAM 存儲引擎的讀寫(INSERT)并發(fā)例子:
可以利用 MyISAM 存儲引擎的并發(fā)插入特性,來解決應(yīng) 用中對同一表查詢和插入的鎖爭用。例如,將 concurrent_insert 系統(tǒng)變量設(shè)為 2,總是允許并發(fā)插入;同時,通過定期在系統(tǒng)空閑時段執(zhí)行 OPTIMIZE TABLE 語句來整理空間碎片,收回因刪除記錄而產(chǎn)生的中間空洞。
MyISAM 的鎖調(diào)度
前面講過,MyISAM 存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。
那么,一個進程請求某個 MyISAM 表的讀鎖,同時另一個進程也請求同一表的寫鎖,MySQL 如何處理呢?
答案是寫進程先獲得鎖。
不僅如此,即使讀請求先到鎖等待隊列,寫請求后 到,寫鎖也會插到讀鎖請求之前!
這是因為 MySQL 認為寫請求一般比讀請求要重要。這也正是 MyISAM 表不太適合于有大量更新操作和查詢操作應(yīng)用的原因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。
這種情況有時可能會變得非常糟糕!幸好我們可以通過一些設(shè)置來調(diào)節(jié) MyISAM 的調(diào)度行為。
通過指定啟動參數(shù) low-priority-updates,使 MyISAM 引擎默認給予讀請求以優(yōu)先的權(quán)利。
通過執(zhí)行命令 SET LOW_PRIORITY_UPDATES=1,使該連接發(fā)出的更新請求優(yōu)先級降低。
通過指定 INSERT、UPDATE、DELETE 語句的 LOW_PRIORITY 屬性,降低該語句的優(yōu)先級。
雖然上面 3 種方法都是要么更新優(yōu)先,要么查詢優(yōu)先的方法,但還是可以用其來解決查詢相對重要的應(yīng)用(如用戶登錄系統(tǒng))中,讀鎖等待嚴重的問題。
另外,MySQL 也提供了一種折中的辦法來調(diào)節(jié)讀寫沖突,即給系統(tǒng)參數(shù) max_write_lock_count 設(shè)置一個合適的值,當一個表的讀鎖達到這個值后,MySQL 就暫時將寫請求的優(yōu)先級降低,給讀進程一定獲得鎖的機會。
上面已經(jīng)討論了寫優(yōu)先調(diào)度機制帶來的問題和解決辦法。這 里還要強調(diào)一點:一些需要長時間運行的查詢操作,也會使寫進程“餓死”!因此,應(yīng)用中應(yīng)盡量避免出現(xiàn)長時間運行的查詢操作,不要總想用一條 SELECT 語 句來解決問題,因為這種看似巧妙的 SQL 語句,往往比較復(fù)雜,執(zhí)行時間較長,在可能的情況下可以通過使用中間表等措施對 SQL 語句做一定的“分解”,使每 一步查詢都能在較短時間完成,從而減少鎖沖突。如果復(fù)雜查詢不可避免,應(yīng)盡量安排在數(shù)據(jù)庫空閑時段執(zhí)行,比如一些定期統(tǒng)計可以安排在夜間執(zhí)行。
InnoDB 鎖
InnoDB 與 MyISAM 的最大不同有兩點:一是支持事務(wù)(TRANSACTION);二是采用了行級鎖。行級鎖與表級鎖本來就有許多不同之處,另外,事務(wù)的引入也帶來了一些新問題。
事務(wù)(Transaction)及其 ACID 屬性事務(wù)是由一組 SQL 語句組成的邏輯處理單元,事務(wù)具有 4 屬性,通常稱為事務(wù)的 ACID 屬性。
原子性(Actomicity):事務(wù)是一個原子操作單元,其對數(shù)據(jù)的修改,要么全都執(zhí)行,要么全都不執(zhí)行。
一致性(Consistent):在事務(wù)開始和完成時,數(shù)據(jù)都必須保持一致狀態(tài)。這意味著所有相關(guān)的數(shù)據(jù)規(guī)則都必須應(yīng)用于事務(wù)的修改,以操持完整性;事務(wù)結(jié)束時,所有的內(nèi)部數(shù)據(jù)結(jié)構(gòu)(如 B 樹索引或雙向鏈表)也都必須是正確的。
隔離性(Isolation):數(shù)據(jù)庫系統(tǒng)提供一定的隔離機制,保證事務(wù)在不受外部并發(fā)操作影響的“獨立”環(huán)境執(zhí)行。這意味著事務(wù)處理過程中的中間狀態(tài)對外部是不可見的,反之亦然。
持久性(Durable):事務(wù)完成之后,它對于數(shù)據(jù)的修改是永久性的,即使出現(xiàn)系統(tǒng)故障也能夠保持。
并發(fā)事務(wù)帶來的問題
相對于串行處理來說,并發(fā)事務(wù)處理能大大增加數(shù)據(jù)庫資源的利用率,提高數(shù)據(jù)庫系統(tǒng)的事務(wù)吞吐量,從而可以支持可以支持更多的用戶。但并發(fā)事務(wù)處理也會帶來一些問題,主要包括以下幾種情況。MySQL事務(wù)
更新丟失(Lost Update):當兩個或多個事務(wù)選擇同一行,然后基于最初選定的值更新該行時,由于每個事務(wù)都不知道其他事務(wù)的存在,就會發(fā)生丟失更新問題——最后的更新覆蓋了其他事務(wù)所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。最后保存其更改保存其更改副本的編輯人員覆蓋另一個編輯人員所做的修改。如果在一個編輯人員完成并提交事務(wù)之前,另一個編輯人員不能訪問同一文件,則可避免此問題。
臟讀(Dirty Reads):一個事務(wù)正在對一條記錄做修改,在這個事務(wù)并提交前,這條記錄的數(shù)據(jù)就處于不一致狀態(tài);這時,另一個事務(wù)也來讀取同一條記錄,如果不加控制,第二個事務(wù)讀取了這些“臟”的數(shù)據(jù),并據(jù)此做進一步的處理,就會產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系。這種現(xiàn)象被形象地叫做“臟讀”。
不可重復(fù)讀(Non-Repeatable Reads):一個事務(wù)在讀取某些數(shù)據(jù)已經(jīng)發(fā)生了改變、或某些記錄已經(jīng)被刪除了!這種現(xiàn)象叫做“不可重復(fù)讀”。
幻讀(Phantom Reads):一個事務(wù)按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù),卻發(fā)現(xiàn)其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù),這種現(xiàn)象就稱為“幻讀”。
事務(wù)隔離級別
在并發(fā)事務(wù)處理帶來的問題中,“更新丟失”通常應(yīng)該是完全避免的。但防止更新丟失,并不能單靠數(shù)據(jù)庫事務(wù)控制器來解決,需要應(yīng)用程序?qū)σ碌臄?shù)據(jù)加必要的鎖來解決,因此,防止更新丟失應(yīng)該是應(yīng)用的責任。
“臟讀”、“不可重復(fù)讀”和“幻讀”,其實都是數(shù)據(jù)庫讀一致性問題,必須由數(shù)據(jù)庫提供一定的事務(wù)隔離機制來解決。數(shù)據(jù)庫實現(xiàn)事務(wù)隔離的方式,基本可以分為以下兩種。
一種是在讀取數(shù)據(jù)前,對其加鎖,阻止其他事務(wù)對數(shù)據(jù)進行修改。
另一種是不用加任何鎖,通過一定機制生成一個數(shù)據(jù)請求時間點的一致性數(shù)據(jù)快照(Snapshot),并用這個快照來提供一定級別(語句級或事務(wù)級)的一致性讀取。從用戶的角度,好像是數(shù)據(jù)庫可以提供同一數(shù)據(jù)的多個版本,因此,這種技術(shù)叫做數(shù)據(jù)多版本并發(fā)控制(M ultiVersion Concurrency Control,簡稱 MVCC 或 MCC),也經(jīng)常稱為多版本數(shù)據(jù)庫。
在 MVCC 并發(fā)控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)??煺兆x,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,并且,當前讀返回的記錄,都會加上鎖,保證其他事務(wù)不會再并發(fā)修改這條記錄。在一個支持 MVCC 并發(fā)控制的系統(tǒng)中,哪些讀操作是快照讀?哪些操作又是當前讀呢?以 MySQL InnoDB 為例:
快照讀:簡單的 select 操作,屬于快照讀,不加鎖。(當然,也有例外)
select * from table where ?;
當前讀:特殊的讀操作,插入/更新/刪除操作,屬于當前讀,需要加鎖。
下面語句都屬于當前讀,讀取記錄的最新版本。并且,讀取之后,還需要保證其他并發(fā)事務(wù)不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加 S 鎖 (共享鎖)外,其他的操作,都加的是 X 鎖 (排它鎖)。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
數(shù)據(jù)庫的事務(wù)隔離越嚴格,并發(fā)副作用越小,但付出的代價也就越大,因為事務(wù)隔離實質(zhì)上就是使事務(wù)在一定程度上 “串行化”進行,這顯然與“并發(fā)”是矛盾的。同時,不同的應(yīng)用對讀一致性和事務(wù)隔離程度的要求也是不同的,比如許多應(yīng)用對“不可重復(fù)讀”和“幻讀”并不敏 感,可能更關(guān)心數(shù)據(jù)并發(fā)訪問的能力。
為了解決“隔離”與“并發(fā)”的矛盾,ISO/ANSI SQL92 定義了 4 個事務(wù)隔離級別,每個級別的隔離程度不同,允許出現(xiàn)的副作用也不同,應(yīng)用可以根據(jù)自己的業(yè)務(wù)邏輯要求,通過選擇不同的隔離級別來平衡 “隔離”與“并發(fā)”的矛盾。下表很好地概括了這 4 個隔離級別的特性。
InonoD行鎖爭用
mysql> show status like 'innodb_row_lock%';
如果發(fā)現(xiàn)鎖爭用比較嚴重,如 InnoDB_row_lock_waits 和 InnoDB_row_lock_time_avg 的值比較高,還可以通過設(shè)置 InnoDB Monitors 來進一步觀察發(fā)生鎖沖突的表、數(shù)據(jù)行等,并分析鎖爭用的原因。
InnoDB 的行鎖模式及加鎖方法

InnoDB 實現(xiàn)了以下兩種類型的行鎖。
1. 共享鎖(s):又稱讀鎖。
允許一個事務(wù)去讀一行,阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖。若事務(wù) T 對數(shù)據(jù)對象 A 加上 S 鎖,則事務(wù) T 可以讀 A 但不能修改 A,其他事務(wù)只能再對 A 加 S 鎖,而不能加 X 鎖,直到 T 釋放 A 上的 S 鎖。這保證了其他事務(wù)可以讀 A,但在 T 釋放 A 上的 S 鎖之前不能對 A 做任何修改。
2. 排他鎖(X):又稱寫鎖。
允許獲取排他鎖的事務(wù)更新數(shù)據(jù),阻止其他事務(wù)取得相同的數(shù)據(jù)集共享讀鎖和排他寫鎖。若事務(wù) T 對數(shù)據(jù)對象 A 加上 X 鎖,事務(wù) T 可以讀 A 也可以修改 A,其他事務(wù)不能再對 A 加任何鎖,直到 T 釋放 A 上的鎖。
對于共享鎖大家可能很好理解,就是多個事務(wù)只能讀數(shù)據(jù)不能改數(shù)據(jù)。
對于排他鎖大家的理解可能就有些差別,我當初就犯了一個錯誤,以為排他鎖鎖住一行數(shù)據(jù)后,其他事務(wù)就不能讀取和修改該行數(shù)據(jù),其實不是這樣的。排他鎖指的是一個事務(wù)在一行數(shù)據(jù)加上排他鎖后,其他事務(wù)不能再在其上加其他的鎖。
mysql InnoDB 引擎默認的修改數(shù)據(jù)語句:update,delete,insert 都會自動給涉及到的數(shù)據(jù)加上排他。
select 語句默認不會加任何鎖類型,如果加排他鎖可以使用 select …for update 語句,加共享鎖可以使用 select … lock in share mode 語句。
所以加過排他鎖的數(shù)據(jù)行在其他事務(wù)種是不能修改數(shù)據(jù)的,也不能通過 for update 和 lock in share mode 鎖的方式查詢數(shù)據(jù),但可以直接通過 select …from…查詢數(shù)據(jù),因為普通查詢沒有任何鎖機制。
另外,為了允許行鎖和表鎖共存,實現(xiàn)多粒度鎖機制,InnoDB 還有兩種內(nèi)部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
3. 意向鎖
意向鎖實際是一種表級鎖。防止在一個事務(wù)執(zhí)行過程中,執(zhí)行DDL操作導(dǎo)致的數(shù)據(jù)不一致(這里和DML鎖有點類似)。
意向鎖分為兩種鎖類型:
意向共享鎖(IS):事務(wù)打算給數(shù)據(jù)行共享鎖,事務(wù)在給一個數(shù)據(jù)行加共享鎖前必須先取得該表的 IS 鎖。
意向排他鎖(IX):事務(wù)打算給數(shù)據(jù)行加排他鎖,事務(wù)在給一個數(shù)據(jù)行加排他鎖前必須先取得該表的 IX 鎖。
DML鎖
DML鎖用于保證表中的原數(shù)據(jù)一致。當開啟一個事務(wù)之后,其他的事務(wù)就不能對表做DDL語句操作。例如對表執(zhí)行alter語句。
InnoDB 行鎖模式兼容性列表:
如果一個事務(wù)請求的鎖模式與當前的鎖兼容,InnoDB 就請求的鎖授予該事務(wù);反之,如果兩者兩者不兼容,該事務(wù)就要等待鎖釋放。
意向鎖是 InnoDB 自動加的,不需用戶干預(yù)。對于 UPDATE、DELETE 和 INSERT 語句,InnoDB 會自動給涉及數(shù)據(jù)集加排他鎖(X);對于普通 SELECT 語句,InnoDB 不會加任何鎖。事務(wù)可以通過以下語句顯式給記錄集加共享鎖或排他鎖:
共享鎖(S):
mysql SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;
排他鎖(X):
SELECT * FROM table_name WHERE ... FOR UPDATE;
用SELECT ... IN SHARE MODE獲得共享鎖,主要用在需要數(shù)據(jù)依存關(guān)系時來確認某行記錄是否存在,并確保沒有人對這個記錄進行 UPDATE 或者 DELETE 操作。但是如果當前事務(wù)也需要對該記錄進行更新操作,則很有可能造成死鎖,對于鎖定行記錄后需要進行更新操作的應(yīng)用,應(yīng)該使用 SELECT… FOR UPDATE 方式獲得排他鎖。
InnoDB 行鎖實現(xiàn)方式
當MySQL的事務(wù)隔離級別為RR級別時(可重復(fù)讀),并且innodb_locks_unsafe_for_binlog=0時,鎖實現(xiàn)的方式有如下三種。
單行加鎖
InnoDB 行鎖是通過給索引上的索引項加鎖來實現(xiàn)的,這一點 MySQL 與 Oracle 不同,后者是通過在數(shù)據(jù)塊中對相應(yīng)數(shù)據(jù)行加鎖來實現(xiàn)的。InnoDB 這種行鎖實現(xiàn)特點意味著:只有通過索引條件檢索數(shù)據(jù),InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖!
在實際應(yīng)用中,要特別注意 InnoDB 行鎖的這一特性,不然的話,可能導(dǎo)致大量的鎖沖突,從而影響并發(fā)性能。下面通過一些實際例子來加以說明。
在不通過索引條件查詢的時候,InnoDB 確實使用的是表鎖,而不是行鎖。
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

在上面的例子中,看起來 session_1 只給一行加了排他鎖,但 session_2 在請求其他行的排他鎖時,卻出現(xiàn)了鎖等待!原因就是在沒有索引的情況下,InnoDB 只能使用表鎖。當我們給其增加一個索引后,InnoDB 就只鎖定了符合條件的行,如下例所示:創(chuàng)建 tab_with_index 表,id 字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;
mysql> alter table tab_with_index add index id(id);
2. 由于 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現(xiàn)鎖沖突的。應(yīng)用設(shè)計的時候要注意這一點。在下面的例子中,表 tab_with_index 的 id 字段有索引,name 字段沒有索引:
mysql> alter table tab_with_index drop index name;
Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0
Warnings: 0
mysql> insert into tab_with_index values(1,'4');
Query OK, 1 row affected (0.00 sec)
mysql> select * from tab_with_index where id = 1;

InnoDB 存儲引擎使用相同索引鍵的阻塞例子.
3. 當表有多個索引的時候,不同的事務(wù)可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數(shù)據(jù)加鎖。在下面的例子中,表 tab_with_index 的 id 字段有主鍵索引,name 字段有普通索引:
mysql> alter table tab_with_index add index name(name);
Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0
Warnings: 0
InnoDB 存儲引擎的表使用不同索引的阻塞例子
4. 即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計劃的代價來決 定的,如果 MySQL 認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖沖突 時,別忘了檢查 SQL 的執(zhí)行計劃,以確認是否真正使用了索引。
比如,在 tab_with_index 表里的 name 字段有索引,但是 name 字段是 varchar 類型的,檢索值的數(shù)據(jù)類型與索引字段不同,雖然 MySQL 能夠進行數(shù)據(jù)類型轉(zhuǎn)換,但卻不會使用索引,從而導(dǎo)致 InnoDB 使用表鎖。通過用 explain 檢查兩條 SQL 的執(zhí)行計劃,我們可以清楚地看到了這一點。
mysql> explain select * from tab_with_index where name = 1 \G
mysql> explain select * from tab_with_index where name = '1' \G
間隙鎖
當MySQL的事務(wù)隔離級別為可重復(fù)讀時,為了避免幻讀情況,采用了鎖定記錄范圍的數(shù)據(jù)行。這種鎖就是間隙鎖(Gap lock)。舉例來說,假如 emp 表中只有 101 條記錄,其 empid 的值分別是 1,2,…,100,101,下面的 SQL:
Select * from emp where empid > 100 for update;
是一個范圍條件的檢索,InnoDB 不僅會對符合條件的 empid 值為 101 的記錄加鎖,也會對 empid 大于 101(這些記錄并不存在)的“間隙”加鎖。例如插入一條新數(shù)據(jù),此時就會發(fā)生鎖等待。
InnoDB 使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關(guān)隔離級別的要求,對于上面的例子,要是不使 用間隙鎖,如果其他事務(wù)插入了 empid 大于 100 的任何記錄,那么本事務(wù)如果再次執(zhí)行上述語句,就會發(fā)生幻讀;另外一方面,是為了滿足其恢復(fù)和復(fù)制的需 要。有關(guān)其恢復(fù)和復(fù)制對鎖機制的影響,以及不同隔離級別下 InnoDB 使用間隙鎖的情況,在后續(xù)的章節(jié)中會做進一步介紹。
很顯然,在使用范圍條件檢索并鎖定記錄時,InnoDB 這種加鎖機制會阻塞符合條件范圍內(nèi)鍵值的并發(fā)插入,這往往會造成嚴重的鎖等待。因此,在實際應(yīng)用開發(fā)中,尤其是并發(fā)插入比較多的應(yīng)用,我們要盡量優(yōu)化業(yè)務(wù)邏輯,盡量使用相等條件來訪問更新數(shù)據(jù),避免使用范圍條件。
還要特別說明的是,InnoDB 除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB 也會使用間隙鎖!下面這個例子假設(shè) emp 表中只有 101 條記錄,其 empid 的值分別是 1,2,……,100,101。InnoDB 存儲引擎的間隙鎖阻塞例子
間隙鎖只適用于事務(wù)的可重復(fù)讀隔離級別。
Next-key Lock
Next-key Lock位于單行記錄鎖和間隙鎖之間。當一個InnoDB掃描索引是,不僅僅對選中的行進行加鎖,也會對索引兩邊的間隙加間隙鎖。
例如數(shù)據(jù)中存在id為1,2,3....,80的數(shù)據(jù),給數(shù)據(jù)添加一個排它鎖,會發(fā)現(xiàn)無法插入ID為81的新數(shù)據(jù)。
小結(jié)
本文重點介紹了 MySQL 中 MyISAM 表級鎖和 InnoDB 行級鎖的實現(xiàn)特點,并討論了兩種存儲引擎經(jīng)常遇到的鎖問題和解決辦法。
對于 MyISAM 的表鎖,主要討論了以下幾點:
共享讀鎖(S)之間是兼容的,但共享讀鎖(S)與排他寫鎖(X)之間,以及排他寫鎖(X)之間是互斥的,也就是說讀和寫是串行的。
在一定條件下,MyISAM 允許查詢和插入并發(fā)執(zhí)行,我們可以利用這一點來解決應(yīng)用中對同一表查詢和插入的鎖爭用問題。
MyISAM 默認的鎖調(diào)度機制是寫優(yōu)先,這并不一定適合所有應(yīng)用,用戶可以通過設(shè)置 LOW_PRIORITY_UPDATES 參數(shù),或在 INSERT、UPDATE、DELETE 語句中指定 LOW_PRIORITY 選項來調(diào)節(jié)讀寫鎖的爭用。
由于表鎖的鎖定粒度大,讀寫之間又是串行的,因此,如果更新操作較多,MyISAM 表可能會出現(xiàn)嚴重的鎖等待,可以考慮采用 InnoDB 表來減少鎖沖突。
對于 InnoDB 表,本文主要討論了以下幾項內(nèi)容:
InnoDB 的行鎖是基于索引實現(xiàn)的,如果不通過索引訪問數(shù)據(jù),InnoDB 會使用表鎖。 介紹了 InnoDB 間隙鎖(Next-key)機制,以及 InnoDB 使用間隙鎖的原因。
在不同的隔離級別下,InnoDB 的鎖機制和一致性讀策略不同。
在了解 InnoDB 鎖特性后,用戶可以通過設(shè)計和 SQL 調(diào)整等措施減少鎖沖突和死鎖,包括:
盡量使用較低的隔離級別;精心設(shè)計索引,并盡量使用索引訪問數(shù)據(jù),使加鎖更精確,從而減少鎖沖突的機會;
選擇合理的事務(wù)大小,小事務(wù)發(fā)生鎖沖突的幾率也更小;
給記錄集顯式加鎖時,最好一次性請求足夠級別的鎖。比如要修改數(shù)據(jù)的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產(chǎn)生死鎖;
不同的程序訪問一組表時,應(yīng)盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;
盡量用相等條件訪問數(shù)據(jù),這樣可以避免間隙鎖對并發(fā)插入的影響;不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;
對于一些特定的事務(wù),可以使用表鎖來提高處理速度或減少死鎖的可能。
