MySQL -- 鎖
- 全域性鎖:對整個 資料庫例項 加鎖
- 加 全域性讀鎖 :
FLUSH TABLES WITH READ LOCK
,阻塞其他執行緒的下列語句- 資料更新語句 (增刪改)
- 資料定義語句 (建表、修改表結構)
- 更新類事務的提交語句
- 主動解鎖:
UNLOCK TABLES
- 典型使用場景: 全庫邏輯備份
- 把整庫每個表都 SELECT 出來,然後存成 文字
- 缺點
- 如果在 主庫 上執行 邏輯備份 ,備份期間 不能執行更新操作 ,導致 業務停擺
- 如果在 備庫 上執行 邏輯備份 ,備份期間從庫 不能執行由主庫同步過來的binlog ,導致 主從延時
- 備份加全域性鎖的必要性
- 保證 全域性檢視 是 邏輯一致 的
mysqldump
-
--single-transaction
- 導資料之前 啟動一個事務 ,確保拿到 一致性檢視
- 由於 MVCC 的支援,在這個過程中是可以 正常更新資料 的
- 需要 儲存引擎 支援 RR的事務隔離級別
- MyISAM不支援事務,如果備份過程中有更新,總是能取到最新的資料,破壞了備份的一致性
- 因此MyISAM只能依賴於
FLUSH TABLES WITH READ LOCK
,不能使用--single-transaction
- 針對 全庫邏輯備份 的場景,
--single-transaction
只適用於 所有的表都使用了事務引擎的庫FLUSH TABLES WITH READ LOCK
- 在邏輯備份時,如果全部庫 都使用InnoDB ,建議使用
--single-transaction
引數,對應用更加友好
邏輯備份 + DDL
在 備庫 用 --single-transaction
做 邏輯備份 的過程中,由 主庫的binlog 傳來了一個針對小表 t1
的 DDL 語句
備份關鍵語句
# 備份過程中的關鍵語句 Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT; /* other tables */ Q3:SAVEPOINT sp; /* 時刻 1 */ Q4:SHOW CREATE TABLE `t1`; /* 時刻 2 */ Q5:SELECT * FROM `t1`; /* 時刻 3 */ Q6:ROLLBACK TO SAVEPOINT sp; /* 時刻 4 */ /* other tables */
- 備份開始時,為了確保 RR 的隔離級別,再設定一次(Q1)
- 啟動事務,用
WITH CONSISTENT SNAPSHOT
確保可以得到一個 一致性檢視 (Q2) - 設定一個儲存點(Q3)
-
SHOW CREATE TABLE
是為了拿到 表結構 (Q4) -
SELECT * FROM
是正式 導資料 (Q5) -
ROLLBACK TO SAVEPOINT
的作用是 釋放t1的MDL鎖 (Q6)
DDL到達時刻
- 時刻1:備份拿到的是 DDL後 的表結構
- 現象為 無影響
- 時刻2:Q5執行時,報異常:
Table definition has changed, please retry transaction
- 現象為 mysqldump終止
- 在時刻2~時刻3之間( 導資料期間 ):mysqldump佔據著t1的 MDL讀鎖 ,因此 binlog會被阻塞 ,直到Q6結束
- 現象為 主從延時
- 時刻4:mysqldump釋放 MDL讀鎖 ,備份拿到的是 DDL前 的表結構
- 現象為 無影響
readonly
-
SET GLOBAL READONLY=true
也能讓全庫進入只讀狀態,推薦使用FLUSH TABLES WITH READ LOCK
- 在有些系統中,
readonly
的值會被用來做其他邏輯,因此修改global
變數的方式 影響面會比較大 - 異常處理機制不同
- 執行
FLUSH TABLES WITH READ LOCK
命令後,客戶端發生異常,MySQL會 自動釋放全域性鎖 - 執行
SET GLOBAL READONLY=true
命令後,客戶端發生異常,MySQL會 一直保持readonly狀態
- 執行
表級鎖
表鎖
- 表鎖:
LOCK TABLES ... READ/WRITE
- 解鎖
UNLOCK TABLES
-
LOCK TABLES
除了會限制 其他執行緒 的讀寫外,也會限制 本執行緒 接下來的操作- 執行緒A執行
LOCK TABLES t1 READ, t2 WRITE
,線上程A執行UNLOCK TABLES
之前 - 其他執行緒允許的操作: 讀t1
- 執行緒A允許的操作: 讀t1 , 讀寫t2 ,同樣 不允許寫t1
- 執行緒A執行
- InnoDB支援 行鎖 ,所以一般不使用
LOCK TABLES
來進行併發控制
元資料鎖(MDL)
- MDL是 隱式使用 的,在 訪問一個表 的時候會被 自動加上
- MDL的作用: 保證讀寫的正確性 ,從 MySQL 5.5 引入
- 防止DDL與DML的併發衝突
- MDL讀鎖 + MDL寫鎖
- 對一個表做 增刪改查 操作( DML )的時候,加 MDL讀鎖
- 對 表結構 做 變更 操作( DDL )的時候,加 MDL寫鎖
- 關係
- 讀鎖之間不互斥 :多執行緒可以併發對同一張表進行增刪改查
- 讀寫鎖之間,寫鎖之間互斥 :用於保證變更表結構操作的安全性
加欄位的問題
- session A先啟動,對錶t加上一個 MDL讀鎖
- session B需要的也是 MDL讀鎖 ,不互斥,可以正常執行
- session C需要的是 MDL寫鎖 ,session A的事務 還未提交 , 持有的MDL讀鎖還未釋放 ,session C會被阻塞
- session D只需要申請 MDL讀鎖 ,但同樣會 被session C阻塞
- 所有對錶的增刪改查都需要先 申請MDL讀鎖 ,此時表現為 完全不可讀寫
- 如果該表上的 查詢比較頻繁 ,而且 客戶端 恰好有 重試機制 (超時後再起一個session去請求)
- 那麼 資料庫的執行緒 很快就會被 佔滿
- 事務中的 MDL鎖 ,在 語句開始執行時申請 ,但會等到 整個事務提交後再釋放
解決辦法
- 首先要解決 長事務 的影響,因為只要事務不提交,就會一直佔用相關的MDL鎖
-
INFORMATION_SCHEMA.INNODB_TRX
中的trx_started
欄位 - 在做 DDL 變更之前,首先 確認是否長事務在執行 ,如果有則先 kill 掉這個長事務
-
- 如果需要執行DDL的表是 熱點表 , 請求很頻繁 ,kill長事務未必管用,因為很快就會有新的請求
-
ALTER TALE
語句設定 等待時間 ,就算拿不到 MDL寫鎖 也不至於 長時間阻塞後面的業務語句 - 目前
MariaDB
和AliSQL
支援該功能
-
ALTER TABLE T [WAIT [n]|NO_WAIT] ADD f INT
關係
- 執行緒A在MyISAM表上更新一行資料,那麼會加 MDL讀鎖 和 表的寫鎖
- 執行緒B在同一個MyISAM表上更新另外一行資料,那麼也會加 MDL讀鎖 和 表的寫鎖
- 執行緒B加 MDL讀鎖 成功,但加 表的寫鎖 失敗
- 表現:執行緒B被執行緒A阻塞
- 引申:如果有多種鎖,必須 全部鎖不互斥 才能 並行 ,只要有一個鎖互斥,就得等
行鎖
- MySQL的行鎖是在 儲存引擎層 實現的
- MyISAM不支援行鎖,而InnoDB支援行鎖,這是InnoDB替代MyISAM的一個重要原因
兩階段鎖
id為表t的主鍵,事務B的update語句會被阻塞,直到事務A執行commit之後,事務B才能繼續執行

- 兩階段鎖
- 在InnoDB事務中,行鎖是在 需要的時候 加上
- 但並不是在不需要了就立刻釋放,而是要等待 事務結束 後才釋放
- 如果事務需要 鎖定多行 ,要就把最可能 造成鎖衝突 和 影響併發度 的鎖儘可能 往後放
電影票業務
- 顧客A在電影院B購買電影票,涉及以下操作(同一事務)
- update:從顧客A的賬戶餘額中扣除電影票價
- update:給電影院B的賬戶餘額增加電影票價
- insert:記錄一條交易日誌
- 假設此時顧客C也要在電影院B買票的,兩個事務衝突的部分就是第2個語句(同一個電影院賬戶)
- 所有操作所需要的行鎖都是在事務結束的時候才會釋放
- 將第2個語句放在最後,能最大程度地 減少事務之間的鎖等待 , 提升併發度
死鎖
假設電影院做活動,在活動開始的時候,CPU消耗接近100%,但整個庫每秒執行不到100個事務

- 事務A在等待事務B釋放id=2的行鎖,事務B在等待事務A釋放id=1的行鎖,導致 死鎖
- 當出現死鎖後,有2種處理策略
- 等待 ,直至超時(不推薦)
- 業務有損 :業務會出現大量超時
- 死鎖檢測 (推薦)
- 業務無損 :業務設計不會將死鎖當成嚴重錯誤,當出現死鎖時可採取: 事務回滾+業務重試
- 等待 ,直至超時(不推薦)
等待
- 由引數
innodb_lock_wait_timeout
控制(MySQL 5.7.15引入) - 預設是50s,對於 線上服務 來說是無法接受的
- 但也 不能設定成很小的值 ,因為如果實際上並不是死鎖,只是簡單的鎖等待,會出現很多 誤傷
死鎖檢測(推薦)
- 發現死鎖後, 主動回滾鎖鏈條中的某一事務 ,讓其他事務繼續執行
- 需要設定引數
innodb_deadlock_detect
- 需要設定引數
- 觸發死鎖檢測: 要加鎖訪問的行上有鎖
- 一致性讀不會加鎖
- 死鎖檢測並 不需要掃描所有事務
- 某個時刻,事務等待狀態為:事務B等待事務A,事務D等待事務C
- 新來事務E,事務E需要等待D,那麼只會判斷事務CDE是否會形成死鎖
- CPU消耗高
- 每個新來的執行緒發現自己 要加鎖訪問的行上有鎖
- 會去判斷會不會 由於自己的加入而導致死鎖 ,總體時間複雜度為
O(N^2)
- 會去判斷會不會 由於自己的加入而導致死鎖 ,總體時間複雜度為
- 假設有1000個併發執行緒,最壞情況下死鎖檢測的操作量級為100W(1000^2)
- 每個新來的執行緒發現自己 要加鎖訪問的行上有鎖
- 解決方法
- 如果業務能確保一定不會出現死鎖,可以 臨時關閉死鎖檢測 ,但存在一定的風險(超時)
- 控制併發度,如果併發下降,那麼死鎖檢測的成本就會降低,這需要在 資料庫服務端 實現
- 如果有 中介軟體 ,可以在中介軟體實現
- 如果能修改 MySQL原始碼 ,可以在MySQL內部實現
- 設計上的優化
- 將一行改成 邏輯上的多行 來 減少鎖衝突
mysql> SHOW VARIABLES LIKE '%innodb_deadlock_detect%'; +------------------------+-------+ | Variable_name| Value | +------------------------+-------+ | innodb_deadlock_detect | ON| +------------------------+-------+ mysql> SHOW VARIABLES LIKE '%innodb_lock_wait_timeout%'; +--------------------------+-------+ | Variable_name| Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 30| +--------------------------+-------+
更新無鎖引欄位
# name欄位沒有索引 UPDATE t SET t.name='abc' WHERE t.name='cde'
InnoDB內部會根據 聚簇索引 , 逐行掃描,逐行加鎖,事務提交後統一釋放鎖
參考資料
《MySQL實戰45講》
轉載請註明出處:http://zhongmingmao.me/2019/01/25/mysql-lock/
訪問原文「MySQL -- 鎖」獲取最佳閱讀體驗並參與討論