MySQL 鎖(InnoDB Locking)

MySQL 的鎖.png

一、屬性鎖:Shared and Exclusive Locks

1.1 簡(jiǎn)介

shared locks 是共享鎖,簡(jiǎn)稱 S 鎖,exclusive locks 是排它鎖,簡(jiǎn)稱 X 鎖,它們既可以是表級(jí)鎖,也可以是行級(jí)鎖,在 MySQL 的 InnoDB 引擎中是行級(jí)鎖,可以加在一行或者多行上,那么何時(shí)在一行上加鎖,何時(shí)在多行上加鎖,這需要根據(jù)索引情況而定

shared locks 允許持有某行 S 鎖的事務(wù)讀?。╯elect)該行,exclusive locks 允許持有某行 X 鎖的事務(wù)更新(update)和刪除(delete)該行

InnoDB 支持通過(guò)特定的語(yǔ)句進(jìn)行顯式加鎖:

  • 顯式加 X 鎖:select ... for update
  • 顯式加 S 鎖:select ... lock in share mode

1.2 S 鎖和 X 鎖的兼容性

共享鎖和排它鎖的兼容性列表如下表,該表表示:

  • 如果事務(wù) T1 獲得了行 r 的 S 鎖,另一個(gè)事務(wù) T2 可以獲取行 r 的 S 鎖,但是不能獲取行 r 的 X 鎖。即 S 鎖可以被多個(gè)事務(wù)共享,所以稱為共享鎖
  • 如果事務(wù) T1 獲得了行 r 的 X 鎖,另一個(gè)事務(wù) T2 既不能獲取行 r 的 S 鎖,也不能獲取行 r 的 X 鎖,必須等待 T1 釋放 X 鎖,故稱為排他鎖
X S
X 不兼容 不兼容
S 不兼容 兼容

1.3 S 鎖和 X 鎖的兼容性示例

1.3.1 創(chuàng)建測(cè)試表

-- ----------------------------
-- Table structure for test
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `xid` int(11) NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of test
-- ----------------------------
INSERT INTO `test`(xid, name) VALUES (1, '1');
INSERT INTO `test`(xid, name) VALUES (5, '5');
INSERT INTO `test`(xid, name) VALUES (9, '9');

1.3.2 S 鎖兼容性測(cè)試

分別打開兩個(gè)會(huì)話(會(huì)話 A 和會(huì)話 B),在會(huì)話 A 中開啟一個(gè)事務(wù)并執(zhí)行:

select * from test where xid = 1 lock in share mode;

在會(huì)話 B 中開啟一個(gè)事務(wù)并執(zhí)行:

select * from test where xid = 1 lock in share mode;
select * from test where xid = 1 for update;

效果如下:

1.3.3 X 鎖兼容性測(cè)試

分別打開兩個(gè)會(huì)話(會(huì)話 A 和會(huì)話 B),在會(huì)話 A 中開啟一個(gè)事務(wù)并執(zhí)行:

select * from test where xid = 1 for update;

在會(huì)話 B 中開啟一個(gè)事務(wù)并執(zhí)行:

select * from test where xid = 1 lock in share mode;
select * from test where xid = 1 for update;

效果如下:

二、狀態(tài)鎖:Intention Locks

Intention Locks 稱為意向鎖,它是表級(jí)鎖,顧名思義,它是用來(lái)鎖定表的,與行級(jí)鎖相對(duì)應(yīng)

如果事務(wù) T1 獲取了一個(gè)表的 intention exclusive 鎖(簡(jiǎn)稱 IX 鎖),相當(dāng)于表級(jí)別的排它鎖,那么事務(wù) T2 就不能再獲取表上的 S 和 X 鎖了;

如果事務(wù) T1 獲取了一個(gè)表的 intention shared 鎖(簡(jiǎn)稱 IS 鎖),那么事務(wù) T2 可以獲取表的 S 鎖,但不能獲取表的 X 鎖,它與共享鎖和排它鎖的關(guān)系如下:

(1)一個(gè)事務(wù)獲取一張表中某行的 S 鎖之前,必須獲取表的 IS 鎖或者更強(qiáng)的鎖(比如 IX);

(2)一個(gè)事務(wù)獲取一張表中某行的 X 鎖之前,必須獲取表的 IX 鎖;

表級(jí)鎖的兼容性如下:

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

當(dāng)前事務(wù) T1 想要修改某張表的一些行,那么首先要獲取該表的 IX 鎖,然后在要修改的行上加上 X 鎖,另一個(gè)事務(wù) T2 也準(zhǔn)備要修改該表的一些行,因?yàn)楸碇谐吮划?dāng)前事務(wù) T1 加鎖的行,其他行是可以修改的,所以 T2 可以獲取該表的 IX 鎖,然后在其他行添加 X 鎖,但是如果要修改當(dāng)前事務(wù) T1 加鎖的行就需要等待了

三、算法鎖

InnoDB 有三種鎖行算法:

  1. Record Lock:?jiǎn)蝹€(gè)行記錄上的鎖
  2. Gap Lock:間隙鎖,鎖定一個(gè)范圍,但不包括記錄本身。GAP 鎖的目的,是為了防止同一事務(wù)的兩次當(dāng)前讀,出現(xiàn)幻讀的情況
  3. Next-Key Lock:Record Lock 和 Gap Lock 的結(jié)合。鎖定一個(gè)范圍,并且鎖定記錄本身。對(duì)于行的查詢,都是采用該方法,主要目的是解決幻讀的問(wèn)題

3.1 for update 簡(jiǎn)介

for update 可以顯式地為表中滿足條件的行加 X 鎖。當(dāng)一個(gè)事務(wù)的操作未完成時(shí)候,其他事務(wù)可以讀取該行,但不能更新或刪除該行

使用場(chǎng)景:高并發(fā)并且對(duì)于數(shù)據(jù)的準(zhǔn)確性很有要求的場(chǎng)景。例如涉及到金錢、庫(kù)存等。一般這些操作都是很長(zhǎng)一串并且是開啟事務(wù)的。如果庫(kù)存剛開始讀的時(shí)候是 1,而立馬另一個(gè)進(jìn)程進(jìn)行了 update 將庫(kù)存更新為 0 了,而事務(wù)還沒(méi)有結(jié)束,會(huì)將錯(cuò)的數(shù)據(jù)一直執(zhí)行下去,就會(huì)有問(wèn)題。所以需要 for upate 進(jìn)行數(shù)據(jù)加鎖防止高并發(fā)時(shí)候數(shù)據(jù)出錯(cuò)

InnoDB 行鎖是通過(guò)給索引項(xiàng)加鎖來(lái)實(shí)現(xiàn)的,如果沒(méi)有索引,InnoDB 將通過(guò)隱藏的聚簇索引來(lái)對(duì)記錄加鎖

3.2 for update 中算法鎖的使用

場(chǎng)景一:明確指定索引,并且有此記錄,加 Next-Key Lock

select * from test where xid = 5 for update;

插入操作

此時(shí)鎖住的范圍除了 ② 之外,還會(huì)鎖住下一個(gè)范圍,即 ③,這就是所謂的 Next-Key Lock。因此不能在另一個(gè)會(huì)話中插入 xid 在 1~9 范圍內(nèi)的數(shù)據(jù)。xid = 1 的數(shù)據(jù)不能插入,但 xid = 9 的數(shù)據(jù)可以插入,這可能與 B-Tree 索引的順序插入有關(guān)。我們可以看下圖描述的聚簇索引(自增 id)和 xid 的索引結(jié)構(gòu)圖:

個(gè)人理解(僅供參考):xid = 1 的數(shù)據(jù)不能插入,因?yàn)橹麈I索引 id 是自增的,因此在 id=2 這條記錄之前,是不允許插入一條 xid=5 的記錄,這樣就破壞了主鍵索引 id 的有序性;xid = 9 的數(shù)據(jù)可以插入可能是因?yàn)?MySQL允許在 (id = 3, xid = 9) 的后面插入一條 (id = 4, xid = 9) 的數(shù)據(jù),并不破壞主鍵索引的有序性

INSERT INTO `test`(xid, name) VALUES (1, '1');
INSERT INTO `test`(xid, name) VALUES (4, '4');
INSERT INTO `test`(xid, name) VALUES (6, '6');
INSERT INTO `test`(xid, name) VALUES (9, '9');
INSERT INTO `test`(xid, name) VALUES (15, '15');

讀取、更新和刪除操作

事務(wù) A 執(zhí)行 select * from test where xid = 5 for update; 將 xid = 5 這一行鎖定后,事務(wù) B 可以執(zhí)行 select,但不能執(zhí)行 update 和 delete

select * from test where xid = 5;
update test set name = '5-1' where xid = 5;
delete from test where xid = 5;

事務(wù) B 對(duì)其他行的操作(select、update 和 delete)則不受影響

場(chǎng)景二:使用范圍條件而不是相等條件檢索數(shù)據(jù)時(shí),InnoDB 會(huì)給滿足條件的索引行加鎖,對(duì)于索引值在條件范圍但不存在的行記錄加 Gap Lock

select * from test where xid > 7 for update;

插入操作

此時(shí)針對(duì)索引 xid 使用的范圍查找,會(huì)鎖住 xid = 9 的行以及 ③、 ④ 。因此除了可以插入 xid = 3 的數(shù)據(jù)外,其余插入語(yǔ)句均不能執(zhí)行

INSERT INTO `test`(xid, name) VALUES (3, '3');
INSERT INTO `test`(xid, name) VALUES (6, '6');
INSERT INTO `test`(xid, name) VALUES (8, '8');
INSERT INTO `test`(xid, name) VALUES (15, '15');

讀取、更新和刪除操作

事務(wù) A 執(zhí)行 select * from test where xid > 7 for update; 將 xid = 9 這一行鎖定后,事務(wù) B 可以執(zhí)行 select,但不能執(zhí)行 update 和 delete

select * from test where xid = 9;
update test set name = '9-1' where xid = 9;
delete from test where xid = 9;

事務(wù) B 對(duì)其他不滿足 xid > 7 的行的操作(select、update 和 delete)則不受影響

場(chǎng)景三:明確指定索引,若查無(wú)此記錄,加 Gap Lock

select * from test where xid = 7 for update;

此時(shí)加鎖區(qū)間是 ③ ,因此除了 xid = 6 和 xid = 8 無(wú)法插入外,其余均可執(zhí)行

INSERT INTO `test`(xid, name) VALUES (3, '3');
INSERT INTO `test`(xid, name) VALUES (6, '6');
INSERT INTO `test`(xid, name) VALUES (8, '8');
INSERT INTO `test`(xid, name) VALUES (15, '15');

讀取、更新和刪除操作

Gap Lock 主要是為了防止其他事務(wù)在鎖定范圍內(nèi)插入數(shù)據(jù),不影響其他事務(wù)操作其他行數(shù)據(jù)

場(chǎng)景四:當(dāng)查詢的索引含有唯一屬性(主鍵或唯一索引)的時(shí)候,Next-Key Lock 會(huì)進(jìn)行優(yōu)化,將其降級(jí)為 Record Lock,即僅鎖住索引本身,不是范圍

將 xid 改為 unique index:

ALTER TABLE `db_zll`.`test` 
DROP INDEX `idx_xid`,
ADD UNIQUE INDEX `idx_xid`(`xid`) USING BTREE;
select * from test where xid = 5 for update;

事務(wù) A 執(zhí)行 select * from test where xid = 5 for update; 只鎖住 xid = 5 這一行,因此事務(wù) B 對(duì) xid = 5 的行只能 select,事務(wù) B 對(duì)其他行的操作不受影響

INSERT INTO `test`(xid, name) VALUES (3, '3');
INSERT INTO `test`(xid, name) VALUES (5, '5');
INSERT INTO `test`(xid, name) VALUES (7, '7');

行鎖失效場(chǎng)景:

  • 未指定主鍵/索引,并且有此記錄,表級(jí)鎖
  • 無(wú)主鍵/索引,表級(jí)鎖
  • 主鍵/索引不明確,表級(jí)鎖,例如 where xid like 、where xid <> 等操作

3.3 for update 超時(shí)回滾

超時(shí)時(shí)間的參數(shù):innodb_lock_wait_timeout ,默認(rèn)是50秒
超時(shí)是否回滾參數(shù):innodb_rollback_on_timeout 默認(rèn)是OFF

默認(rèn)情況下,InnoDB 存儲(chǔ)引擎不會(huì)回滾超時(shí)引發(fā)的異常,除死鎖外。當(dāng)參數(shù) innodb_rollback_on_timeout 設(shè)置成 ON 時(shí),則可以回滾

3.4 for update 注意點(diǎn)

  1. for update 僅適用于 InnoDB,并且必須開啟事務(wù),在 begin 與 commit 之間才生效

  2. 當(dāng)開啟一個(gè)事務(wù)進(jìn)行 for update 的時(shí)候,另一個(gè)事務(wù)也有 for update 的時(shí)候會(huì)一直等待,直到第一個(gè)事務(wù)結(jié)束嗎?

    答:會(huì)的。除非第一個(gè)事務(wù) commit 或者 rollback 或者斷開連接,第二個(gè)事務(wù)會(huì)立馬拿到鎖進(jìn)行后面操作。不過(guò)也可以設(shè)置鎖等待超時(shí)參數(shù)innodb_lock_wait_timeout 來(lái)解決

  3. 如果沒(méi)查到記錄會(huì)加鎖嗎?

    答:會(huì)的。有主鍵/索引產(chǎn)生間隙鎖,無(wú)主鍵/索引產(chǎn)生表鎖表級(jí)鎖

  4. for update 和 for update nowait 區(qū)別(前者阻塞其他事務(wù),后者拒絕其他事務(wù))

    for update 鎖住表或者鎖住行,只允許當(dāng)前事務(wù)進(jìn)行操作(讀寫),其他事務(wù)被阻塞,直到當(dāng)前事務(wù)提交或者回滾,被阻塞的事務(wù)自動(dòng)執(zhí)行 for update nowait 鎖住表或者鎖住行,只允許當(dāng)前事務(wù)進(jìn)行操作(讀寫),其他事務(wù)被拒絕,事務(wù)占據(jù)的 statement 連接也會(huì)被斷開

行鎖分析

show status like 'innodb_row_lock%';

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+
5 rows in set (0.06 sec)
  • Innodb_row_lock_current_waits:當(dāng)前正在等待鎖定的數(shù)量
  • Innodb_row_lock_time:從系統(tǒng)啟動(dòng)到現(xiàn)在鎖定的時(shí)長(zhǎng)
  • Innodb_row_lock_time_avg:每次等待鎖所花平均時(shí)間
  • Innodb_row_lock_time_max:從系統(tǒng)啟動(dòng)到現(xiàn)在鎖等待最長(zhǎng)的一次所花的時(shí)間
  • Innodb_row_lock_waits:系統(tǒng)啟動(dòng)后到現(xiàn)在總共等待鎖的次數(shù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容