講一下MySQL里有哪些鎖?
面試官您好,MySQL中的鎖機制非常豐富,它是保證數據一致性和并發安全的核心。我通常會從鎖的粒度(加鎖范圍) 和鎖的模式(功能) 這兩個維度來理解它們。
第一維度:按鎖的粒度劃分
按加鎖的范圍,MySQL的鎖可以分為三級:全局鎖、表級鎖、行級鎖。粒度從大到小,加鎖開銷從低到高,并發性能則從低到高。
-
1. 全局鎖 (Global Lock)
- 特點:對整個數據庫實例加鎖。
- 具體實現:通過命令
FLUSH TABLES WITH READ LOCK;
(FTWRL) 來實現。 - 作用:執行后,整個數據庫會處于只讀狀態。所有的數據更新語句(
INSERT
,UPDATE
,DELETE
)、數據定義語句(DDL
)和事務提交語句(COMMIT
)都會被阻塞。 - 應用場景:主要用于全庫的邏輯備份。通過加全局鎖,可以保證在備份期間,獲得一個完全一致的數據快照。
-
2. 表級鎖 (Table-level Lock)
- 特點:對整張數據表加鎖。
- 具體實現:
- 表鎖:通過
LOCK TABLES ... READ/WRITE;
命令可以顯式地加表鎖。 - 元數據鎖 (Meta Data Lock, MDL):這是MySQL 5.5引入的,由系統自動添加。當對一個表做增刪改查時,會自動加上MDL讀鎖;當要修改表結構(
DDL
)時,會自動加上MDL寫鎖。MDL鎖主要是為了防止在查詢時,有另一個線程來修改表結構,保證數據一致性。 - 意向鎖 (Intention Lock):這是InnoDB引擎特有的、由系統自動管理的表級鎖。它不與行鎖沖突,它的唯一作用就是“表態”——告訴其他事務,“這張表里有某些行已經被加鎖了”。比如,一個事務想加表級寫鎖,它就不需要去遍歷每一行看有沒有被鎖,只需要檢查一下表上有沒有意向鎖即可,大大提高了效率。
- 表鎖:通過
-
3. 行級鎖 (Row-level Lock)
- 特點:只對某一行或某幾行數據加鎖。這是InnoDB存儲引擎的巨大優勢,也是它能支持高并發的關鍵。行級鎖的粒度最細,鎖沖突的概率最低,并發性能最好。
- 具體實現 (InnoDB):
- 記錄鎖 (Record Lock):最簡單的行鎖,就是精確地鎖住一條索引記錄。
- 間隙鎖 (Gap Lock):它鎖住的是一個 “間隙”,即兩條索引記錄之間的開區間。比如,鎖住(3, 8)這個區間。它的主要作用是防止幻讀,阻止其他事務在這個間隙中插入新的記錄。
- 臨鍵鎖 (Next-Key Lock):可以看作是記錄鎖和間隙鎖的結合體。它既鎖住了記錄本身,又鎖住了該記錄之前的那個間隙。這是InnoDB在 “可重復讀”隔離級別下,默認的行鎖算法。
第二維度:按鎖的模式/功能劃分
- 共享鎖 (Shared Lock, S鎖):也叫讀鎖。多個事務可以同時持有同一份數據的S鎖,大家可以一起讀。
- 排他鎖 (Exclusive Lock, X鎖):也叫寫鎖。它是獨占的。只要有一個事務持有了X鎖,其他任何事務都不能再獲取該數據的任何鎖(無論是S鎖還是X鎖)。
補充:樂觀鎖與悲觀鎖
- 這是一種思想層面的劃分。
- 悲觀鎖:就是上面提到的所有鎖機制,認為沖突總會發生,所以先加鎖再操作。
- 樂觀鎖:不加鎖,而是在更新時通過版本號(version)或CAS機制來檢查數據是否被修改過。這在應用層實現,不是MySQL數據庫自帶的鎖機制。
總結一下,MySQL通過一個從粗到細(全局->表->行)的鎖粒度體系,并結合讀寫模式(S/X鎖),以及在InnoDB中精巧的行鎖實現(記錄鎖、間隙鎖、臨鍵鎖),為我們提供了非常豐富和強大的并發控制能力。在開發中,理解并善用InnoDB的行級鎖,是實現高性能并發事務的關鍵。
數據庫的表鎖和行鎖有什么作用?
面試官您好,表鎖和行鎖是數據庫為了管理并發訪問而采用的兩種不同粒度的鎖定機制。它們沒有絕對的優劣之分,而是分別適用于不同的場景,是在 “加鎖開銷” 和 “并發性能” 之間做出不同權衡的結果。
1. 表鎖 (Table Lock) —— “簡單粗暴,開銷小,但并發差”
-
作用與特點:
- 粒度最大:當一個事務對一張表加鎖時,它會鎖定整張表。
- 實現簡單,加鎖開銷小:因為只需要一個鎖來管理整張表,所以加鎖和釋放鎖的邏輯非常簡單,系統開銷很低。
- 鎖沖突概率最高:這是它最致命的缺點。只要有一個事務鎖住了這張表,其他任何想操作這張表的事務(無論是讀還是寫,取決于鎖的模式)都必須等待。這使得并發性能非常差。
-
適用場景:
- 它非常適合那些大批量的、針對全表的操作。比如,
ALTER TABLE
修改表結構,或者對整張表進行數據遷移、批量更新等。在這些場景下,鎖定整張表反而是最簡單高效的方式。 - MyISAM存儲引擎主要使用的就是表級鎖,這也是為什么MyISAM在寫操作頻繁的場景下并發性能很差的原因。
- 它非常適合那些大批量的、針對全表的操作。比如,
2. 行鎖 (Row Lock) —— “精準控制,并發好,但開銷大”
-
作用與特點:
- 粒度最細:只鎖定被操作的特定一行或幾行數據。
- 并發性能最高:這是它最大的優勢。不同的事務可以同時操作同一張表中的不同行,而互不干擾,極大地提高了系統的并發處理能力。
- 實現復雜,加鎖開銷大:因為需要為每一行都可能建立鎖信息,所以行鎖的實現邏輯更復雜,加鎖和管理的系統開銷也比表鎖要大。
- 可能引發死鎖:由于鎖的粒度變細,多個事務在操作不同行時,更容易形成復雜的鎖等待關系,從而增加了死鎖發生的概率。
-
適用場景:
- 非常適合那些高并發、事務操作只涉及少量行的場景。這幾乎是所有現代在線交易系統(OLTP)的典型特征。比如,訂單系統、用戶賬戶系統等,每次操作都只是針對一個或幾個用戶的特定數據。
- InnoDB存儲引擎的巨大優勢,就在于它實現了高效的行級鎖,這也是它成為MySQL默認和主流存儲引擎的核心原因。
總結與權衡
特性 | 表鎖 (Table Lock) | 行鎖 (Row Lock) |
---|---|---|
鎖定粒度 | 整張表 | 單行/多行數據 |
加鎖開銷 | 小 | 大 |
并發性能 | 差 | 高 |
鎖沖突概率 | 高 | 低 |
死鎖概率 | 低 | 高 |
主要使用者 | MyISAM | InnoDB |
一句話總結:表鎖是用“犧牲并發度”來換取“低開銷和簡單性”,而行鎖是用“更高的系統開銷和更復雜的實現”來換取“極高的并發性能”。在今天的互聯網應用中,高并發是常態,因此支持行鎖的InnoDB引擎成為了絕對的主流。
MySQL兩個線程的UPDATE語句同時處理一條數據,會不會有阻塞?
面試官您好,您提出的這個問題,直擊了數據庫并發控制的核心。
答案是:會的,后一個UPDATE
語句會被阻塞。
這背后的根本原因,是MySQL InnoDB存儲引擎強大的行級鎖機制。
1. 詳細的執行過程分析
我們來詳細地模擬一下這個過程,假設我們有兩個獨立的事務,事務A和事務B,它們都要執行UPDATE ... WHERE id = 1
。
-
事務A搶先執行:
- 首先,事務A開始執行
UPDATE
語句。 - 為了保證數據的一致性和操作的原子性,InnoDB會在定位到
id = 1
這條記錄時,立即為它加上一把排他鎖(Exclusive Lock),也就是X鎖。這把鎖,具體來說,就是一把記錄鎖(Record Lock)。 - 加上X鎖后,事務A開始對這條記錄進行修改。
- 首先,事務A開始執行
-
事務B隨后執行:
- 緊接著,事務B也開始執行它自己的
UPDATE
語句,同樣嘗試去修改id = 1
的記錄。 - 當事務B嘗試去獲取
id = 1
這條記錄的鎖時,它會發現這行記錄已經被事務A持有了X鎖。 - 鎖沖突發生:X鎖是排他的,意味著只要它存在,其他任何事務都無法再對這條記錄獲取任何類型的鎖(無論是共享的S鎖還是排他的X鎖)。
- 緊接著,事務B也開始執行它自己的
-
事務B進入阻塞狀態:
- 因此,事務B的
UPDATE
語句會立即進入阻塞(Waiting)狀態。它會一直在這里等待,直到事務A釋放這把鎖。
- 因此,事務B的
2. 阻塞之后會發生什么?
-
情況一:事務A提交(COMMIT)
- 當事務A完成所有操作并提交后,它會釋放掉所有持有的鎖,包括
id = 1
這條記錄上的X鎖。 - 鎖被釋放的瞬間,正在等待的事務B會被喚醒,它會立即獲取到X鎖,然后繼續執行自己的
UPDATE
操作。
- 當事務A完成所有操作并提交后,它會釋放掉所有持有的鎖,包括
-
情況二:事務A回滾(ROLLBACK)
- 如果事務A因為某種原因回滾了,它同樣會釋放所有鎖。
- 事務B同樣會被喚醒,獲取鎖,然后執行
UPDATE
。
-
情況三:鎖等待超時
- 事務B的等待不是無限的。這個等待時間由MySQL的參數
innodb_lock_wait_timeout
控制(默認是50秒)。 - 如果事務A長時間不提交也不回滾,導致事務B的等待時間超過了這個閾值,那么事務B的
UPDATE
語句就會失敗,并拋出一個 “Lock wait timeout exceeded” 的錯誤。
- 事務B的等待不是無限的。這個等待時間由MySQL的參數
3. 與“讀”操作的對比
-
如果事務B執行的是普通
SELECT
:SELECT * FROM ... WHERE id = 1;
- 在InnoDB的默認隔離級別(可重復讀)下,這個讀操作會通過MVCC來執行,它會去讀取一個歷史快照版本的數據,不會去獲取鎖。因此,它和事務A的
UPDATE
不會發生沖突,不會阻塞。
- 在InnoDB的默認隔離級別(可重復讀)下,這個讀操作會通過MVCC來執行,它會去讀取一個歷史快照版本的數據,不會去獲取鎖。因此,它和事務A的
-
如果事務B執行的是加鎖讀
SELECT ... FOR UPDATE
:- 這個操作也需要獲取X鎖,所以它的行為會和
UPDATE
一樣,同樣會被阻塞。
- 這個操作也需要獲取X鎖,所以它的行為會和
總結一下,兩個線程(事務)同時UPDATE
同一條數據,由于InnoDB行級鎖(X鎖)的互斥性,必然會導致后一個事務被阻塞,直到前一個事務結束。這是數據庫保證寫-寫操作數據一致性的基本手段。
兩條UPDATE語句處理一張表的不同的主鍵范圍的記錄,一個<10,一個>15,會不會遇到阻塞?底層是為什么的?
面試官您好,您提出的這個問題非常好,它觸及了InnoDB在“可重復讀”(Repeatable Read)隔離級別下,范圍更新時臨鍵鎖(Next-Key Lock) 的工作機制。
直接的答案是:在絕大多數情況下,它們不會相互阻塞。但存在一種特殊的邊界情況,可能會導致阻塞。
要理解這一點,我們需要先明確InnoDB的默認行鎖算法——臨鍵鎖。
- 臨鍵鎖 (Next-Key Lock) = 記錄鎖 (Record Lock) + 間隙鎖 (Gap Lock)
- 它不僅會鎖住滿足條件的記錄本身,還會鎖住這條記錄之前的那個 “間隙”。
- 它的主要目的是為了防止幻讀。
下面我們來分兩種情況討論:
情況一:兩個UPDATE
語句的范圍內,都存在實際的數據行(最常見的情況)
假設我們的表中,數據分布是這樣的:id
有 1, 5, 8, 16, 20
…
-
事務A執行:
UPDATE ... WHERE id < 10;
- 加鎖分析:InnoDB會掃描
id
小于10的范圍。- 它會給
id=1, 5, 8
這三條記錄加上X型的記錄鎖。 - 同時,它會給這些記錄之間的間隙,以及
id=8
到id=16
之間的間隙,都加上X型的間隙鎖。
- 它會給
- 最終鎖定的范圍:大致可以理解為
(-∞, 1]
,(1, 5]
,(5, 8]
,(8, 16)
。 注意,16
這條記錄本身沒有被鎖,但它之前的間隙被鎖了。
- 加鎖分析:InnoDB會掃描
-
事務B執行:
UPDATE ... WHERE id > 15;
- 加鎖分析:InnoDB會掃描
id
大于15的范圍。- 它會給
id=16, 20
這兩條記錄加上X型的記錄鎖。 - 同時,它會給
id=16
到id=20
的間隙,以及id=20
到正無窮的間隙,都加上X型的間隙鎖。
- 它會給
- 最終鎖定的范圍:大致可以理解為
(8, 16]
,(16, 20]
,(20, +∞)
。注意,這里的(8, 16]
和事務A的(8, 16)
,實際上是對同一個間隙加鎖,但由于事務B要鎖id=16
這條記錄,它會成功,因為事務A沒有鎖住這條記錄。
- 加鎖分析:InnoDB會掃描
-
結論:在這種情況下,兩個事務加鎖的記錄和間隙是完全不重疊的。因此,它們不會相互阻塞,可以并行執行。
情況二:兩個UPDATE
語句的范圍內,沒有任何數據行(特殊的邊界情況)
這是一個容易被忽略的、但能體現理解深度的特例。假設我們的表中,數據是 id = 20, 30
。
-
事務A執行:
UPDATE ... WHERE id < 10;
- 加鎖分析:InnoDB掃描
id < 10
的范圍,發現沒有任何記錄。 - 間隙鎖的行為:為了防止有其他事務插入
id < 10
的數據(防止幻讀),它仍然需要加一個間隙鎖。它會找到第一個大于10的記錄(即id=20
),然后鎖住從負無窮到id=20
之間的這個巨大間隙。 - 最終鎖定的范圍:
(-∞, 20)
。注意,是開區間,不包括20這條記錄。
- 加鎖分析:InnoDB掃描
-
事務B執行:
UPDATE ... WHERE id > 15;
- 加鎖分析:InnoDB掃描
id > 15
的范圍。- 它首先要定位到大于15的第一條記錄,也就是
id=20
。 - 它嘗試對
id=20
這條記錄以及它之前的間隙(..., 20]
加臨鍵鎖。
- 它首先要定位到大于15的第一條記錄,也就是
- 鎖沖突發生:當它嘗試在
id=20
之前的間隙(也就是(..., 20)
這個區間)加鎖時,發現這個間隙已經被事務A的間隙鎖鎖住了。
- 加鎖分析:InnoDB掃描
-
結論:在這種特殊情況下,雖然兩個
UPDATE
的條件范圍(<10
和>15
)在邏輯上沒有任何交集,但由于間隙鎖的存在,它們可能會去爭搶同一個“間隙”的鎖,從而導致事務B被事務A阻塞。
總結
- 在絕大多數數據分布正常的情況下,兩個
UPDATE
操作不同范圍的記錄,由于行鎖和間隙鎖的范圍不重疊,不會發生阻塞。 - 但在一些邊界或數據稀疏的特殊情況下,由于間隙鎖可能會鎖定一個比查詢范圍更大的“空隙”,導致兩個看似無關的
UPDATE
語句,也可能發生鎖沖突和阻塞。
能分析到第二種情況,并解釋清楚間隙鎖在其中的作用,就能充分展示您對InnoDB鎖機制的深刻理解。
如果2個范圍不是主鍵或索引?還會阻塞嗎?
面試官您好,您提出的這個問題,其答案與上一個問題截然相反。
答案是:會的,后一個UPDATE
語句會被阻塞。
這背后的根本原因,正如您所分析的:當UPDATE
語句的WHERE
條件中,使用的列沒有索引時,MySQL無法進行高效的定位,只能退化為全表掃描。而在InnoDB的“可重復讀”隔離級別下,全表掃描會給掃描過的每一條記錄都加上行鎖(臨鍵鎖)。
1. 發生了什么?—— 從索引定位到全表掃描
- 有索引時:當
WHERE
條件是主鍵或索引列時,InnoDB可以利用B+樹,精確地、快速地定位到需要修改的那幾行記錄,然后只對這幾行以及它們周圍的間隙加鎖。 - 沒有索引時:當
WHERE
條件是一個普通列時(比如status
列),InnoDB不知道哪些行的status
滿足條件。它唯一的辦法,就是從聚簇索引的第一行開始,逐行地向后掃描,直到表的末尾,然后對每一行都判斷其status
值是否符合條件。這個過程,就是全表掃描。
2. 為什么全表掃描會鎖住整張表?
這是InnoDB為了保證事務的隔離性和數據的一致性而必須采取的措施。
- 加鎖過程:在全表掃描的過程中,InnoDB為了確保它正在檢查的行不會被其他事務修改(以避免不可重復讀等問題),它會對自己掃描過的每一條記錄,都加上X型的臨鍵鎖(Next-Key Lock)。
- 最終結果:當這個全表掃描結束后,相當于表中的每一條記錄,以及記錄之間的每一個間隙,都被加上了鎖。從效果上看,這就等同于鎖住了整張表。
3. 場景分析
-
事務A執行:
UPDATE ... WHERE status < 10;
- 由于
status
列沒有索引,InnoDB開始全表掃描。 - 它從第一行開始,掃描一行,加一個臨鍵鎖;再掃描下一行,再加一個臨鍵鎖……
- 當事務A完成掃描并修改了滿足條件的行后,它已經持有了整張表的行鎖和間隙鎖。
- 由于
-
事務B執行:
UPDATE ... WHERE status > 15;
- 事務B開始執行,它也需要進行全表掃描。
- 當它嘗試去掃描并鎖定第一行記錄時,發現這行記錄已經被事務A的鎖鎖住了。
- 鎖沖突發生,事務B立即進入阻塞狀態,等待事務A提交或回滾。
結論與實踐建議
- 結論:當
UPDATE
或DELETE
語句的WHERE
條件列沒有索引時,InnoDB會從行級鎖 “升級” 為事實上的 表級鎖,導致嚴重的鎖競爭和性能問題。 - 實踐建議:
- 這是我們在SQL開發中必須極力避免的情況。
- 對于所有會出現在
UPDATE
或DELETE
語句的WHERE
子句中的列,都應該建立合適的索引。 - 在上線前,對所有核心的寫操作SQL,都應該使用
EXPLAIN
來檢查其執行計劃,確保type
列不是ALL
(全表掃描),并且key
列顯示它用上了正確的索引。
這個例子也從反面印證了索引對于寫操作的重要性——它不僅提升查詢性能,更是縮小寫操作鎖范圍、保證高并發寫入的關鍵。