mysql鎖相關講解及其應用——《深究mysql鎖》
一、mysql的鎖型別
(1) 共享/排它鎖(Shared and Exclusive Locks)
共享鎖和排他鎖是InnoDB引擎實現的標準行級別鎖。
拿共享鎖是為了讓當前事務去讀一行資料。
拿排他鎖是為了讓當前事務去修改或刪除某一行資料。。
設定共享鎖:select * from user where id = 1 LOCK IN SHARE MODE;
設定排他鎖:select * from user where id = 1 FOR UPDATE;
(2) 意向鎖(Intention Locks)
意向鎖存在的意義在於,使得行鎖和表鎖能夠共存。
意向鎖是表級別的鎖,用來說明事務稍後會對錶中的資料行加哪種型別的鎖(共享鎖或獨佔鎖)。
當一個事務對錶加了意向排他鎖時,另外一個事務在加鎖前就會通過該表的意向排他鎖知道前面已經有事務在對該表進行獨佔操作,從而等待。
(3) 記錄鎖(Record Locks)
記錄鎖是索引記錄上的鎖,例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;會阻止其他事務對c1=10的資料行進行插入、更新、刪除等操作。
記錄鎖總是鎖定索引記錄。如果一個表沒有定義索引,那麼就會去鎖定隱式的“聚集索引”。
(4) 間隙鎖(Gap Locks)
間隙鎖是一個在索引記錄之間的間隙上的鎖。
一個間隙可能跨越單個索引值、多個索引值,甚至為空。
對於使用唯一索引 來搜尋唯一行的語句,只加記錄鎖不加間隙鎖(這並不包括組合唯一索引)。
(5) 臨鍵鎖(Next-key Locks)
Next-Key Locks是行鎖與間隙鎖的組合。當InnoDB掃描索引記錄的時候,會首先對選中的索引記錄加上記錄鎖(Record Lock),然後再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。
(6) 插入意向鎖(Insert Intention Locks)
插入意向鎖是在資料行插入之前通過插入操作設定的間隙鎖定型別。
如果多個事務插入到相同的索引間隙中,如果它們不在間隙中的相同位置插入,則無需等待其他事務。例如:在4和7的索引間隙之間兩個事務分別插入5和6,則兩個事務不會發衝突阻塞。
(7) 自增鎖(Auto-inc Locks)
自增鎖是事務插入到有自增列的表中而獲得的一種特殊的表級鎖。如果一個事務正在向表中插入值,那麼任何其他事務都必須等待,保證第一個事務插入的行是連續的自增值。
二、鎖的實現方式
InnoDB行鎖是通過給索引加鎖來實現的,如果沒有索引,InnoDB會通過隱藏的聚簇索引來對記錄進行加鎖(全表掃描,也就是表鎖)。
但是,為了效率考量,MySQL做了優化,對於不滿足條件的記錄,會放鎖,最終持有的,是滿足條件的記錄上的鎖。但是不滿足條件的記錄上的加鎖/放鎖動作是不會省略的。所以在沒有索引時,不滿足條件的資料行會有加鎖又放鎖的耗時過程。
索引分為主鍵索引和非主鍵索引兩種。如果一條sql語句操作了主鍵索引,MySQL就會鎖定對應主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定非主鍵索引,再鎖定對應的主鍵索引。
三、mysql鎖在4種事務隔離級別裡的應用
事務的四種隔離級別有:
-
讀未提交(Read Uncommitted)
此時select語句不加任何鎖。此時併發最高,但會產生髒讀。
-
讀提交(Read Committed, RC)
普通select語句是快照讀
update語句、delete語句、顯示加鎖的select語句(select … in share mode 或者 select … for update) 等,除了在外來鍵約束檢查和重複鍵檢查時會封鎖區間,其他情況都只使用記錄鎖;
-
可重複讀(Repeated Read, RR)
普通select語句也是快照讀
update語句、delete語句、顯示加鎖的select語句(select … in share mode 或者 select … for update)則要分情況:
-
在唯一索引上使用唯一的查詢條件,則使用記錄鎖。如: select * from user where id = 1;其中id建立了唯一索引。
-
在唯一索引上使用 範圍查詢條件,則使用間隙鎖與臨鍵鎖。如: select * from user where id >20;
-
-
序列化(Serializable)
此時所有select語句都會被隱式加鎖:select … in share mode.
四、快照讀、當前讀
要理解前面四種隔離級別的加鎖方式,對於MVCC、快照讀、當前讀 都是必須要理解的。
MVCC併發控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)。
快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。
當前讀,讀取的是記錄的最新版本,並且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再併發修改這條記錄。
什麼是多版本併發控制(MVCC:multi-version concurrency control )
-
MVCC定義:多版併發控制系統。可認為是行級鎖的一個變種,它能夠避免更多情況下的加鎖操作。
-
作用:避免一些加鎖操作,提升併發效能。
-
實現:通過在每行記錄的後面儲存行的建立時間和過期時間或刪除時間(它們是隱藏的),這兩個時間實際都是系統的版本號。每開始一個新的事務,版本號都會自動增加。
-
具體原理
4.1) select:innoBD查詢時會檢查以下兩個條件:一個是資料行的版本號早於當前事務的版本號;另一個是行的刪除版本號,要麼沒有,要麼大於當前事務的版本號。4.2)insert/delete:innoDB將當前的系統版本號作為新插入(刪除)的資料行的版本號。
4.3)update:先新插入一行資料,並將當前系統版本號作為行的版本號,同時將當前系統版本號作為原來行的刪除版本號。更新主鍵時,聚集索引和普通索引都會產生兩個版本;而更新非主鍵時,只要普通索引會產生兩個版本。
-
注意:MVCC只在read committed和repeatable read兩個隔離級別下工作。
[參考:《高效能mysql》]
快 照 讀 是 哪 些
一個正常的select…語句就是快照讀。
快照讀,使得在RR(repeatable read)級別下一個普通select...語句也能做到可重複讀。即前面MVCC裡提到的利用可見版本來保證資料的一致性。
當 前 讀 是 哪 些
insert語句、update語句、delete語句、顯示加鎖的select語句(select… LOCK IN SHARE MODE、select… FOR UPDATE)是當前讀。
為什麼insert、update、delete語句都屬於當前讀?
這是因為這些語句在執行時,都會執行一個讀取當前資料最新版本的過程。
當前讀的SQL語句,InnoDB是逐條與MySQL Server互動的。即先對一條滿足條件的記錄加鎖後,再返回給MySQL Server,當MySQL Server做完DML操作後,再對下一條資料加鎖並處理。
五、檢視行級鎖爭用情況
執行SQL:mysql> show status like 'InnoDB_row_lock%';
mysql> show status like 'InnoDB_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0 |
| InnoDB_row_lock_time | 0 |
| InnoDB_row_lock_time_avg | 0 |
| InnoDB_row_lock_time_max | 0 |
| InnoDB_row_lock_waits | 0 |
+-------------------------------+-------+
如果發現鎖爭用比較嚴重,還可以通過設定InnoDB Monitors 來進一步觀察發生鎖衝突的表、資料行等,並分析鎖爭用的原因。如:
設定監視器:mysql> create table InnoDB_monitor(a INT) engine=InnoDB;
檢視:mysql> show engine InnoDB status;
停止檢視:mysql> drop table InnoDB_monitor;
六、死鎖
什麼是死鎖:你等我釋放鎖,我等你釋放鎖就會形成死鎖。
如何發現死鎖: 在InnoDB的事務管理和鎖定機制中,有專門檢測死鎖的機制,會在系統中產生死鎖之後的很短時間內就檢測到該死鎖的存在
解決辦法:回滾較小的那個事務
在REPEATABLE-READ隔離級別下,如果兩個執行緒同時對相同條件記錄用SELECT…FOR UPDATE加排他鎖,在沒有符合該條件記錄情況下,兩個執行緒都會加鎖成功。程式發現記錄尚不存在,就試圖插入一條新記錄,如果兩個執行緒都這麼做,就會出現死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可避免問題。
如何判斷事務大小:事務各自插入、更新或者刪除的資料量
注意:
當產生死鎖的場景中涉及到不止InnoDB儲存引擎的時候,InnoDB是沒辦法檢測到該死鎖的,這時候就只能通過鎖定超時限制引數InnoDB_lock_wait_timeout來解決。
七、優化行級鎖定
(1)要想合理利用InnoDB的行級鎖定,做到揚長避短,我們必須做好以下工作:
a)儘可能讓所有的資料檢索都通過索引來完成,從而避免InnoDB因為無法通過索引鍵加鎖而升級為表級鎖定;
b)合理設計索引,讓InnoDB在索引鍵上面加鎖的時候儘可能準確,儘可能的縮小鎖定範圍,避免造成不必要的鎖定而影響其他Query的執行;
c)儘可能減少基於範圍的資料檢索過濾條件,避免因為間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄;
d)儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度;
e)在業務環境允許的情況下,儘量使用較低級別的事務隔離,以減少MySQL因為實現事務隔離級別所帶來的附加成本。
(2)由於InnoDB的行級鎖定和事務性,所以肯定會產生死鎖,下面是一些比較常用的減少死鎖產生概率的小建議:
a)類似業務模組中,儘可能按照相同的訪問順序來訪問,防止產生死鎖;
b)在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
c)對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率。
檢視SQL語句的鎖資訊
檢視sql句的鎖資訊前,需要做如下幾件事:
檢視事務的隔離級別:
- 通過show global variables like “tx_isolation”; 命令檢視。
可通過執行set session transaction isolation level repeatable read;更改成我們想要隔離級別,隔離級別取值如下:
read uncommitted、read committed、repeatable read、serializable
保證事務為手動提交:
- 通過show global variables like “autocommit”;檢視。
如果為ON,則通過執行set session autocommit=0;改為手動提交。
保證間隙鎖開啟:
- 通過show global variables like “innodb_locks%”;檢視
OFF時表示開啟。預設是OFF