騰訊一面:MySQL的共享鎖和獨(dú)占鎖
在InnoDB存儲(chǔ)引擎中,行級(jí)別鎖有兩種類型:共享鎖(S鎖)和排他鎖(X鎖),理解這 2種鎖的工作機(jī)制及其交互關(guān)系對(duì)于掌握MySQL的并發(fā)控制和鎖機(jī)制非常重要,因此,今天就來一起聊聊MySQL的共享鎖和排他鎖。
申明:本文基于 MySQL 8.0.30 版本,InnoDB引擎。
一、共享鎖
1.什么是共享鎖?
共享鎖(shared lock,S鎖),也叫讀鎖。它是指當(dāng)對(duì)象被鎖定時(shí),允許多個(gè)事務(wù)同時(shí)讀取該資源,也允許其它事務(wù)從該對(duì)象上再次獲取共享鎖,但不能對(duì)該對(duì)象進(jìn)行寫操作。
2.加鎖方式
共享鎖一般通過下面 2種方式進(jìn)行加鎖:
# 方式1
select ... lock in share mode;
# 方式2
select ... for share;
如果事務(wù)T1 持有某對(duì)象的共享(S)鎖,則事務(wù)T2 需要再次獲取該對(duì)象的鎖時(shí),會(huì)出現(xiàn)下面兩種情況:
- 如果T2 獲取該對(duì)象的共享(S)鎖,則可以立即獲取鎖;
- 如果T2 獲取該對(duì)象的排他(X)鎖,則無(wú)法獲取鎖;
二、舉例說明
為了更好地理解上述兩種情況,這里分別以下面的執(zhí)行順序流對(duì)InnoDB存儲(chǔ)引擎和MyISAM存儲(chǔ)引擎進(jìn)行驗(yàn)證:
1.InnoDB存儲(chǔ)引擎
創(chuàng)建一張用戶user表,表結(jié)構(gòu)如下:
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
(1) 給行加共享鎖
這里給user表中id=3行加共享鎖為例,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB | 線程C sessionC |
#開啟事務(wù) begin; | ||
#給 select * from user where id = 3 lock in share mode; | ||
#獲取 #select操作執(zhí)行成功 select * from user where id=3; | #獲取 #select操作執(zhí)行成功 select * from user where id=3; | |
#獲取 #delete操作被堵塞 delete from user where id = 3; | #獲取 #delete操作執(zhí)行成功 delete from user where id = 4; | |
#提交事務(wù) # commit; | ||
#獲取 #被堵塞的delete操作執(zhí)行成功 delete from user where id = 3; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)對(duì)user中id=3這行添加共享鎖后,事務(wù)B(sessionB)和事務(wù)C(sessionC)都可以獲取user表的共享鎖,也就是select操作能成功執(zhí)行,但是事務(wù)B(SessionB)獲取user表id=3的寫鎖失敗,即delete where id=3操作被阻塞,而事務(wù)C(sessionC)獲取user表id=4的寫鎖成功,即delete where id=4操作成功;
(2) 給表加共享鎖
這里通過lock in share mode方式給user整張表添加共享鎖,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB |
#開啟事務(wù) begin; | |
#對(duì) select * from user lock in share mode; | |
#成功獲取 select * from user; | |
#獲取 delete from user where id = 1; | |
#提交事務(wù) # commit; | |
#獲取 delete from user where id = 1; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)對(duì)user整張表添加共享鎖后,事務(wù)B(sessionB)可以獲取user表的共享鎖,也就是select操作能成功執(zhí)行,但是事務(wù)B(SessionB)獲取user表的寫鎖失敗,即delete操作被阻塞。
所以,盡管共享鎖(S鎖)是InnoDB存儲(chǔ)引擎的行級(jí)別鎖,但是一旦它作用到整張表時(shí),其實(shí)是對(duì)表中所有的行加共享鎖。
2.MyISAM引擎
創(chuàng)建一張用戶person表,表結(jié)構(gòu)如下:
CREATE TABLE `person` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_name` (`name`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_
給行加共享鎖
這里給person表的id=3行加共享鎖為例,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB |
#開啟事務(wù) begin; | |
#給 select * from person where id = 3 lock in share mode; | |
#獲取 #select操作成功 select * from person where id=3; | |
#獲取 #update操作成功 update person set name='name3xx' user where id = 3; | |
select * from person where id=3; | |
#提交事務(wù) # commit; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)對(duì)person中id=3這行添加共享鎖后,事務(wù)B(sessionB)既能獲取person表的共享鎖,也能獲取person表id=3的寫鎖,即select和update where id=3都操作成功;
因此,在MyISAM引擎中其實(shí)不存在共享鎖。
3.總結(jié)
通過上述示例及其運(yùn)行結(jié)果可以看出:
共享鎖是InnoDB存儲(chǔ)引擎的行級(jí)鎖,在MyISAM存儲(chǔ)引擎中不存在;
共享鎖是盡管是行級(jí)別鎖,但是當(dāng)鎖加在整個(gè)表時(shí)(表中所有的行,一種特殊的行),排他鎖也會(huì)在表級(jí)別生效;
三、排它鎖
1.什么是排他鎖?
排它鎖(exclusive lock,X鎖),也叫寫鎖或者獨(dú)占鎖,主要是防止其它事務(wù)和當(dāng)前加鎖事務(wù)鎖定同一對(duì)象,同一對(duì)象主要有兩層含義:
- 當(dāng)排他鎖加在表上,則其它事務(wù)無(wú)法對(duì)該表進(jìn)行insert,update,delete,alter,drop等更新操作;
- 當(dāng)排他鎖加在行上,則其它事務(wù)無(wú)法對(duì)該行進(jìn)行insert,update,delete,alter,drop等更新操作;
2.加鎖方式
排他鎖加鎖的方式一般有 2種:顯式加鎖和隱式加鎖,如下:
-- 顯式加鎖
select ... for update;
-- 隱式加鎖,是 MySQL內(nèi)部自動(dòng)加鎖
為了更好的說明排他鎖,這里以下面的執(zhí)行順序流來進(jìn)行驗(yàn)證,用戶user表的結(jié)構(gòu)還是和上面的一樣:
四、舉例說明
為了更好地理解上述兩種情況,這里分別以下面的執(zhí)行順序流對(duì)InnoDB存儲(chǔ)引擎和MyISAM存儲(chǔ)引擎進(jìn)行驗(yàn)證:
1.InnoDB存儲(chǔ)引擎
(1) 給行加排他鎖
這里通過for update顯式給user表中id=6行加排他鎖為例,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB | 線程C sessionC |
#開啟事務(wù) begin; | ||
#給 select * from user where id = 6 for update; | ||
#獲取 select * from user where id=6; | #獲取 select * from user where id=6; | |
#獲取 delete from user where id = 6; | #獲取 delete from user where id = 7; | |
#提交事務(wù) # commit; | ||
#獲取 #被堵塞的delete操作執(zhí)行成功 delete from user where id = 6; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)對(duì)user中id=6這行添加共享鎖后,事務(wù)B(sessionB)和事務(wù)C(sessionC)都可以獲取user表的共享鎖,也就是select操作能成功執(zhí)行,但是事務(wù)B(SessionB)獲取user表id=6的寫鎖失敗,即delete where id=6操作被阻塞,而事務(wù)C(sessionC)獲取user表id=7的寫鎖成功,即delete where id=7操作成功;
(2) 給表加排他鎖
這里通過for update顯式方式給user整張表添加排他鎖,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB |
#開啟事務(wù) begin; | |
#對(duì) select * from user for update; | |
#獲取 select * from user; | |
#獲取 delete from user where id=3; | |
#提交事務(wù) # commit; | |
#獲取 delete from user where id = 3; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)對(duì)user整張表加排他鎖后,事務(wù)B(sessionB)可以獲取user表的共享鎖,也就是select操作能成功執(zhí)行,但是事務(wù)B(SessionB)獲取user表的排他鎖失敗,即delete操作被阻塞;
所以,盡管排他鎖(X鎖)是InnoDB存儲(chǔ)引擎的行級(jí)別鎖,但是一旦它作用到整張表時(shí),其實(shí)是對(duì)表中所有的行加排他鎖。
2.MySQL 隱式加排他鎖
這里通過MySQL隱式給user的id=6行添加排他鎖,執(zhí)行順序流如下表
加鎖線程 sessionA | 線程B sessionB |
#開啟事務(wù) begin; | |
#MySQL隱式給 update user set name = 'name6' where id =6; | |
#獲取 select * from user where id = 6 lock in share mode; | |
#提交事務(wù) # commit; | |
#獲取 |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)執(zhí)行update where id=6時(shí),MySQL會(huì)隱式加排他鎖,事務(wù)B(sessionB)在lock in share mode模式下獲取user表id=6的共享鎖失敗,也就是select操作能成功被阻塞;
3.MyISAM引擎
MySQL 隱式加排他鎖
這里通過MySQL隱式給person的id=4行添加排他鎖,執(zhí)行順序流如下表:
加鎖線程 sessionA | 線程B sessionB |
#開啟事務(wù) begin; | |
#MySQL不會(huì)隱式給 update person set name = 'name4' where id =4; | |
#獲取 select * from user where id=4 lock in share mode; | |
#獲取 | |
#提交事務(wù) commit; |
示例執(zhí)行結(jié)果圖如下:
通過上述的示例執(zhí)行結(jié)果可以看出:當(dāng)事務(wù)A(sessionA)執(zhí)行update where id=6時(shí),MySQL不會(huì)隱式加排他鎖,事務(wù)B(sessionB)既能獲取id=4的共享鎖,也能獲取id=4的排他鎖;
因此,在MyISAM引擎中其實(shí)不存在排他鎖。
4.總結(jié)
通過上述 3個(gè)示例及其運(yùn)行結(jié)果可以看出:排他鎖有表級(jí)別共享鎖和行級(jí)別共享鎖和自動(dòng)鎖機(jī)制 3種 表級(jí)別共享鎖:
- 鎖定整個(gè)表,排他鎖也會(huì)在表級(jí)別生效;
- 行級(jí)別共享鎖:鎖定特定行,排他鎖也會(huì)在行級(jí)別生效;
- 自動(dòng)鎖機(jī)制:根據(jù)操作是表級(jí)別還行級(jí)別自動(dòng)加對(duì)應(yīng)的鎖;
五、共享鎖和排他鎖的兼容性矩陣
為了更好地理解共享鎖和排他鎖的互斥關(guān)系,可以參考以下兼容性矩陣:
無(wú)鎖 | 共享鎖 | 排他鎖 | |
無(wú)鎖 | 允許 | 允許 | 允許 |
共享鎖 | 允許 | 允許 | 阻塞 |
排他鎖 | 允許 | 阻塞 | 阻塞 |
從上述矩陣可以看出:
- 無(wú)鎖狀態(tài)下可以獲取任何類型的鎖
- 共享鎖狀態(tài)下可以繼續(xù)獲取共享鎖,但不能獲取排他鎖
- 排他鎖狀態(tài)下不能獲取任何其他鎖
六、總結(jié)
- 共享鎖(S鎖)和排他鎖(X鎖)是InnoDB存儲(chǔ)引擎中的 2種行級(jí)別鎖,MyISAM存儲(chǔ)引擎不存在。
- 盡管共享鎖(S 鎖)和排他鎖(X 鎖)是行級(jí)鎖,但是當(dāng)他們加到表級(jí)別時(shí),對(duì)表所有行都生效,這樣看上去等同表級(jí)鎖
- 共享鎖(S 鎖)允許多個(gè)事務(wù)同時(shí)讀取數(shù)據(jù),但不允許修改數(shù)據(jù)。多個(gè)事務(wù)可以同時(shí)持有共享鎖
- 排他鎖(X 鎖)允許一個(gè)事務(wù)修改數(shù)據(jù)。只有一個(gè)事務(wù)可以持有排他鎖,并且在它釋放鎖之前,其他事務(wù)不能獲得任何類型的鎖