MySQL的并發訪問機制
在MySQL中,鎖是用于解決并發訪問沖突的核心機制。當多個事務同時操作數據庫中的數據時(如讀取、修改、刪除),可能會出現數據不一致(如臟讀、不可重復讀、幻讀)或操作沖突(如同時修改同一行),鎖的作用就是通過合理限制不同事務的操作權限,保證數據的一致性和并發操作的正確性。本文只討論InnoDB引擎下并發訪問控制。
鎖的粒度
MySQL從鎖的粒度上分為表級鎖和行級鎖,MySQL會根據不同情況判斷是對表上鎖還是對行上鎖。
表級鎖
表級鎖有四種類型:
- 共享鎖
- 顯示上鎖 : LOCK TABLES 表名 READ; UNLOCK TABLES;
- 排他鎖
- 顯示上鎖 : LOCK TABLES 表名 WRITE; UNLOCK TABLES;
- 隱式上鎖:如果行級鎖需要加鎖的數據條數太多,會直接加表鎖,減少維護行鎖的消耗。
- 意向共享鎖
- 給行加共享鎖會嘗試給表加意向共享鎖
- 意向排他鎖
- 給行加排他鎖會嘗試給表加意向排他鎖
如果沒有意向鎖,當一個事務想要給整個表加表級排他鎖(X 鎖)時,需要逐行檢查是否有行級鎖(S 鎖或 X 鎖),這在大表中會非常低效。而意向鎖通過 “預先聲明” 的方式,讓表級鎖的檢查只需判斷意向鎖的類型,無需掃描每行,大幅提升性能。
行級鎖
行級鎖粒度:
- 行鎖 對每一行數據加鎖
- 間隙鎖 區域鎖,防止其他事務數據插入導致的幻讀。
- next-key 同時給行鎖和其前面的間隙加鎖。
間隙鎖和Next-Key鎖(行級+間隙)只存在不小于RR(可重復讀)隔離級別下,用于解決幻讀,下面有論述。
行級鎖類型:
- 共享鎖
- 顯示加鎖:SELECT ... LOCK IN SHARE MODE
- 隱式加鎖:串行化隔離級別時候,讀取數據會加共享鎖,確保整個事務內讀取的數據不會變化
- 排他鎖
- 顯示加鎖:SELECT ... FOR UPDATE
- 隱式加鎖:嘗試更新數據的時候
共享鎖之間不沖突,多個事務可以對同一個表或數據加共享鎖。共享鎖和排他鎖之間沖突,A事務對一個表或數據加了共享鎖,B事務就無法再在這個表或數據上加排他鎖。顯示的加共享鎖,可以防止某些數據被其他事務更新,可以在可重復讀隔離級別下實現串行化。
不同隔離級別的上鎖邏輯
不同隔離級別的時候的上鎖邏輯不一樣的。
讀取數據
| 隔離級別 | 是否默認加S鎖 | 顯式加鎖方式(強制加鎖) | 主要目的 |
|---|---|---|---|
| READ UNCOMMITTED | 否 | 無(通常無需) | 允許臟讀,追求極致并發 |
| READ COMMITTED | 否 | SELECT ... FOR SHARE |
避免臟讀,不阻塞讀寫 |
| REPEATABLE READ | 否(依賴 MVCC) | SELECT ... FOR SHARE |
默認無鎖,顯式加鎖用于特殊場景 |
| SERIALIZABLE | 是(隱式加 S 鎖) | 無需顯式,自動加鎖 | 完全串行化,保證最高一致性 |
修改數據
| 隔離級別 | 是否加行鎖(X 鎖) | 是否加間隙鎖 / Next-Key Lock | 主要目的 |
|---|---|---|---|
| READ UNCOMMITTED | 是 | 否 | 防止并發修改沖突 |
| READ COMMITTED | 是 | 否 | 防止并發修改沖突 |
| REPEATABLE READ | 是 | 可能(范圍 / 未命中時) | 防止并發修改 + 幻讀 |
| SERIALIZABLE | 是 | 是(范圍 / 未命中時) | 最高一致性,徹底防幻讀 |
MVCC
MVCC 是 InnoDB 存儲引擎實現非阻塞讀的關鍵機制,其核心思想是:為數據維護多個版本,事務讀取時無需加鎖,而是通過 “版本鏈” 找到符合自身可見性規則的數據版本,從而避免讀操作阻塞寫操作(反之亦然),提升并發性能。
MVCC 在不同隔離級別的表現
- 讀已提交(RC)隔離級別
MVCC 生效:讀操作(SELECT)會通過 MVCC 讀取 “已提交的最新版本” 數據。
具體行為:
- 每次執行 SELECT 時,都會生成一個新的 Read View(讀視圖,用于判斷數據版本的可見性)。
- 因此,同一事務中兩次執行相同的 SELECT,可能讀到其他事務已提交的新數據(即 “不可重復讀”)。
- 例如:事務 A 第一次查詢某行值為 1,事務 B 修改為 2 并提交,事務 A 再次查詢會讀到 2。
- MVCC 的作用:避免讀操作加鎖,同時保證不會讀到未提交的臟數據(符合 “讀已提交” 的要求)。
- 可重復讀(RR)隔離級別
MVCC 生效:讀操作通過 MVCC 讀取 “事務啟動時的一致性版本” 數據。
具體行為:
- 事務啟動時生成一個 Read View,并在整個事務期間復用該視圖,不再重新生成。
- 因此,同一事務中多次執行 SELECT 會讀到相同的數據(即使其他事務已提交修改),實現 “可重復讀”。
- 例如:事務 A 啟動時查詢某行值為 1,事務 B 修改為 2 并提交,事務 A 再次查詢仍讀到 1。
- MVCC 的作用:在無鎖的情況下,保證事務內讀取數據的一致性,同時避免 “不可重復讀”。
不同隔離級別的實現原理
| 事務級別 | 描述 | 實現 |
|---|---|---|
| READ UNCOMMITTED | 能夠讀取到其他事務未提交的數據 | 直接讀取數據的最新版本。 |
| READ COMMITTED | 不能讀取到未提交數據,同一事務里多次讀取可能查詢的數據不一樣 | 通過MVCC快照實現,排除掉正在執行的事務,讀取當前事務之前提交的版本。但是重新讀取的時候會重新生成readview,所以會讀取到已提交的數據。 |
| REPEATABLE READ | 同一事務下每次讀取的數據保持一致 | 讀取的時候通過mvvc創建一個readview,再次讀取的時候使用之前readview,所以不會查詢到事務執行期間提交的數據。特殊情況下使用間隙鎖保證,下面有描述。 |
| SERIALIZABLE | 同一事務下每次讀取的數據保持一致 | 通過select加讀鎖保證數據不被修改。 |
事務隔離級別是用于控制不同事務級別下同一事務讀取數據的邏輯。更新數據的時候因為需要更新最新版本的數據,無法使用MVCC,還是需要靠鎖進行數據隔離。
MVCC和間隙鎖
MVCC已經能解決幻讀的問題了,為什么還要有間隙鎖。具體場景分析:
假設表user的id(主鍵)存在記錄10、20,初始數據如下:
| id | name |
|---|---|
| 10 | Alice |
| 20 | Bob |
步驟 1:事務 A 啟動,執行快照讀(普通 SELECT)
事務 A 開始后,執行第一次范圍查詢(快照讀,依賴 MVCC):
sql
-- 事務A
START TRANSACTION;
-- 快照讀:基于MVCC的Read View,只能看到事務啟動前已提交的數據
SELECT * FROM user WHERE id BETWEEN 10 AND 20;
-- 結果:僅能看到id=10和id=20的記錄
步驟 2:事務 B 插入新記錄并提交
此時事務 B 插入一條在[10,20]范圍內的新記錄,并提交:
sql
-- 事務B
START TRANSACTION;
INSERT INTO user (id, name) VALUES (15, 'Charlie'); -- 插入新記錄
COMMIT;
由于沒有間隙鎖,事務 B 的插入操作不會被阻塞(間隙鎖的作用就是阻止這種插入)。
步驟 3:事務 A 執行當前讀(如 UPDATE / 加鎖查詢)
事務 A 接著執行一個 “當前讀” 操作(如更新或加鎖查詢,會讀取最新數據,而非 MVCC 快照):
sql
-- 事務A
-- 當前讀:讀取最新數據,而非快照
UPDATE user SET name = 'Updated' WHERE id BETWEEN 10 AND 20;
-- 此時會更新id=10、20(原有記錄)和id=15(事務B插入的新記錄)
步驟 4:事務 A 再次執行快照讀,出現幻讀
事務 A 再次執行快照讀時:
sql
-- 事務A
SELECT * FROM user WHERE id BETWEEN 10 AND 20;
此時結果會包含id=15的記錄(因為事務 A 自己更新過這條記錄,MVCC 規則中 “事務可以看到自己修改的內容”)。
但這條記錄在事務 A 第一次查詢時并不存在,因此出現了 “幻讀”—— 同一事務內,兩次相同的范圍查詢,第二次出現了第一次未見過的新記錄。

浙公網安備 33010602011771號