MySQL 中鎖的概念
排它鎖(Exclusive Lock)
X 鎖,也稱為寫鎖,若事務T對對象A加上X鎖,則只允許T讀取和修改A,其他任何事物都不能再對A 加任何鎖,直到T釋放A上的鎖。
SELECT…FOR UPDATE 對讀取的行記錄加一個X鎖,其他事務不能對已鎖定的行加上任何鎖。
共享鎖(Shared Lock)
**S 鎖,**也稱為讀鎖,若事務T對數據對象A加上S鎖,則事務T可以讀A,但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。
SELECT…LOCK IN SHARE MODE對讀取的行記錄加一個S鎖,其他事務可以向被鎖定 的行加S鎖,但是如果加X鎖,則會被阻塞。
活鎖
事務T1封鎖了R,T2又請求封鎖R,于是T2等待,T3也請求封鎖R,當T1釋放了R 上的鎖,系統首先批準了T3的請求,T2繼續等待,這就是活鎖。
死鎖
事務T1封鎖了R1,T2封鎖了R2,T1又請求封鎖R2,因為T2已經封鎖了R2,于是T1等待T2釋放R2上的鎖,接著T2又申請封鎖R1,因為T1已經封鎖了R1,T2只能等待T1釋放R1上的鎖,這就是死鎖。
解決死鎖的方法
一次封鎖法 每個事務必須將所有要使用的數據全部加鎖,否則就不能執行,弊端 加大封鎖范圍,降低了并發速度。
樂觀鎖
總是假設最好的情況,在事務提交前不會對數據進行鎖定,而是在更新數據時會進行版本或時間戳的比較,以確定數據是否被其他事務修改過。如果數據沒有被修改,則允許提交;如果數據被修改,則需要進行沖突解決
悲觀鎖
總是假設最壞的情況,在整個事務過程中,假設其他事務會對數據進行修改,因此在讀取或修改數據時,會先對數據進行鎖定,以防止其他事務對數據進行干擾(排它鎖、共享鎖都是悲觀鎖,共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)
MySQL 行鎖加鎖的分析
版本使用 MySQL 8.2.0
MySQL InnoDB 中支持三種行鎖的方式:行鎖(Record Lock)、間隙鎖(Gap Lock)、臨鍵鎖(Next-Key Lock),默認加的是臨鍵鎖,但是會根據不同的查詢條件進行優化。創建一個 user 表用來測試,表中 id 是主鍵索引,name 是唯一索引,salary 是普通索引,gender 沒有索引。
id | name | salary | gender |
---|---|---|---|
10 | 惠月 | 48000 | 女 |
20 | 光濟 | 50000 | 男 |
30 | 杰霖 | 55000 | 男 |
40 | 紫妤 | 60000 | 女 |
50 | 娜溱 | 70000 | 女 |
創建表并插入數據
CREATE TABLE `user` (`id` int(11) NOT NULL,`name` varchar(255) DEFAULT NULL,`salary` int DEFAULT NULL,`gender` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,INDEX `salary`(`salary`) USING BTREE,UNIQUE INDEX `name`(`name` ASC) USING BTREE
);INSERT INTO `user` VALUES (10, '惠月', 48000, '女');
INSERT INTO `user` VALUES (20, '光濟', 50000, '男');
INSERT INTO `user` VALUES (30, '杰霖', 55000, '男');
INSERT INTO `user` VALUES (40, '紫妤', 62000, '女');
INSERT INTO `user` VALUES (50, '娜溱', 75000, '女');
加鎖情況
-- 普通的select查詢是快照讀,不加鎖
SELECT * FROM user WHERE id=30;
-- 查詢時給主鍵索引加S共享鎖時,是當前讀
SELECT * FROM user WHERE id=30 LOCK IN SHARE MODE;
-- 查詢時加 X排他鎖,為當前讀
SELECT * FROM user WHERE id=30 FOR UPDATE
執行 SELECT * FROM … FOR UPDATE 會對表加上 IX 寫意向鎖,表示有可能會對這些記錄進行寫操作,并且給記錄加一個X,REC_NOT_GAP,鎖定了該條數據。 執行SELECT * FROM … FOR SHARE 會對表加上一個 IS 讀意向鎖,并且會給記錄加一個S,REC_NOT_GAP。
主鍵索引
主索引等值查詢,數據存在的情況
事務 1
BEGIN;
SELECT * FROM user WHERE id=30 FOR UPDATE;
-- SELECT * FROM user WHERE id=40 FOR SHARE;
-- ROLLBACK
查看鎖的情況
-- mysql 8
SELECT * FROM performance_schema.data_locks;-- mysql 5.7
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
1、表鎖是意向寫鎖
2、數據加行鎖
各字段意思:
INDEX_NAME 鎖定索引的名稱 PRIMARY 說明是主鍵索引
LOCK_TYPE 鎖的類型,RECORD 行鎖 、 TABLE 表鎖
LOCK_MODE 鎖的模式,IS 讀意向鎖、IX 寫意向鎖、S 讀鎖,又稱共享鎖、X 寫鎖,又稱排它鎖、GAP 間隙鎖。
**LOCK_DATA **要鎖定的數據,當 LOCK_TYPE 是 RECORD行鎖時。當鎖在主鍵索引上時,顯示主鍵索的值。當鎖是在輔助索引上時,顯示主索引和輔助索引的值。
LOCK_MODE | LOCK_DATA | 鎖范圍 |
---|---|---|
X,REC_NOT_GAP | 40 | id=40 行鎖 |
X,GAP | 40 | id=40 間隙鎖,不包含 40(前開后開) |
X | 40 | id=40 臨鍵鎖,包含 40(前開后閉) |
事務 2
1、會對主索引 id=30 添加行鎖
BEGIN;
-- 更新會失敗,因為事務 1 對 id=30 加行鎖。
UPDATE user SET salary = 56000 WHERE id = 30;
主索引等值查詢,數據不存在的情況
事務 1
BEGIN;
SELECT * FROM user WHERE id=31 FOR UPDATE; -- 數據庫中沒有id=30的記錄
查看加鎖情況
1、表加的是意向寫鎖
2、id 加的是 GAP Lock,范圍是(30, 40)
注意
LOCK_MODE 是 X,GAP 表示間隙鎖,LOCK_DATA 是 40 表示鎖定的范圍是在 id 為 40 之前的間隙
事務 2
1、會鎖住主索引 id=31 所在的間隙
BEGIN;
-- 可以執行成功
UPDATE `bostore`.`user` SET `salary` = 56000.00 WHERE `id` = 30;
UPDATE `bostore`.`user` SET `salary` = 63000.00 WHERE `id` = 40;
-- 執行失敗
INSERT INTO `bostore`.`user` VALUES (33, '六零', 68000.00, '女');
主索引范圍查詢,前閉后開情況
事務 1
BEGIN;
SELECT * FROM user WHERE id>=30 AND id<33 FOR UPDATE;
查看鎖情況
1、 表示 IX 意向寫鎖
2、id=30 是行鎖
3、id=40 加的是 GAP Lock,范圍是(30, 40)
事務 2
BEGIN;
-- 會阻塞
UPDATE user SET salary = 57000.00 WHERE id = 30;
INSERT INTO user VALUES (35, '六零', 68000.00, '女');
INSERT INTO user VALUES (33, '合吧', 64000.00, '女');
-- 不會阻塞
UPDATE user SET salary = 63000.00 WHERE id = 40;
主索引范圍查詢,前開后閉情況
事務 1
BEGIN;
SELECT * FROM user WHERE id>30 AND id<=40 FOR UPDATE;
查看鎖情況
1、表加的是意向寫鎖
2、id=40 加 NEXT-Key Lock,范圍是(30, 40]
事務 2
BEGIN;
-- 會阻塞
UPDATE user SET salary = 63000 WHERE id = 40;
INSERT INTO user VALUES (35, '六零', 68000, '女');
INSERT INTO user VALUES (33, '合吧', 64000, '女');-- 不會阻塞
UPDATE user SET salary = 57000 WHERE id = 30;
UPDATE user SET salary = 78000 WHERE id = 50;
INSERT INTO user VALUES (53, '考拉', 84000, '女');
普通索引
普通索引(普通索引只針對表中的單一列進行索引,普通索引可以是唯一的,也可以不唯一,普通索引對于等值查詢(例如WHERE column = value)和范圍查詢(例如WHERE column > value)都能提供較好的性能提升)
普通索引等值查詢,數據存在的情況
事務 1
BEGIN;
SELECT * FROM user WHERE salary = 62000 FOR UPDATE;
查看鎖情況
1、 表加意向寫鎖 IX
2、 索引salary加臨鍵鎖,范圍是(55000, 62000]
3、 主鍵 id = 40 加行鎖
4、 索引salary加間隙鎖,范圍是(62000, 75000)
事務 2
1、 主鍵 id=40 加了行鎖,不能更新和刪除
-- 修改id=40有行鎖會阻塞
UPDATE user SET salary = 63000 WHERE id = 40;
UPDATE user SET name="紫是" where salary = 62000;
2、salary 在(55000, 62000] 范圍加了 NEXT-Key Lock(針對該范圍的 id 也會加鎖),insert 時salary 在此范圍,會阻塞
3、salary 在(62000, 75000)范圍加了 GAP Lock(避免幻讀),insert 時salary 在此范圍,會阻塞
4、 插入 salary=55000時,id<30可以不阻塞,id>30會阻塞
-- id=40 salary=62000 之前有臨鍵鎖(55000, 62000]
-- 是對salary的鎖,但是整個區間都會被鎖住包括主索引id
INSERT INTO user VALUES (35, '六零', 55000, '女'); -- 阻塞
INSERT INTO user VALUES (51, '哈西', 55000, '女'); -- 阻塞
-- id=30 salary=55000 之前沒有間隙鎖
INSERT INTO user VALUES (29, '湖西', 55000, '女'); -- 不阻塞
5、 插入 salary=62000時,會阻塞
INSERT INTO user VALUES (39, '哈子', 62000, '女');
INSERT INTO user VALUES (44, '靠是', 62000, '女');
INSERT INTO user VALUES (55, '西歐', 62000, '女');
-- 自增id時也是會阻塞
INSERT INTO `bostore`.`user`(`name`, `salary`, `gender`) VALUES ('學律', 62000, '女');
6、插入 salary=75000時,id<50 會阻塞,id>50 不阻塞
-- LOCK_MODE為X,GAP
-- LOCAK_DATA為75000, 50 表示 要插入salary為75000,id<50時會加鎖,即會阻塞
INSERT INTO user VALUES (29, '合吧', 75000, '女');
-- id > 50 不會阻塞
INSERT INTO user VALUES (51, '歐下', 75000, '女');
7、當salary 不在(55000, 62000] 和(62000, 75000)范圍時,id 不會加鎖
INSERT INTO user VALUES (31, '湖西', 54000, '女');
INSERT INTO user VALUES (49, '離下', 54000, '女');
INSERT INTO user VALUES (45, '的大', 76000, '女');
UPDATE user SET salary = 78000 WHERE id = 50;-- id < 30 和 salary < 55000 不阻塞
INSERT INTO user VALUES (29, '六零', 50000, '女');
-- id > 50 和 salary > 75000 不阻塞
INSERT INTO user VALUES (63, '合吧', 94000, '女');
普通索引等值查詢,數據不存在的情況
事務 1
BEGIN;
SELECT * FROM user WHERE salary = 60000 FOR UPDATE;
查看鎖情況
1、表加意向寫鎖
2、salary 加間隙鎖,范圍是(55000, 62000)
事務 2
1、salary 在(55000, 62000)范圍加了 GAP Lock,insert 時salary 在此范圍,會阻塞
2、插入 salary=55000時,id<30可以不阻塞,id>30會阻塞
INSERT INTO user VALUES (35, '六零', 55000, '女'); -- 阻塞
INSERT INTO user VALUES (51, '哈西', 55000, '女'); -- 阻塞
-- id=30 salary=55000 之前沒有間隙鎖
INSERT INTO user VALUES (29, '湖西', 55000, '女'); -- 不阻塞
3、插入 salary=62000時,id<40 會阻塞,id>40 不阻塞
-- id < 40 會阻塞
INSERT INTO user VALUES (39, '合吧', 62000, '女');
-- id > 40 不會阻塞
INSERT INTO user VALUES (41, '歐下', 62000, '女');
4、當 salary 不在(55000, 62000),id 不會加鎖
INSERT INTO user VALUES (31, '湖西', 54000, '女');
INSERT INTO user VALUES (39, '離下', 63000, '女');
UPDATE user SET salary = 63000 WHERE id = 40;-- id < 30 和 salary < 55000 不阻塞
INSERT INTO user VALUES (29, '六零', 50000, '女');
-- id > 40 和 salary > 62000 不阻塞
INSERT INTO user VALUES (41, '合吧', 65000, '女');
普通索引范圍查詢,前閉后開的情況
事務 1
BEGIN;
SELECT * FROM user WHERE salary>=55000 AND salary<62000 FOR UPDATE;
查看鎖情況
1、表加意向寫鎖
2、salary 加 NEXT-Key Lock 范圍是(50000, 55000]
3、salary 加 NEXT-Key Lock 范圍是(55000, 62000]
4、id=30 加行鎖
事務 2
1、 插入 salary=50000時,id<20可以不阻塞,id>20會阻塞
INSERT INTO user VALUES (19, '六零', 50000, '女'); -- 不阻塞
INSERT INTO user VALUES (21, '哈西', 50000, '女'); -- 阻塞
INSERT INTO user VALUES (41, '湖西', 50000, '女'); -- 阻塞
2、 插入 salary=55000時,會阻塞
INSERT INTO user VALUES (29, '哈子', 55000, '女');
INSERT INTO user VALUES (35, '靠是', 55000, '女');
INSERT INTO user VALUES (44, '西歐', 55000, '女');
-- 自增id時也是會阻塞
INSERT INTO `bostore`.`user`(`name`, `salary`, `gender`) VALUES ('學律', 55000, '女');
3、 插入 salary=62000時,id<40 會阻塞,id>40 不阻塞
-- 阻塞
INSERT INTO user VALUES (39, '合吧', 62000, '女');
-- 不阻塞
INSERT INTO user VALUES (41, '歐下', 62000, '女');
4、當salary 不在(50000, 55000] 和(55000, 62000]范圍時,id 不會加鎖
INSERT INTO user VALUES (31, '湖西', 44000, '女');
INSERT INTO user VALUES (49, '離下', 44000, '女');
INSERT INTO user VALUES (45, '的大', 66000, '女');-- id < 20 和 salary < 50000 不阻塞
INSERT INTO user VALUES (19, '六零', 40000, '女');
-- id > 40 和 salary > 62000 不阻塞
INSERT INTO user VALUES (53, '合吧', 94000, '女');
5、id=30 加行鎖,不能刪除和更新
UPDATE user SET salary = 56000 WHERE id = 30;
UPDATE user SET name="紫下" where salary = 55000;
普通索引范圍查詢,前開后閉的情況
事務 1
BEGIN;
SELECT * FROM user WHERE salary>55000 AND salary<=62000 FOR UPDATE;
查看鎖情況
1、表加意向寫鎖
2、salary 加 NEXT-Key Lock 范圍是(55000, 62000]
3、salary 加 NEXT-Key Lock 范圍是(62000, 75000]
4、id=40 加行鎖
事務 2 的加鎖情況和上面類似
沒有索引的情況
BEGIN;
SELECT * FROM user WHERE gender='男' FOR UPDATE;
查看加鎖情況
InnoDB 的鎖是加上在索引上的,沒有索引的時候,就會給所有的記錄都加上鎖 NEXT-Key Lock,相當于表鎖。