【MySQL】MySQL中的鎖機制

MySQL鎖機制起步

鎖是計算機用以協調多個進程間併發訪問同一共享資源的一種機制。MySQL中爲了保證數據訪問的一致性與有效性等功能,實現了鎖機制,MySQL中的鎖是在服務器層或者存儲引擎層實現的。html

行鎖與表鎖

首先咱們來了解行鎖與表鎖的基本概念,從名字中咱們就能夠了解:表鎖就是對整張表進行加鎖,而行鎖則是鎖定某行、某幾行數據或者行之間的間隙。mysql

各引擎對鎖的支持狀況以下:linux

行鎖 表鎖 頁鎖
MyISAM
BDB
InnoDB

行鎖

A record lock is a lock on an index record. Record locks always lock index records, even if a table is defined with no indexes. For such cases, InnoDB creates a hidden clustered index and uses this index for record locking.sql

上文出自MySQL的官方文檔,從這裏咱們能夠看出行鎖是做用在索引上的,哪怕你在建表的時候沒有定義一個索引,InnoDB也會建立一個聚簇索引並將其做爲鎖做用的索引。數據庫

這裏仍是講一下InnoDB中的聚簇索引。每個InnoDB表都須要一個聚簇索引,有且只有一個。若是你爲該表表定義一個主鍵,那麼MySQL將使用主鍵做爲聚簇索引;若是你不爲定義一個主鍵,那麼MySQL將會把第一個惟一索引(並且要求NOT NULL)做爲聚簇索引;若是上訴兩種狀況都GG,那麼MySQL將自動建立一個名字爲GEN_CLUST_INDEX的隱藏聚簇索引。服務器

由於是聚簇索引,因此B+樹上的葉子節點都存儲了數據行,那麼若是如今是二級索引呢?InnoDB中的二級索引的葉節點存儲的是主鍵值(或者說聚簇索引的值),因此經過二級索引查詢數據時,還須要將對應的主鍵去聚簇索引中再次進行查詢。網絡

關於索引的問題就到這,咱們用一張直觀的圖來表示行鎖:session

接下來以兩條SQL的執行爲例,講解一下InnoDB對於單行數據的加鎖原理:併發

update user set age = 10 where id = 49;
update user set age = 10 where name = 'Tom';

第一條SQL使用主鍵查詢,只須要在 id = 49 這個主鍵索引上加上鎖。第二條 SQL 使用二級索引來查詢,那麼首先在 name = Tom 這個索引上加寫鎖,而後因爲使用 InnoDB 二級索引還需再次根據主鍵索引查詢,因此還須要在 id = 49 這個主鍵索引上加鎖。性能

也就是說使用主鍵索引須要加一把鎖,使用二級索引須要在二級索引和主鍵索引上各加一把鎖。

根據索引對單行數據進行更新的加鎖原理了解了,那若是更新操做涉及多個行呢,好比下面 SQL 的執行場景。

update user set age = 10 where id > 49;

上述 SQL 的執行過程以下圖所示。MySQL Server 會根據 WHERE 條件讀取第一條知足條件的記錄,而後 InnoDB 引擎會將第一條記錄返回並加鎖,接着 MySQL Server 發起更新改行記錄的 UPDATE 請求,更新這條記錄。一條記錄操做完成,再讀取下一條記錄,直至沒有匹配的記錄爲止。

表鎖

上面咱們講解行鎖的時候,操做語句中的條件判斷列都是有創建索引的,那麼若是如今的判斷列不存在索引呢?InnoDB既支持行鎖,也支持表鎖,當沒有查詢列沒有索引時,InnoDB就不會去搞什麼行鎖了,畢竟行鎖必定要有索引,因此它如今搞表鎖,把整張表給鎖住了。那麼具體啥是表鎖?還有其餘什麼狀況下也會進行鎖表呢?

表鎖使用的是一次性鎖技術,也就是說,在會話開始的地方使用 lock 命令將後續須要用到的表都加上鎖,在表釋放前,只能訪問這些加鎖的表,不能訪問其餘表,直到最後經過 unlock tables 釋放全部表鎖。

除了使用 unlock tables 顯示釋放鎖以外,會話持有其餘表鎖時執行lock table 語句會釋放會話以前持有的鎖;會話持有其餘表鎖時執行 start transaction 或者 begin 開啓事務時,也會釋放以前持有的鎖。

表鎖由 MySQL Server 實現,行鎖則是存儲引擎實現,不一樣的引擎實現的不一樣。在 MySQL 的經常使用引擎中 InnoDB 支持行鎖,而 MyISAM 則只能使用 MySQL Server 提供的表鎖。

兩種鎖的比較

表鎖:加鎖過程的開銷小,加鎖的速度快;不會出現死鎖的狀況;鎖定的粒度大,發生鎖衝突的概率大,併發度低;

  • 通常在執行DDL語句時會對整個表進行加鎖,好比說 ALTER TABLE 等操做;

  • 若是對InnoDB的表使用行鎖,被鎖定字段不是主鍵,也沒有針對它創建索引的話,那麼將會鎖整張表;

  • 表級鎖更適合於以查詢爲主,併發用戶少,只有少許按索引條件更新數據的應用,如Web 應用。

行鎖:加鎖過程的開銷大,加鎖的速度慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的機率最低,併發度也最高;

  • 最大程度的支持併發,同時也帶來了最大的鎖開銷。
  • 在 InnoDB 中,除單個 SQL 組成的事務外,鎖是逐步得到的,這就決定了在 InnoDB 中發生死鎖是可能的。
  • 行級鎖只在存儲引擎層實現,而 MySQL 服務器層沒有實現。 行級鎖更適合於有大量按索引條件併發更新少許不一樣數據,同時又有併發查詢的應用,如一些在線事務處理(OLTP)系統。

MyISAM表鎖

MyISAM表級鎖模式

  • 表共享讀鎖(Table Read Lock):不會阻塞其餘線程對同一個表的讀操做請求,但會阻塞其餘線程的寫操做請求;

  • 表獨佔寫鎖(Table Write Lock):一旦表被加上獨佔寫鎖,那麼不管其餘線程是讀操做仍是寫操做,都會被阻塞;

默認狀況下,寫鎖比讀鎖具備更高的優先級;當一個鎖釋放後,那麼它會優先相應寫鎖等待隊列中的鎖請求,而後再是讀鎖中等待的獲取鎖的請求。

This ensures that updates to a table are not 「starved」 even when there is heavy SELECT activity for the table. However, if there are many updates for a table, SELECT statements wait until there are no more updates.

這種設定也是MyISAM表不適合於有大量更新操做和查詢操做的緣由。大量更新操做可能會形成查詢操做很難以獲取讀鎖,從而過長的阻塞。同時一些須要長時間運行的查詢操做,也會使得線程「餓死」,應用中應儘可能避免出現長時間運行的查詢操做(在可能的狀況下能夠經過使用中間表等措施對SQL語句作必定的「分解」,使每一步查詢都能在較短的時間內完成,從而減小鎖衝突。若是複雜查詢不可避免,應儘可能安排在數據庫空閒時段執行,好比一些按期統計能夠安排在夜間執行。)

咱們能夠經過一些設置來調節MyISAM的調度行爲:

  • 經過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利;
  • 經過執行命令SET LOW_PRIORITY_UPDATES=1,使該鏈接發出的更新請求優先級下降;
  • 經過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,下降該語句的優先級;
  • 給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值後,MySQL就暫時將寫請求的優先級下降,給讀進程必定得到鎖的機會。

MyISAM對錶加鎖分析

MyISAM在執行查詢語句(SELECT)前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預,所以用戶通常不須要直接用 LOCK TABLE 命令給 MyISAM 表顯式加鎖。在自動加鎖的狀況下,MyISAM 老是一次得到 SQL 語句所須要的所有鎖,這也正是 MyISAM 表不會出現死鎖(Deadlock Free)的緣由。

MyISAM存儲引擎支持併發插入,以減小給定表的讀操做和寫操做之間的爭用:

若是MyISAM表在數據文件中沒有空閒塊(因爲刪除或更新致使的空行),則行始終插入數據文件的末尾。在這種狀況下,你能夠自由混合併發使用MyISAM表的 INSERT 和 SELECT 語句而不須要加鎖(你能夠在其餘線程進行讀操做的狀況下,同時將行插入到MyISAM表中)。若是文件中有空閒塊,則併發插入會被禁止,但當全部的空閒塊從新填充有新數據時,它又會自動啓用。 要控制此行爲,可使用MySQL的concurrent_insert系統變量。

  • 當concurrent_insert=0時,不容許併發插入功能。

  • 當concurrent_insert=1時,容許對沒有空閒塊的表使用併發插入,新數據位於數據文件結尾(缺省)。

  • 當concurrent_insert=2時,無論表有沒有空想快,都容許在數據文件結尾併發插入。

顯式加表鎖的應用

上面已經說起了表鎖的加鎖方式,通常表鎖都是隱式加鎖的,不須要咱們去主動聲明,可是也有須要顯式加鎖的狀況,這裏簡單作下介紹:

給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;

查看錶鎖爭用狀況:

能夠經過檢查 table_locks_waited 和 table_locks_immediate 狀態變量來分析系統上的表鎖的爭奪,若是 Table_locks_waited 的值比較高,則說明存在着較嚴重的表級鎖爭用狀況:

mysql> SHOW STATUS LIKE 'Table%';
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+-----------------------+---------+

InnoDB行鎖與表鎖

InnoDB鎖模式

1)InnoDB中的行鎖

InnoDB實現瞭如下兩種類型的行鎖:

  • 共享鎖(S):加了鎖的記錄,全部事務都能去讀取但不能修改,同時阻止其餘事務得到相同數據集的排他鎖;
  • 排他鎖(X):容許已經得到排他鎖的事務去更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖;

2)InnoDB表鎖——意向鎖

因爲表鎖和行鎖雖然鎖定範圍不一樣,可是會相互衝突。當你要加表鎖時,勢必要先遍歷該表的全部記錄,判斷是否有排他鎖。這種遍歷檢查的方式顯然是一種低效的方式,MySQL引入了意向鎖,來檢測表鎖和行鎖的衝突。

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table。

The intention locking protocol is as follows:

  • Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.

  • Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

意向鎖也是表級鎖,分爲讀意向鎖(IS鎖)和寫意向鎖(IX鎖)。當事務要在記錄上加上行鎖時,要首先在表上加上意向鎖。這樣判斷表中是否有記錄正在加鎖就很簡單了,只要看下錶上是否有意向鎖就好了,從而就能提升效率。

意向鎖之間是不會產生衝突的,它只會阻塞表級讀鎖或寫鎖。意向鎖不於行級鎖發生衝突。

鎖模式的兼容矩陣

下面表顯示了了各類鎖之間的兼容狀況:

X IX S IS
X
IX 兼容 兼容
S 兼容 兼容
IS 兼容 兼容 兼容

(注意上面的X與S是說表級的X鎖和S鎖,意向鎖不和行級鎖發生衝突)

若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;若是二者不兼容,那麼該事務就須要等待鎖的釋放。

InnoDB的加鎖方法

  • 意向鎖是 InnoDB 自動加的,不須要用戶干預;
  • 對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及的數據集加上排他鎖;
  • 對於普通的SELECT語句,InnoDB不會加任何鎖;事務能夠經過如下語句顯示給記錄集添加共享鎖或排他鎖:
    • 共享鎖(S):select * from table_name where ... lock in share mode。此時其餘 session 仍然能夠查詢記錄,並也能夠對該記錄加 share mode 的共享鎖。可是若是當前事務須要對該記錄進行更新操做,則頗有可能形成死鎖。
    • 排他鎖(X):select * from table_name where ... for update。其餘session能夠查詢記錄,可是不能對該記錄加共享鎖或排他鎖,只能等待鎖釋放後在加鎖。

select for update

在執行這個 select 查詢語句的時候,會將對應的索引訪問條目加上排他鎖(X鎖),也就是說這個語句對應的鎖就至關於update帶來的效果;

使用場景:爲了讓確保本身查找到的數據必定是最新數據,而且查找到後的數據值容許本身來修改,此時就須要用到select for update語句;

性能分析:select for update語句至關於一個update語句。在業務繁忙的狀況下,若是事務沒有及時地commit或者rollback可能會形成事務長時間的等待,從而影響數據庫的併發使用效率。

select lock in share mode

in share mode 子句的做用就是將查找的數據加上一個share鎖,這個就是表示其餘的事務只能對這些數據進行簡單的 select 操做,而不能進行 DML 操做。

使用場景:爲了確保本身查詢的數據不會被其餘事務正在修改,也就是確保本身查詢到的數據是最新的數據,而且不容許其餘事務來修改數據。與select for update不一樣的是,本事務在查找完以後不必定能去更新數據,由於有可能其餘事務也對同數據集使用了 in share mode 的方式加上了S鎖;

性能分析:select lock in share mode 語句是一個給查找的數據上一個共享鎖(S 鎖)的功能,它容許其餘的事務也對該數據上S鎖,可是不可以容許對該數據進行修改。若是不及時的commit 或者rollback 也可能會形成大量的事務等待。

InnoDB的鎖爭用狀況

能夠經過檢查 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 | 
+-------------------------------+-------+ 
5 rows in set (0.01 sec)

行鎖的類型

上面咱們根據了鎖的粒度將鎖分爲了行鎖與表鎖,接下來根據使用場景的不一樣,又能夠將行鎖進行進一步的劃分:Next-Key Lock、Gap Lock、Record Lock以及插入意向GAP鎖。

不一樣的鎖鎖定的位置是不一樣的,好比說記錄鎖只鎖定對應的記錄,而間隙鎖鎖住記錄和記錄之間的間隙,Next-key Lock則鎖住所屬記錄之間的間隙。不一樣的鎖類型鎖定的範圍大體如圖所示:

記錄鎖(Record Lock)

記錄鎖最簡單的一種行鎖形式,上面咱們以及稍微說起過了。這裏補充下的點就是:行鎖是加在索引上的,若是當你的查詢語句不走索引的話,那麼它就會升級到表鎖,最終形成效率低下,因此在寫SQL語句時須要特別注意。

間隙鎖(Gap Lock)

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record。

當咱們使用範圍條件而不是相等條件去檢索,並請求鎖時,InnoDB就會給符合條件的記錄的索引項加上鎖;而對於鍵值在條件範圍內但並不存在(參考上面所說的空閒塊)的記錄,就叫作間隙,InnoDB在此時也會對間隙加鎖,這種記錄鎖+間隙鎖的機制叫Next-Key Lock。額,扯的有點快。

從上面這句話能夠代表間隙鎖是所在兩個存在的索引之間,是一個開區間,像最開始的那張索引圖,15和18之間,是有(16,17)這個間隙存在的。

Gap locks in InnoDB are 「purely inhibitive」, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

上面這段話代表間隙鎖是能夠共存的,共享間隙鎖與獨佔間隙鎖之間是沒有區別的,二者之間並不衝突。其存在的目的都是防止其餘事務往間隙中插入新的紀錄,故而一個事務所採起的間隙鎖是不會去阻止另一個事務在同一個間隙中加鎖的。

固然也不是在何時都會去加間隙鎖的:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED. Under these circumstances, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

這段話代表,在 RU 和 RC 兩種隔離級別下,即便你使用 select in share mode 或 select for update,也沒法防止幻讀(讀後寫的場景)。由於這兩種隔離級別下只會有行鎖,而不會有間隙鎖。而若是是 RR 隔離級別的話,就會在間隙上加上間隙鎖。

臨鍵鎖(Next-key Lock)

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

臨鍵鎖是記錄鎖與與間隙鎖的結合,因此臨鍵鎖與間隙鎖是一個同時存在的概念,而且臨鍵鎖是個左開有閉的卻好比(16, 18]。

關於臨鍵鎖與幻讀,官方文檔有這麼一條說明:

By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows.

就是說 MySQL 默認隔離級別是RR,在這種級別下,若是你使用 select in share mode 或者 select for update 語句,那麼InnoDB會使用臨鍵鎖(記錄鎖 + 間隙鎖),於是能夠防止幻讀;

可是我也在網上看到相關描述:即便你的隔離級別是 RR,若是你這是使用普通的select語句,那麼此時 InnoDB 引擎將是使用快照讀,而不會使用任何鎖,於是仍是沒法防止幻讀。

插入意向鎖(Insert Intention Lock)

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

官方文檔已經解釋得很清楚了,這裏我作個翻譯機:

插入意圖鎖是一種間隙鎖,在行執行 INSERT 以前的插入操做設置。若是多個事務 INSERT 到同一個索引間隙之間,但沒有在同一位置上插入,則不會產生任何的衝突。假設有值爲4和7的索引記錄,如今有兩事務分別嘗試插入值爲 5 和 6 的記錄,在得到插入行的排他鎖以前,都使用插入意向鎖鎖住 4 和 7 之間的間隙,但二者之間並不會相互阻塞,由於這兩行並不衝突。

插入意向鎖只會和 間隙或者 Next-key 鎖衝突,正如上面所說,間隙鎖做用就是防止其餘事務插入記錄形成幻讀,正是因爲在執行 INSERT 語句時須要加插入意向鎖,而插入意向鎖和間隙鎖衝突,從而阻止了插入操做的執行。

不一樣類型鎖之間的兼容

不一樣類型的鎖之間的兼容以下表所示:

RECORED GAP NEXT-KEY II GAP(插入意向鎖)
RECORED 兼容 兼容
GAP 兼容 兼容 兼容 兼容
NEXT-KEY 兼容 兼容
II GAP 兼容 兼容

(其中行表示已有的鎖,列表示意圖加上的鎖)

其中,第一行表示已有的鎖,第一列表示要加的鎖。插入意向鎖較爲特殊,因此咱們先對插入意向鎖作個總結,以下:

  • 插入意向鎖不影響其餘事務加其餘任何鎖。也就是說,一個事務已經獲取了插入意向鎖,對其餘事務是沒有任何影響的;
  • 插入意向鎖與間隙鎖和 Next-key 鎖衝突。也就是說,一個事務想要獲取插入意向鎖,若是有其餘事務已經加了間隙鎖或 Next-key 鎖,則會阻塞。

其餘類型的鎖的規則較爲簡單:

  • 間隙鎖不和其餘鎖(不包括插入意向鎖)衝突;
  • 記錄鎖和記錄鎖衝突,Next-key 鎖和 Next-key 鎖衝突,記錄鎖和 Next-key 鎖衝突;

文章寫到這裏吧,再寫下去有點長了。上面文章中不少信息都來源網絡,我只是個搬運工,倘若哪裏有表述錯誤,請評論區留言。

參考資料:

相關文章
相關標籤/搜索