MySQL中的八種鎖!你知道幾種?
前言
在雙11期間,支付寶數(shù)據(jù)庫集群每秒處理25萬筆交易,而支撐這一切的核心技術(shù)之一就是MySQL的鎖機(jī)制。
很多小伙伴在工作中都遇到過這樣的場景:
- 凌晨批量處理數(shù)據(jù)時(shí)系統(tǒng)突然卡死。
- 高并發(fā)場景下出現(xiàn)詭異的死鎖報(bào)錯(cuò)。
- 明明只更新一行卻導(dǎo)致全表阻塞。
這篇文章跟大家一起聊聊MySQL的8種鎖,希望對(duì)你會(huì)有所幫助。
一、鎖的本質(zhì):并發(fā)控制的基石
1.為什么需要鎖?
當(dāng)多個(gè)事務(wù)同時(shí)操作同一數(shù)據(jù)時(shí),可能引發(fā):
- 臟讀:讀到未提交的數(shù)據(jù)
- 不可重復(fù)讀:同事務(wù)內(nèi)兩次讀取結(jié)果不同
- 幻讀:同條件查詢出現(xiàn)新記錄
鎖的作用:通過對(duì)數(shù)據(jù)資源加鎖,實(shí)現(xiàn)事務(wù)的隔離性(ACID中的"I")
二、鎖的分類全景圖
1.按粒度劃分
按粒度劃分為:
- 表鎖
- 頁鎖
- 行鎖
2.按模式劃分
鎖類型 | 共享性 | 典型場景 |
共享鎖(S) | 可共享 | SELECT ... LOCK IN SHARE MODE |
排他鎖(X) | 獨(dú)占 | UPDATE/DELETE/INSERT |
意向共享鎖(IS) | 表級(jí)標(biāo)記 | 準(zhǔn)備加行級(jí)S鎖前 |
意向排他鎖(IX) | 表級(jí)標(biāo)記 | 準(zhǔn)備加行級(jí)X鎖前 |
三、行級(jí)鎖:高并發(fā)的核心戰(zhàn)場
1.記錄鎖(Record Lock)
鎖定索引記錄:
-- 事務(wù)A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 對(duì)id=1加X鎖
-- 事務(wù)B(將被阻塞)
UPDATE users SET name = 'Tom' WHERE id = 1;
底層實(shí)現(xiàn):
2.間隙鎖(Gap Lock)
鎖定索引區(qū)間(解決幻讀):
假設(shè)當(dāng)前表結(jié)構(gòu):id主鍵(當(dāng)前有id=1,5,10)
BEGIN;
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
-- 阻塞所有[5,10]區(qū)間的插入
INSERT INTO users(id) VALUES(6); -- 被阻塞!
INSERT INTO users(id) VALUES(11); -- 成功
鎖定范圍:
3.臨鍵鎖(Next-Key Lock)
記錄鎖+間隙鎖組合:
假設(shè)當(dāng)前數(shù)據(jù)庫隔離級(jí)別是RR(Repeatable Read):
BEGIN;
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 阻塞操作
UPDATE users SET name='A' WHERE id=10; -- 記錄鎖阻塞
INSERT INTO users(id) VALUES(6); -- 間隙鎖阻塞
鎖范圍示意圖:
四、表級(jí)鎖:全表掃描的保護(hù)傘
1.表鎖(Table Lock)
顯式加鎖:
LOCK TABLES users WRITE; -- 獲取寫鎖
-- 執(zhí)行更新...
UNLOCK TABLES;
隱式加鎖(DDL操作自動(dòng)加鎖):
ALTER TABLE users ADD COLUMN age INT; -- 自動(dòng)加表級(jí)X鎖
2.元數(shù)據(jù)鎖(MDL)
保護(hù)表結(jié)構(gòu):
-- 會(huì)話A
BEGIN;
SELECT * FROM users; -- 獲取MDL讀鎖
-- 會(huì)話B(被阻塞)
ALTER TABLE users ADD COLUMN email VARCHAR(255);
等待鏈:
五、死鎖:高并發(fā)的終極挑戰(zhàn)
1.經(jīng)典死鎖場景
-- 事務(wù)A
BEGIN;
UPDATE accounts SET balance = balance - 100WHEREid = 1;
UPDATE accounts SET balance = balance + 100WHEREid = 2;
-- 事務(wù)B(反向操作)
BEGIN;
UPDATE accounts SET balance = balance - 100WHEREid = 2;
UPDATE accounts SET balance = balance + 100WHEREid = 1;
死鎖形成過程:
2.死鎖檢測與解決
自動(dòng)檢測:
SHOW ENGINE INNODB STATUS;
-- 查看LATEST DETECTED DEADLOCK
手動(dòng)處理:
// Spring事務(wù)重試
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100))
@Transactional
public void transferMoney(Long from, Long to, BigDecimal amount) {
// 轉(zhuǎn)賬邏輯
}
六、鎖監(jiān)控與優(yōu)化實(shí)戰(zhàn)
1.鎖等待分析
-- 查看鎖等待
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
2.索引優(yōu)化避免全表鎖
問題SQL:
UPDATE users SET status=1 WHERE name LIKE 'A%'; -- 無索引導(dǎo)致表鎖
優(yōu)化方案:
ALTER TABLE users ADD INDEX idx_name(name); -- 創(chuàng)建索引
UPDATE users SET status=1 WHERE name LIKE 'A%'; -- 僅加行鎖
3.鎖超時(shí)配置
# my.cnf
[mysqld]
innodb_lock_wait_timeout=50 # 默認(rèn)50秒
七、不同隔離級(jí)別的鎖差異
隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 | 鎖機(jī)制 |
讀未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 不加鎖 |
讀已提交(Read Committed) | 不可能 | 可能 | 可能 | 語句級(jí)快照 |
可重復(fù)讀(Repeatable Read) | 不可能 | 不可能 | 可能(*) | 臨鍵鎖(默認(rèn)) |
串行化(Serializable) | 不可能 | 不可能 | 不可能 | 全表鎖 |
InnoDB在RR級(jí)別通過Next-Key Lock解決幻讀問題。
八、鎖機(jī)制最佳實(shí)踐
1. 鎖優(yōu)化口訣
- 一快:事務(wù)執(zhí)行要快。
- 二小:鎖粒度盡量小。
- 三避免:避免大事務(wù)、全表掃描、長等待 。
2. 不同場景鎖選擇
場景 | 推薦方案 |
精確更新單行 | 行級(jí)X鎖(WHERE主鍵) |
范圍更新 | Next-Key Lock(RR隔離級(jí)別) |
全表更新 | 分批提交+低峰期執(zhí)行 |
結(jié)構(gòu)變更 | PT-Online-Schema-Change工具 |
總結(jié)
- 鎖是雙刃劍:保護(hù)數(shù)據(jù)一致性的同時(shí)降低并發(fā)度
- 粒度決定性能:行鎖 > 頁鎖 > 表鎖
- 隔離級(jí)別是基礎(chǔ):根據(jù)業(yè)務(wù)選擇合適級(jí)別(推薦RR)
- 索引是鑰匙:80%的鎖問題可通過優(yōu)化索引解決
- 監(jiān)控是眼睛:善用
SHOW ENGINE INNODB STATUS
正如數(shù)據(jù)庫專家Michael Stonebraker所言:“The best locking strategy is no locking at all.”
最高明的鎖策略是“無鎖”,而這正是我們不斷優(yōu)化的方向。