MySQL 全局鎖、表鎖、行鎖、間隙鎖、臨鍵鎖詳解,架構師必備高并發下的鎖優化策略
余弦:碼哥,我今天面試被問到 “事務并發執行會帶來什么問題,并發安全如何解決呢?MySQL 有哪些鎖?”
今天我要跟你聊聊 MySQL 的鎖。數據庫鎖設計的初衷是處理并發問題。
并發事務訪問相同記錄的情況大致可以劃分以下幾種:
讀-讀情況:即并發事務相繼讀取相同的記錄。讀取操作本身不會對記錄有一毛錢影響,并不會引起什么問題,所以允許這種情況的發生。寫-寫情況:即并發事務相繼對相同的記錄做出改動。
作為多用戶共享的資源,當出現并發訪問的時候,數據庫需要合理地控制資源的訪問規則。
而鎖就是用來實現這些訪問規則的重要數據結構。
根據加鎖的范圍,MySQL 的鎖可以分為全局鎖、表鎖和行鎖。
對于
MyISAM、MEMORY、MERGE這些存儲引擎來說,它們只支持表級鎖,而且這些引擎并不支持事務,所以使用這些存儲引擎的鎖一般都是針對當前會話來說的。
全局鎖
余弦:什么是全局鎖?干嘛用的?
全局鎖就是對整個數據庫實例加鎖。MySQL 提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。
當你需要讓整個庫處于只讀狀態的時候,可以使用這個命令,之后其他線程的以下語句會被阻塞。
- 數據更新語句(數據的增刪改)
- 數據定義語句(包括建表、修改表結構等)
- 更新類事務的提交語句。
全局鎖的適應場景之一,做全庫的邏輯備份。把整個庫的表數據都查出來存儲為文本。
余弦:讓整庫都只讀,在備份期間都不能執行更新,業務基本上就得停擺。這怎么辦?
是的。
- 如果你在主庫上備份,那么在備份期間都不能執行更新,業務基本上就得停擺;
- 如果你在從庫上備份,那么備份期間從庫不能執行主庫同步過來的 binlog,會導致主從延遲。
不加鎖產生的問題
比如手機卡,購買套餐信息。這里分為兩張表 u_acount (用于余額表),u_pricing (資費套餐表)。
- u_account 表中數據 用戶 A 余額:300;u_pricing 表中數據 用戶 A 套餐:空
- 發起備份,備份過程中先備份 u_account 表,備份完了這個表,這個時候 u_account 用戶余額是 300。
- 這個時候套用戶購買了一個資費套餐 100,餐購買完成,寫入到 u_print 套餐表購買成功,備份期間的數據。
- 備份完成。
可以看到備份的結果是,u_account 表中的數據沒有變, u_pricing 表中的數據 已近購買了資費套餐 100.
哪這時候用這個備份文件來恢復數據的話,用戶 A 賺了 100 ,用戶是不是很舒服啊。但是你的想想公司利益啊。
也就是說,不加鎖的話,備份系統備份的得到的庫不是一個邏輯時間點,這個數據是邏輯不一致的。
表級鎖
MySQL 里面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。
表鎖的語法是 lock tables … read/write。與 FTWRL 類似,可以用 unlock tables 主動釋放鎖,也可以在客戶端斷開的時候自動釋放。
另一類表級的鎖是 MDL(metadata lock)。MDL 不需要顯式使用,在訪問一個表的時候會被自動加上。
當對一個表做增刪改查操作的時候,加 MDL 讀鎖;當要對表做結構變更操作的時候,加 MDL 寫鎖。
- 讀鎖之間不互斥,因此你可以有多個線程同時對一張表增刪改查。
- 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。
不過請盡量避免在使用InnoDB存儲引擎的表上使用LOCK TABLES這樣的手動鎖表語句,它們并不會提供什么額外的保護,只是會降低并發能力而已。
InnoDB的厲害之處還是實現了更細粒度的行鎖,關于表級別的鎖大家了解一下就罷了。
余弦:在使用
MySQL過程中,我們可以為表的某個列添加AUTO_INCREMENT屬性。之后在插入記錄時,可以不指定該列的值,系統會自動為它賦上遞增的值。這是什么鎖?
比方說我們有一個表:
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;系統實現這種自動給AUTO_INCREMENT修飾的列遞增賦值的原理主要是兩個:
- 采用表級別
AUTO-INC鎖,然后為每條待插入記錄的AUTO_INCREMENT修飾的列分配遞增的值,在該語句執行結束后,再把AUTO-INC鎖釋放掉。
這樣一個事務在持有AUTO-INC鎖的過程中,其他事務的插入語句都要被阻塞,可以保證一個語句中分配的遞增值是連續的。
如果我們的插入語句在執行前不可以確定具體要插入多少條記錄(無法預計即將插入記錄的數量),比方說使用INSERT ... SELECT、REPLACE ... SELECT或者LOAD DATA這種插入語句,一般是使用AUTO-INC鎖為AUTO_INCREMENT修飾的列生成對應的值。
- 輕量級的鎖,這種方式可以避免鎖定表,可以提升插入性能:
- 在為插入語句生成
AUTO_INCREMENT修飾的列的值時獲取一下這個輕量級鎖,然后生成本次插入語句需要用到的AUTO_INCREMENT列的值之后,就把該輕量級鎖釋放掉,并不需要等到整個插入語句執行完才釋放鎖。 - 如果我們的插入語句在執行前就可以確定具體要插入多少條記錄,比方說我們上邊舉的關于表
t的例子中,在語句執行前就可以確定要插入 2 條記錄,那么一般采用輕量級鎖的方式對AUTO_INCREMENT修飾的列進行賦值。
行鎖
行鎖,也稱為記錄鎖,顧名思義就是在記錄上加的鎖。這是最復雜的鎖,前面的只是開胃菜。
一個行鎖玩出了各種花樣,也就是把行鎖分成了各種類型。
MySQL 的行級鎖是 InnoDB 存儲引擎實現高并發的核心技術之一。它允許在保證數據一致性的同時,大幅提升數據庫的并發處理能力。
下面這張表格匯總了行級鎖的主要類型和核心特點,可以幫助你快速建立整體印象。
鎖類型 | 英文名 | 鎖定對象 | 主要作用 | 常見觸發方式 |
記錄鎖 | Record Lock | 索引中的單條記錄 | 防止其他事務修改或刪除被鎖定的記錄 |
或更新語句通過索引精準匹配到一條存在記錄時 |
間隙鎖 | Gap Lock | 索引記錄之間的間隙 | 防止其他事務在間隙中插入新數據,從而避免“幻讀” | 范圍查詢(如 |
臨鍵鎖 | Next-Key Lock | 記錄鎖 + 間隙鎖 ,鎖定一個左開右閉的區間 | 既防止幻讀,又保證當前讀的數據一致性,是 InnoDB 在可重復讀隔離級別下的默認鎖算法 | 范圍查詢或在非唯一索引上的等值查詢時 |
共享鎖與排他鎖
從鎖的互斥性來看,行級鎖分為共享鎖和排他鎖,它們的兼容關系是理解鎖沖突的基礎。
- 共享鎖(Shared Lock, S Lock):也稱為“讀鎖”。
作用:允許多個事務同時讀取同一行數據。
互斥性:一個事務持有共享鎖后,其他事務可以繼續加共享鎖來讀取數據,但不能加排他鎖來修改數據
加鎖方式:使用 SELECT ... LOCK IN SHARE MODE;或 SELECT ... FOR SHARE;
- 排他鎖(Exclusive Lock, X Lock):也稱為“寫鎖”。
使用 SELECT ... FOR UPDATE;顯式加鎖。
自動加鎖:UPDATE, DELETE, INSERT語句會自動對其操作的記錄加排他鎖。
作用:允許一個事務讀寫某行數據。
互斥性:一個事務持有排他鎖后,其他事務不能再對該行加任何類型的鎖(包括共享鎖和其他排他鎖),直到該鎖被釋放。
加鎖方式:
它們的兼容關系可以總結為下表:
當前已持有的鎖 | 請求 共享鎖 (S) | 請求 排他鎖 (X) |
共享鎖 (S) | ?? 兼容 | ? 沖突 |
排他鎖 (X) | ? 沖突 | ? 沖突 |
行鎖的底層實現和特性
行鎖基于索引實現
這是理解行鎖最核心的一點。InnoDB 的行鎖是加在索引項上的,而不是直接加在物理數據行上的。這意味著:
- 有效使用行鎖的前提:你的
WHERE條件必須能夠有效命中索引。 - 否則退化為表鎖:如果查詢條件不能使用索引,MySQL 將進行全表掃描,從而對所有記錄加鎖,實際效果等同于鎖住了整個表,會嚴重降低并發性能
兩階段鎖協議(Two-Phase Locking, 2PL)
InnoDB 遵循此協議,鎖的操作分為兩個階段:
- 加鎖階段:在事務執行過程中,根據需要逐步獲取鎖。
- 解鎖階段:直到事務提交(COMMIT)或回滾(ROLLBACK)時,一次性釋放所有在該事務中獲取的鎖。
- 重要提示:由于鎖是在事務結束后才釋放,為了減少鎖沖突和提高并發性,應盡量將最可能引起沖突的寫操作(如
SELECT ... FOR UPDATE)放在事務的后面執行,以縮短排他鎖的持有時間。
意向鎖(Intention Locks)
意向鎖是表級鎖,用于快速判斷表內是否有被鎖定的行,從而避免為了檢查行鎖而需要遍歷每一行的低效操作。
- 意向共享鎖(IS):表示事務準備在表中的某些行上加共享鎖。
- 意向排他鎖(IX):表示事務準備在表中的某些行上加排他鎖。意向鎖之間是兼容的(例如,多個事務可以同時對一個表持有 IX 鎖),但它們與表級共享鎖(S)和排他鎖(X)存在互斥關系。意向鎖由 InnoDB 自動管理,無需用戶干預
行鎖類型可視化
理解行鎖的關鍵在于區分其三種基本類型。下圖通過一個數據索引的例子,清晰展示了三種鎖的鎖定范圍差異,這是理解所有高級鎖概念的基礎。
圖片
圖解說明:
- 記錄鎖(Record Lock):像一把鑰匙只鎖一個特定的座位(如
id=10)。它確保在更新或刪除某條確切存在的記錄時,其他事務無法修改或刪除它。 - 間隙鎖(Gap Lock):鎖住的是座位與座位之間的“空檔”(如
(10, 15))。它的存在僅僅是為了防止插入(防止幻讀),但不會阻止其他事務修改這個區間內已存在的記錄(例如,如果id=12存在,你仍然可以修改它)。 - 臨鍵鎖(Next-Key Lock):這是 InnoDB 在可重復讀(RR)隔離級別下默認的鎖算法。它是記錄鎖和間隙鎖的結合,鎖定一個左開右閉的區間。例如,
(5, 10]意味著它鎖定了id=10這條記錄,也鎖定了5到10之間的間隙。這能有效防止在10之前插入新數據(解決幻讀),同時保護10這條記錄本身。
以下通過示例和場景進一步解釋這三種鎖。
記錄鎖(Record Lock)
它鎖住的是索引項。例如,執行 SELECT * FROM users WHERE id = 10 FOR UPDATE;會在 id=10這個索引項上加一個排他型的記錄鎖,防止其他事務修改或刪除這行數據。
間隙鎖(Gap Lock)
它鎖住的是索引項之間的“空隙”,以防止其他事務在這個空隙中插入新數據,從而解決“幻讀”問題。
間隙鎖只在可重復讀(REPEATABLE READ)及以上隔離級別生效
- 示例:假設一張表
users的id字段有值 5, 10, 15。 - 事務 A 執行:
SELECT * FROM users WHERE id BETWEEN 10 AND 15 FOR UPDATE; - 它不僅會鎖住
id=10和15的記錄,還會鎖住它們之間的間隙(10, 15)。 - 此時事務 B 嘗試執行
INSERT INTO users (id) VALUES (12);會被阻塞,因為12落在了被鎖定的間隙內。
臨鍵鎖(Next-Key Lock)
它是 InnoDB 在可重復讀(REPEATABLE READ)隔離級別下默認使用的鎖算法。
它相當于一個 記錄鎖 + 間隙鎖,鎖定一個左開右閉的區間 (previous_index, current_index]。
- 示例:同樣對于
id值為 5, 10, 15 的表。 - 事務 A 執行:
SELECT * FROM users WHERE id > 10 FOR UPDATE; - 它可能鎖住的區間包括
(10, 15]和(15, +∞)。 - 這既防止了在
(10, 15)區間內插入新的id=12,也防止了修改或刪除id=15的現有記錄,同時還防止了插入任何大于 15 的新 ID。
死鎖與最佳實踐
死鎖是如何產生的呢?
行級鎖雖然提升了并發度,但也帶來了死鎖的風險。當兩個或多個事務互相等待對方釋放鎖時,就會形成死鎖。
理解 MySQL 中事務的加鎖流程以及死鎖如何形成,是構建高并發應用的基石。
下面我們通過一個清晰的流程圖來展示一個安全的事務加鎖/解鎖全過程,然后深入剖析幾種典型的死鎖場景。
事務加鎖與解鎖完成流程
首先要明確一個核心概念:兩階段鎖協議。它規定鎖的操作分為兩個階段:
- 加鎖階段:在事務執行過程中,根據需要逐步獲取鎖。
- 解鎖階段:直到事務提交(
COMMIT)或回滾(ROLLBACK)時,一次性釋放所有在該事務中獲取的鎖。
下面的序列圖清晰地展示了一個安全、無沖突的事務加鎖與解鎖流程。
圖片
流程解讀:
- 加鎖:事務 A 首先開啟事務,然后執行一條
SELECT ... FOR UPDATE語句,意圖鎖定某行數據(例如行 1)進行更新。此時,它會向鎖管理器申請該行的排他鎖(X Lock) - 執行:成功獲得鎖后,事務 A 可以安全地執行修改操作。在此期間,其他事務(如事務 B)如果嘗試獲取同一行的排他鎖或共享鎖,都會被阻塞。
- 解鎖:當事務 A 提交(
COMMIT)后,進入解鎖階段,一次性釋放它持有的所有鎖。 - 后續:鎖被釋放后,之前被阻塞的事務 B 會被喚醒,并獲得它所需要的鎖,繼續執行。
這個流程是理想狀態下的。但當多個事務并發執行且鎖的獲取順序出現環狀依賴時,死鎖就發生了。
典型死鎖場景詳解
這個流程是理想狀態下的。但當多個事務并發執行且鎖的獲取順序出現環狀依賴時,死鎖就發生了。
場景 1:共享鎖升級導致的死鎖
這是非常經典的死鎖情況,常發生在先讀后寫的業務邏輯中。
時間點 | 事務 A | 事務 B |
T1 |
(獲得 id=1 的S 鎖) | |
T2 |
(也獲得 id=1 的S 鎖) | |
T3 |
(嘗試將S 鎖升級為 X 鎖,但需等待事務 B 的 S 鎖釋放,阻塞) | |
T4 |
(也嘗試將S 鎖升級為 X 鎖,但需等待事務 A 的 S 鎖釋放) |
死鎖形成:
- 在 T3 時刻,事務 A 需要事務 B 釋放 S 鎖才能升級。
- 在 T4 時刻,事務 B 需要事務 A 釋放 S 鎖才能升級。
- 雙方互相等待,形成循環等待,死鎖產生。
場景 2:順序交叉訪問導致的死鎖
當多個事務以不同的順序訪問和鎖定資源時,極易發生死鎖。
時間點 | 事務 A | 事務 B |
T1 |
(成功獲得 id=1 的X 鎖) | |
T2 |
(成功獲得 id=2 的X 鎖) | |
T3 |
(嘗試獲取 id=2 的 X 鎖,被事務 B 阻塞) | |
T4 |
(嘗試獲取 id=1 的 X 鎖,被事務 A 阻塞) |
死鎖形成:
- 事務 A 在等待事務 B 釋放 id=2 的鎖。
- 事務 B 在等待事務 A 釋放 id=1 的鎖。
- 循環等待再次形成,死鎖發生。
場景 3:Gap 鎖沖突導致的死鎖
在可重復讀(REPEATABLE READ)隔離級別下,MySQL 會使用間隙鎖(Gap Lock)來防止幻讀,這也可能引發更復雜的死鎖。
假設 accounts表 id 有 1, 5, 10 三個值,存在間隙 (1,5), (5,10)。
時間點 | 事務 A | 事務 B |
T1 |
(在間隙(1,5)上加了Gap 鎖) | |
T2 |
(同樣在間隙(1,5)上加了Gap 鎖,Gap 鎖之間不互斥,成功) | |
T3 |
(嘗試插入,需要獲取插入意向鎖,與事務 B 的 Gap 鎖沖突,阻塞) | |
T4 |
(同樣需要獲取插入意向鎖,與事務 A 的 Gap 鎖沖突) |
死鎖形成:
- 事務 A 的插入在等待事務 B 釋放(1,5)間隙上的鎖。
- 事務 B 的插入在等待事務 A 釋放(1,5)間隙上的鎖。
- 循環等待形成,死鎖發生。
死鎖的處理與預防
nnoDB 存儲引擎內置了死鎖檢測機制。當檢測到死鎖時,它會選擇一個回滾代價較小的事務(通常是影響行數較少的事務)進行回滾,并讓另一個事務繼續執行。
被回滾的事務會收到 ERROR 1213 (40001): Deadlock found錯誤。
核心預防策略
- 固定訪問順序:在應用設計中,保證所有事務以相同的順序訪問數據行。例如,約定更新賬戶時總是按 id 從小到大處理,可以避免場景二的死鎖
- 避免在事務中加鎖:如果業務允許,使用樂觀鎖(如版本號機制)替代悲觀鎖,從根本上減少鎖競爭
- 保持事務小巧且快速:大事務會長時間持有鎖,增加死鎖概率。應將大事務拆分為小事務
- 為查詢創建合適的索引:如果查詢條件未使用索引,InnoDB 可能會鎖住更多記錄(甚至全表),大幅增加死鎖風險。
- 使用較低的隔離級別:如果業務能接受,將隔離級別設為讀已提交(READ COMMITTED),此級別下 InnoDB 不會使用間隙鎖(Gap Lock),可減少死鎖。
事務的加鎖解鎖遵循“兩階段鎖協議”,提交或回滾時釋放所有鎖。死鎖的本質是事務間形成了對鎖資源的循環等待。
通過理解其原理并采用固定的資源訪問順序、使用樂觀鎖、減小事務粒度等預防措施,可以顯著降低死鎖發生概率。
希望這些圖解和說明能幫助你更深入地理解 MySQL 的鎖機制。






























