天天看點

Mysql中的鎖機制

​鎖是計算機協調多個程序或線程并發通路某一資源的機制。​在資料庫中,除傳統的 計算資源(如CPU、RAM、I/O等)的争用以外,資料也是一種供許多使用者共享的資源。如何保證資料并發通路的一緻性、有效性是所有資料庫必須解決的一 個問題,鎖沖突也是影響資料庫并發通路性能的一個重要因素。從這個角度來說,鎖對資料庫而言顯得尤其重要,也更加複雜。本章我們着重讨論MySQL鎖機制 的特點,常見的鎖問題,以及解決MySQL鎖問題的一些方法或建議。 

Mysql用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。這些鎖統稱為悲觀鎖(Pessimistic Lock)。

MySQL鎖概述

相對其他資料庫而言,MySQL的鎖機制比較簡單,其最 顯著的特點是不同的存儲引擎支援不同的鎖機制。比如,MyISAM和MEMORY存儲引擎采用的是表級鎖(table-level locking);BDB存儲引擎采用的是頁面鎖(page-level locking),但也支援表級鎖;InnoDB存儲引擎既支援行級鎖(row-level locking),也支援表級鎖,但預設情況下是采用行級鎖。 

​表級鎖:​開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的機率最高,并發度最低。 

​行級鎖:​開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的機率最低,并發度也最高。 

​頁面鎖:​開銷和加鎖時間界于表鎖和行鎖之間;會出現死鎖;鎖定粒度界于表鎖和行鎖之間,并發度一般 

從上述特點可見,很難籠統地說哪種鎖更好,隻能就具體應用的特點來說哪種鎖更合适!僅從鎖的角度 來說:表級鎖更适合于以查詢為主,隻有少量按索引條件更新資料的應用,如Web應用;而行級鎖則更适合于有大量按索引條件并發更新少量不同資料,同時又有 并發查詢的應用,如一些線上事務處理(OLTP)系統。

MyISAM表鎖

MySQL的表級鎖有兩種模式:​表共享讀鎖(Table Read Lock)​和​表獨占寫鎖(Table Write Lock)​。 

對MyISAM表的讀操作,不會阻塞其他使用者對同一表的讀請求,但會阻塞對同一表的寫請求;對 MyISAM表的寫操作,則會阻塞其他使用者對同一表的讀和寫操作;MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的!根據如表20-2所示的 例子可以知道,當一個線程獲得對一個表的寫鎖後,隻有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。

​MyISAM存儲引擎的寫鎖阻塞讀例子:​ 

當一個線程獲得對一個表的寫鎖後,隻有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。 

Mysql中的鎖機制

​MyISAM存儲引擎的讀鎖阻塞寫例子:​ 

一個session使用LOCK TABLE指令給表film_text加了讀鎖,這個session可以查詢鎖定表中的記錄,但更新或通路其他表都會提示錯誤;同時,另外一個session可以查詢表中的記錄,但更新就會出現鎖等待。 

Mysql中的鎖機制

如何加表鎖

MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作 (UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程并不需要使用者幹預,是以,使用者一般不需要直接用LOCK TABLE指令給MyISAM表顯式加鎖。在示例中,顯式加鎖基本上都是為了示範而已,并非必須如此。 

給MyISAM表顯示加鎖,一般是為了在一定程度模拟事務操作,實作對某一時間點多個表的一緻性讀取。例如, 有一個訂單表orders,其中記錄有各訂單的總金額total,同時還有一個訂單明細表order_detail,其中記錄有各訂單每一産品的金額小計 subtotal,假設我們需要檢查這兩個表的金額合計是否相符,可能就需要執行如下兩條SQL:

Select sum(total) from orders;

Select sum(subtotal) from order_detail;

這時,如果不先給兩個表加鎖,就可能産生錯誤的結果,因為第一條語句執行過程中,order_detail表可能已經發生了改變。是以,正确的方法應該是:

Lock tables orders read local, order_detail read local;

Select sum(total) from orders;

Select sum(subtotal) from order_detail;

Unlock tables;

要特别說明以下兩點内容: 

1、上面的例子在LOCK TABLES時加了​“local”​選項,其作用就是在滿足MyISAM表并發插入條件的情況下,允許其他使用者在表尾并發插入記錄,有關MyISAM表的并發插入問題,在後面還會進一步介紹。 

2、在用LOCK TABLES給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,并且MySQL不支援鎖更新。也就是說,在執行LOCK TABLES後,隻能通路顯式加鎖的這些表,不能通路未加鎖的表;同時,如果加的是讀鎖,那麼隻能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的 情況下也基本如此,MyISAM總是一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因。

當使用LOCK TABLES時,不僅需要一次鎖定用到的所有表,而且,同一個表在SQL語句中出現多少次,就要通過與SQL語句中相同的别名鎖定多少次,否則也會出錯!舉例說明如下。 

(1)對actor表獲得讀鎖:

mysql> lock table actor read; 

Query OK, 0 rows affected (0.00 sec)

(2)但是通過别名通路會提示錯誤:

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;

ERROR 1100 (HY000): Table ‘a’ was not locked with LOCK TABLES

(3)需要對别名分别鎖定:

mysql> lock table actor as a read,actor as b read;

  • 1

Query OK, 0 rows affected (0.00 sec)

(4)按照别名的查詢可以正确執行:

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狀态變量來分析系統上的表鎖定争奪:

mysql> show status like 'table%';

  • 1

​Variable_name | Value​ 

​Table_locks_immediate | 2979​ 

​Table_locks_waited | 0​ 

2 rows in set (0.00 sec))

如果Table_locks_waited的值比較高,則說明存在着較嚴重的表級鎖争用情況。

并發插入(Concurrent Inserts)

上文提到過MyISAM表的讀和寫是串行的,但這是就總體而言的。在一定條件下,MyISAM表也支援查詢和插入操作的并發進行。 

MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其并發插入的行為,其值分别可以為0、1或2。

  • 當concurrent_insert設定為0時,不允許并發插入。
  • 當concurrent_insert設定為1時,如果MyISAM表中沒有空洞(即表的中間沒有被删除的行),MyISAM允許在一個程序讀表的同時,另一個程序從表尾插入記錄。這也是MySQL的預設設定。
  • 當concurrent_insert設定為2時,無論MyISAM表中有沒有空洞,都允許在表尾并發插入記錄。

在下面的例子中,session_1獲得了一個表的READ LOCAL鎖,該線程可以對表進行查詢操作,但不能對表進行更新操作;其他的線程(session_2),雖然不能對表進行删除和更新操作,但卻可以對該表進行并發插入操作,這裡假設該表中間不存在空洞。

​MyISAM存儲引擎的讀寫(INSERT)并發例子:​ 

Mysql中的鎖機制

可以利用MyISAM存儲引擎的并發插入特性,來解決應 用中對同一表查詢和插入的鎖争用。例如,将concurrent_insert系統變量設為2,總是允許并發插入;同時,通過定期在系統空閑時段執行 OPTIMIZE TABLE語句來整理空間碎片,收回因删除記錄而産生的中間空洞。

MyISAM的鎖排程

前面講過,MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。那麼,一個程序請求某個 MyISAM表的讀鎖,同時另一個程序也請求同一表的寫鎖,MySQL如何處理呢?答案是寫程序先獲得鎖。不僅如此,即使讀請求先到鎖等待隊列,寫請求後 到,寫鎖也會插到讀鎖請求之前!這是因為MySQL認為寫請求一般比讀請求要重要。這也正是MyISAM表不太适合于有大量更新操作和查詢操作應用的原 因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,進而可能永遠阻塞。這種情況有時可能會變得非常糟糕!幸好我們可以通過一些設定來調節MyISAM 的排程行為。

  • 通過指定啟動參數low-priority-updates,使MyISAM引擎預設給予讀請求以優先的權利。
  • 通過執行指令SET LOW_PRIORITY_UPDATES=1,使該連接配接發出的更新請求優先級降低。
  • 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。

雖然上面3種方法都是要麼更新優先,要麼查詢優先的方法,但還是可以用其來解決查詢相對重要的應用(如使用者登入系統)中,讀鎖等待嚴重的問題。 

另外,MySQL也提供了一種折中的辦法來調節讀寫沖突,即給系統參數max_write_lock_count設定一個合适的值,當一個表的讀鎖達到這個值後,MySQL就暫時将寫請求的優先級降低,給讀程序一定獲得鎖的機會。

上面已經讨論了寫優先排程機制帶來的問題和解決辦法。這 裡還要強調一點:一些需要長時間運作的查詢操作,也會使寫程序“餓死”!是以,應用中應盡量避免出現長時間運作的查詢操作,不要總想用一條SELECT語 句來解決問題,因為這種看似巧妙的SQL語句,往往比較複雜,執行時間較長,在可能的情況下可以通過使用中間表等措施對SQL語句做一定的“分解”,使每 一步查詢都能在較短時間完成,進而減少鎖沖突。如果複雜查詢不可避免,應盡量安排在資料庫空閑時段執行,比如一些定期統計可以安排在夜間執行。

InnoDB鎖

InnoDB與MyISAM的最大不同有兩點:一是支援事務(TRANSACTION);二是采用了行級鎖。行級鎖與表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。

​1、事務(Transaction)及其ACID屬性​ 

事務是由一組SQL語句組成的邏輯處理單元,事務具有4屬性,通常稱為事務的ACID屬性。

  • 原子性(Actomicity):事務是一個原子操作單元,其對資料的修改,要麼全都執行,要麼全都不執行。
  • 一緻性(Consistent):在事務開始和完成時,資料都必須保持一緻狀态。這意味着所有相關的資料規則都必須應用于事務的修改,以操持完整性;事務結束時,所有的内部資料結構(如B樹索引或雙向連結清單)也都必須是正确的。
  • 隔離性(Isolation):資料庫系統提供一定的隔離機制,保證事務在不受外部并發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀态對外部是不可見的,反之亦然。
  • 持久性(Durable):事務完成之後,它對于資料的修改是永久性的,即使出現系統故障也能夠保持。

​2、并發事務帶來的問題​ 

相對于串行處理來說,并發事務處理能大大增加資料庫資源的使用率,提高資料庫系統的事務吞吐量,進而可以支援可以支援更多的使用者。但并發事務處理也會帶來一些問題,主要包括以下幾種情況。

  • 更新丢失(Lost Update):當兩個或多個事務選擇同一行,然後基于最初標明的值更新該行時,由于每個事務都不知道其他事務的存在,就會發生丢失更新問題——最後的更新覆寫了其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然後儲存更改後的副本,這樣就覆寫了原始文檔。最後儲存其更改儲存其更改副本的編輯人員覆寫另一個編輯人員所做的修改。如果在一個編輯人員完成并送出事務之前,另一個編輯人員不能通路同一檔案,則可避免此問題。
  • 髒讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務并送出前,這條記錄的資料就處于不一緻狀态;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“髒”的資料,并據此做進一步的處理,就會産生未送出的資料依賴關系。這種現象被形象地叫做“髒讀”。
  • 不可重複讀(Non-Repeatable Reads):一個事務在讀取某些資料已經發生了改變、或某些記錄已經被删除了!這種現象叫做“不可重複讀”。
  • 幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的資料,卻發現其他事務插入了滿足其查詢條件的新資料,這種現象就稱為“幻讀”。

​3、事務隔離級别​ 

在并發事務處理帶來的問題中,“更新丢失”通常應該是完全避免的。但防止更新丢失,并不能單靠資料庫事務控制器來解決,需要應用程式對要更新的資料加必要的鎖來解決,是以,防止更新丢失應該是應用的責任。

“髒讀”、“不可重複讀”和“幻讀”,其實都是資料庫讀一緻性問題,必須由資料庫提供一定的事務隔離機制來解決。資料庫實作事務隔離的方式,基本可以分為以下兩種。

  • 一種是在讀取資料前,對其加鎖,阻止其他事務對資料進行修改。
  • 另一種是不用加任何鎖,通過一定機制生成一個資料請求時間點的一緻性資料快照(Snapshot),并用這個快照來提供一定級别(語句級或事務級)的一緻性讀取。從使用者的角度,好像是資料庫可以提供同一資料的多個版本,是以,這種技術叫做資料多版本并發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也經常稱為多版本資料庫。

在MVCC并發控制中,讀操作可以分成兩類:快照讀 (snapshot read)與目前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是曆史版本),不用加鎖。目前讀,讀取的是記錄的最新版本,并且,目前讀傳回的記錄,都會加上鎖,保證其他事務不會再并發修改這條記錄。 

在一個支援MVCC并發控制的系統中,哪些讀操作是快照讀?哪些操作又是目前讀呢?以MySQL InnoDB為例:

  • 快照讀:簡單的select操作,屬于快照讀,不加鎖。(當然,也有例外)

select * from table where ?;

  • 1
  • 目前讀:特殊的讀操作,插入/更新/删除操作,屬于目前讀,需要加鎖。

    下面語句都屬于目前讀,讀取記錄的最新版本。并且,讀取之後,還需要保證其他并發事務不能修改目前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加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 ?;

資料庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上 “串行化”進行,這顯然與“并發”是沖突的。同時,不同的應用對讀一緻性和事務隔離程度的要求也是不同的,比如許多應用對“不可重複讀”和“幻讀”并不敏 感,可能更關心資料并發通路的能力。

為了解決“隔離”與“并發”的沖突,ISO/ANSI SQL92定義了4個事務隔離級别,每個級别的隔離程度不同,允許出現的副作用也不同,應用可以根據自己的業務邏輯要求,通過選擇不同的隔離級别來平衡 “隔離”與“并發”的沖突。下表很好地概括了這4個隔離級别的特性。 

Mysql中的鎖機制

擷取InonoD行鎖争用情況

可以通過檢查InnoDB_row_lock狀态變量來分析系統上的行鎖的争奪情況:

mysql> show status like 'innodb_row_lock%';

  • 1
Mysql中的鎖機制

如果發現鎖争用比較嚴重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比較高,還可以通過設定InnoDB Monitors來進一步觀察發生鎖沖突的表、資料行等,并分析鎖争用的原因。

InnoDB的行鎖模式及加鎖方法

InnoDB實作了以下兩種類型的行鎖。

  • ​共享鎖(s):又稱讀鎖。​允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖。若事務T對資料對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務隻能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
  • ​排他鎖(X):又稱寫鎖。​允許擷取排他鎖的事務更新資料,阻止其他事務取得相同的資料集共享讀鎖和排他寫鎖。若事務T對資料對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。
  • 對于共享鎖大家可能很好了解,就是多個事務隻能讀資料不能改資料。 

    對于排他鎖大家的了解可能就有些差别,我當初就犯了一個錯誤,以為排他鎖鎖住一行資料後,其他事務就不能讀取和修改該行資料,其實不是這樣的。排他鎖指的是一個事務在一行資料加上排他鎖後,其他事務不能再在其上加其他的鎖。mysql InnoDB引擎預設的修改資料語句:​update,delete,insert都會自動給涉及到的資料加上排他鎖,select語句預設不會加任何鎖類型​,如果加排他鎖可以使用select …for update語句,加共享鎖可以使用select … lock in share mode語句。​是以加過排他鎖的資料行在其他事務種是不能修改資料的,也不能通過for update和lock in share mode鎖的方式查詢資料,但可以直接通過select …from…查詢資料,因為普通查詢沒有任何鎖機制。​

另外,為了允許行鎖和表鎖共存,實作多粒度鎖機制,InnoDB還有兩種内部使用的​意向鎖(Intention Locks)​,這兩種意向鎖都是表鎖。

  • 意向共享鎖(IS):事務打算給資料行共享鎖,事務在給一個資料行加共享鎖前必須先取得該表的IS鎖。
  • 意向排他鎖(IX):事務打算給資料行加排他鎖,事務在給一個資料行加排他鎖前必須先取得該表的IX鎖。

​InnoDB行鎖模式相容性清單:​ 

Mysql中的鎖機制

如果一個事務請求的鎖模式與目前的鎖相容,InnoDB就請求的鎖授予該事務;反之,如果兩者兩者不相容,該事務就要等待鎖釋放。 

意向鎖是InnoDB自動加的,不需使用者幹預。對于UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及資料集加排他鎖(X);對于普通SELECT語句,InnoDB不會加任何鎖。 

事務可以通過以下語句顯式給記錄集加共享鎖或排他鎖:

  • 共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
  • 排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。

用SELECT ... IN SHARE MODE獲得共享鎖,主要用在需要資料依存關系時來确認某行記錄是否存在,并確定沒有人對這個記錄進行UPDATE或者DELETE操作。​但是如果目前事務也需要對該記錄進行更新操作,則很有可能造成死鎖,對于鎖定行記錄後需要進行更新操作的應用,應該使用SELECT… FOR UPDATE方式獲得排他鎖。​

InnoDB行鎖實作方式

InnoDB行鎖是通過給索引上的索引項加鎖來實作的,這一點MySQL與Oracle不同,後者是通過在資料塊中對相應資料行加鎖來實作的。InnoDB這種行鎖實作特點意味着:隻有通過索引條件檢索資料,InnoDB才使用行級鎖,​否則,InnoDB将使用表鎖!​ 

在實際應用中,要特别注意InnoDB行鎖的這一特性,不然的話,可能導緻大量的鎖沖突,進而影響并發性能。下面通過一些實際例子來加以說明。

(1)在不通過索引條件查詢的時候,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

Mysql中的鎖機制

在上面的例子中,看起來session_1隻給一行加了排他鎖,但session_2在請求其他行的排他鎖時,卻出現了鎖等待!​原因就是在沒有索引的情況下,InnoDB隻能使用表鎖。​當我們給其增加一個索引後,InnoDB就隻鎖定了符合條件的行,如下例所示: 

建立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);

Mysql中的鎖機制

(2)由于MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,是以雖然是通路不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。應用設計的時候要注意這一點。 

在下面的例子中,表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;

Mysql中的鎖機制

InnoDB存儲引擎使用相同索引鍵的阻塞例子 

Mysql中的鎖機制

(3)當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對資料加鎖。 

在下面的例子中,表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存儲引擎的表使用不同索引的阻塞例子​ 

Mysql中的鎖機制

(4)即便在條件中使用了索引字段,但是否使用索引來檢索資料是由MySQL通過判斷不同執行計劃的代價來決 定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB将使用表鎖,而不是行鎖。是以,在分析鎖沖突 時,别忘了檢查SQL的執行計劃,以确認是否真正使用了索引。 

比如,在tab_with_index表裡的name字段有索引,但是name字段是varchar類型的,檢索值的資料類型與索引字段不同,雖然MySQL能夠進行資料類型轉換,但卻不會使用索引,進而導緻InnoDB使用表鎖。通過用explain檢查兩條SQL的執行計劃,我們可以清楚地看到了這一點。

mysql> explain select * from tab_with_index where name = 1 \G

mysql> explain select * from tab_with_index where name = '1' \G

間隙鎖(Next-Key鎖)

當我們用範圍條件而不是相等條件檢索資料,并請求共享或排他鎖時,InnoDB會給符合條件的已有資料記錄的 索引項加鎖;對于鍵值在條件範圍内但并不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖 (Next-Key鎖)。 

舉例來說,假如emp表中隻有101條記錄,其empid的值分别是 1,2,…,100,101,下面的SQL:

Select * from  emp where empid > 100 for update;

  • 1

是一個範圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大于101(這些記錄并不存在)的“間隙”加鎖。

InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級别的要求,對于上面的例子,要是不使 用間隙鎖,如果其他事務插入了empid大于100的任何記錄,那麼本事務如果再次執行上述語句,就會發生幻讀;另外一方面,是為了滿足其恢複和複制的需 要。有關其恢複和複制對鎖機制的影響,以及不同隔離級别下InnoDB使用間隙鎖的情況,在後續的章節中會做進一步介紹。

很顯然,在使用範圍條件檢索并鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍内鍵值的并發插入,這往往會造成嚴重的鎖等待​。是以,在實際應用開發中,尤其是并發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來通路更新資料,避免使用範圍條件。​

還要特别說明的是,InnoDB除了通過範圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!下面這個例子假設emp表中隻有101條記錄,其empid的值分别是1,2,……,100,101。 

InnoDB存儲引擎的間隙鎖阻塞例子 

Mysql中的鎖機制

小結

本文重點介紹了MySQL中MyISAM表級鎖和InnoDB行級鎖的實作特點,并讨論了兩種存儲引擎經常遇到的鎖問題和解決辦法。

​對于MyISAM的表鎖,主要讨論了以下幾點:​ 

(1)共享讀鎖(S)之間是相容的,但共享讀鎖(S)與排他寫鎖(X)之間,以及排他寫鎖(X)之間是互斥的,也就是說讀和寫是串行的。 

(2)在一定條件下,MyISAM允許查詢和插入并發執行,我們可以利用這一點來解決應用中對同一表查詢和插入的鎖争用問題。 

(3)MyISAM預設的鎖排程機制是寫優先,這并不一定适合所有應用,使用者可以通過設定LOW_PRIORITY_UPDATES參數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的争用。 

(4)由于表鎖的鎖定粒度大,讀寫之間又是串行的,是以,如果更新操作較多,MyISAM表可能會出現嚴重的鎖等待,可以考慮采用InnoDB表來減少鎖沖突。

​對于InnoDB表,本文主要讨論了以下幾項内容:​ 

(1)InnoDB的行鎖是基于索引實作的,如果不通過索引通路資料,InnoDB會使用表鎖。 

(2)介紹了InnoDB間隙鎖(Next-key)機制,以及InnoDB使用間隙鎖的原因。 

在不同的隔離級别下,InnoDB的鎖機制和一緻性讀政策不同。

  • 盡量使用較低的隔離級别; 精心設計索引,并盡量使用索引通路資料,使加鎖更精确,進而減少鎖沖突的機會;
  • 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小;
  • 給記錄集顯式加鎖時,最好一次性請求足夠級别的鎖。比如要修改資料的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易産生死鎖;
  • 不同的程式通路一組表時,應盡量約定以相同的順序通路各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;
  • 盡量用相等條件通路資料,這樣可以避免間隙鎖對并發插入的影響; 不要申請超過實際需要的鎖級别;除非必須,查詢時不要顯示加鎖;
  • 對于一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能。