MySQL實(shí)現(xiàn)分布式鎖
基于MySQL分布式鎖實(shí)現(xiàn)原理及代碼
工欲善其事必先利其器,在基于MySQL實(shí)現(xiàn)分布式鎖之前,我們要先了解一點(diǎn)MySQL鎖自身的相關(guān)內(nèi)容
MySQL鎖
我們知道:鎖是計(jì)算機(jī)協(xié)調(diào)多個(gè)進(jìn)程或者線程并發(fā)訪問(wèn)同一資源的機(jī)制,而在數(shù)據(jù)庫(kù)中,除了傳統(tǒng)的機(jī)器資源的爭(zhēng)用之外,存儲(chǔ)下來(lái)的數(shù)據(jù)也屬于供用戶(hù)共享的資源,所以如何保證數(shù)據(jù)并發(fā)的一致性,有效性是每個(gè)數(shù)據(jù)庫(kù)必須解決的問(wèn)題。
除此之外,鎖沖突也是影響數(shù)據(jù)庫(kù)并發(fā)性能的主要因素,所以鎖對(duì)于數(shù)據(jù)庫(kù)而言就顯得非常重要,也非常復(fù)雜。
而存儲(chǔ)引擎是MySQL中非常重要的底層組件,主要用來(lái)處理不同類(lèi)型的SQL操作,其中包括創(chuàng)建,讀取,刪除和修改操作。在MySQL中提供了不同類(lèi)型的存儲(chǔ)引擎,根據(jù)其不同的特性提供了不同的存儲(chǔ)機(jī)制,索引和鎖功能。
根據(jù)show engines;
能夠列出MySQL下支持的存儲(chǔ)引擎
如果沒(méi)有特殊指定,那么在MySQL8.0
中會(huì)設(shè)置InnoDB
為默認(rèn)的存儲(chǔ)引擎
在實(shí)際工作中,根據(jù)需求選擇最多的兩種存儲(chǔ)引擎分別為:
- InnoDB
- MyISAM
所以我們主要針對(duì)這兩種類(lèi)型來(lái)介紹MySQL的鎖
InnoDB
InnoDB
支持多粒度鎖定,可以支持行鎖,也可以支持表鎖。如果沒(méi)有升級(jí)鎖粒度,那么默認(rèn)情況下是以行鎖來(lái)設(shè)計(jì)的。
關(guān)于行鎖和表鎖的介紹:
- 行鎖對(duì)指定數(shù)據(jù)進(jìn)行加鎖,鎖定粒度最小,開(kāi)銷(xiāo)大,加鎖慢,容易出現(xiàn)死鎖問(wèn)題,出現(xiàn)鎖沖突的概率最小,并發(fā)性最高
- 表鎖對(duì)整個(gè)表進(jìn)行加鎖,鎖定粒度大,開(kāi)銷(xiāo)小,加鎖快,不會(huì)出現(xiàn)死鎖,出現(xiàn)鎖沖突的概率最大,并發(fā)性最低
這里沒(méi)法說(shuō)明那種鎖最好,只有合適不合適
在行級(jí)鎖中,可以分為兩種類(lèi)型
- 共享鎖
- 排他鎖
共享鎖
共享鎖又稱(chēng)為讀鎖,允許其他事務(wù)讀取被鎖定的對(duì)象,也可以在其上獲取其他共享鎖,但不能寫(xiě)入。
舉個(gè)例子:
- 事務(wù)T在數(shù)據(jù)A擁有共享鎖,那么當(dāng)前事務(wù)T對(duì)數(shù)據(jù)A可以讀,但是不能修改。而且事務(wù)T2同樣可以對(duì)數(shù)據(jù)A擁有共享鎖,這樣相當(dāng)于在數(shù)據(jù)A上分別存在不同事務(wù)的共享鎖
- 數(shù)據(jù)A擁有了事務(wù)T的共享鎖,那么就不能再擁有其他事務(wù)的排他鎖
下面是關(guān)于共享鎖的具體實(shí)現(xiàn),關(guān)鍵代碼:select .. from table lock in share mode
-- 創(chuàng)建實(shí)例表 create table tb_lock( id bigint primary key auto_increment, t_name varchar(20) ) engine=InnoDB;
開(kāi)啟兩個(gè)窗口來(lái)測(cè)試:
session1 | session2 |
---|---|
set autocommit=0; | set autocommit=0; |
select * from tb_lock where t_name = ‘zs’ lock in share mode; | |
select * from tb_lock where t_name = ‘zs’ lock in share mode; | |
select * from tb_lock where t_name = ‘lsp’ lock in share mode; | |
update tb_lock set t_name = ‘lzs’ where t_name = ‘zs’; | |
update tb_lock set t_name = ‘lsp111’ where t_name = ‘lsp’; | |
select * from tb_lock where t_name = ‘zs’; | |
commit; |
自動(dòng)提交全部關(guān)閉,可以通過(guò)
select @@autocommit;
來(lái)查看
通過(guò)以上實(shí)驗(yàn),我們總結(jié):
- 共享鎖基于行鎖處理,不同事務(wù)可以在同一條數(shù)據(jù)上獲取共享鎖
- 如果多個(gè)事務(wù)在同一條數(shù)據(jù)上獲取共享鎖,當(dāng)想要修改該條數(shù)據(jù)的時(shí)候,會(huì)出現(xiàn)阻塞狀態(tài)。直到其他事務(wù)將鎖釋放,該能夠繼續(xù)修改
修改,刪除,插入會(huì)默認(rèn)對(duì)涉及到的數(shù)據(jù)加上排他鎖
- 單純的
select
操作不會(huì)有任何影響,select
不會(huì)加任何鎖 - 執(zhí)行
commit;
自動(dòng)釋放鎖
排它鎖
又叫寫(xiě)鎖。只允許獲取鎖的事務(wù)對(duì)數(shù)據(jù)進(jìn)行操作【更新,刪除】,其他事務(wù)對(duì)相同數(shù)據(jù)集只能進(jìn)行讀取,不能有跟新或者刪除操作。而且也不能在相同數(shù)據(jù)集獲取到共享鎖。
沒(méi)錯(cuò),就是這么霸道
在MySQL中,想要基于排它鎖實(shí)現(xiàn)行級(jí)鎖,就需要對(duì)表中索引列加鎖,否則的話,排它鎖就屬于表級(jí)鎖
下面一一來(lái)展示,關(guān)鍵代碼:select .. from XX for update
首先是有索引列狀態(tài)
session1 | session2 |
---|---|
set autocommit=0; | set autocommit=0; |
select * from tb_lock; | select * from tb_lock; |
select * from tb_lock where id = 1 for update; | |
select * from tb_lock where id = 1 for update; | |
select * from tb_lock where id = 2 for update; | |
commit; |
通過(guò)以上實(shí)驗(yàn),得到結(jié)論:
- 對(duì)索引列進(jìn)行加鎖的鎖定級(jí)別為行級(jí)鎖,如上所示,當(dāng)其他事務(wù)想要對(duì)相同的數(shù)據(jù)再次加鎖的時(shí)候,就會(huì)進(jìn)行到阻塞狀態(tài)。并且如果等待時(shí)間過(guò)長(zhǎng),會(huì)出現(xiàn)如下異常:
Lock wait timeout exceeded; try restarting transaction
- 對(duì)不同行數(shù)據(jù)再次加排它鎖,是沒(méi)有任何問(wèn)題的。
- 對(duì)已經(jīng)上鎖的相同數(shù)據(jù)做修改和刪除操作不需要多說(shuō),因?yàn)镮nnoDB默認(rèn)會(huì)對(duì)其加入排它鎖
下面是無(wú)索引列狀態(tài)
session1 | session2 |
---|---|
set autocommit=0; | set autocommit=0; |
select * from tb_lock; | select * from tb_lock; |
select * from tb_lock where t_name = ‘ls’ for update; | |
select * from tb_lock where t_name = ‘ls’ for update; | |
commit |
通過(guò)以上實(shí)驗(yàn),得到結(jié)論:
- 對(duì)非索引列其中一條數(shù)據(jù)加入了排它鎖后,在其他事務(wù)中對(duì)不同數(shù)據(jù)再次加入排它鎖,進(jìn)入了阻塞狀態(tài)
- 說(shuō)明當(dāng)加鎖列屬于非索引時(shí),InnoDB會(huì)對(duì)整個(gè)表進(jìn)行上鎖,進(jìn)入到表級(jí)鎖
接下來(lái)我們來(lái)看看MyISAM的方式
MyISAM
MyISAM屬于表級(jí)鎖,被用來(lái)防止任何其他事務(wù)訪問(wèn)表的鎖。
其中表鎖又分為兩種形式
- 表共享讀鎖: READ
- 表獨(dú)占寫(xiě)鎖: WRITE
這里我們要注意:表級(jí)鎖只能防止其他會(huì)話進(jìn)行不適當(dāng)?shù)淖x取或?qū)懭搿?/p>
- 持有
WRITE
鎖的會(huì)話可以執(zhí)行表級(jí)操作,比如DELETE
或者TRUNCATE
- 持有會(huì)話
READ
鎖,不能夠執(zhí)行DELETE
或者TRUNCATE
操作
表共享讀鎖
不管是READ
還是WRITE
,都是通過(guò)lock table
來(lái)獲取表鎖的,而READ
鎖擁有如下特性:
- 持有鎖的會(huì)話可以讀取表,但是不能進(jìn)行寫(xiě)入操作
- 多個(gè)會(huì)話可以同時(shí)獲取
READ
表的鎖,而其他會(huì)話可以在不顯式獲取READ
鎖的情況下讀取該表:也就是說(shuō)直接通過(guò)select
來(lái)操作
那么,接下來(lái)我們來(lái)看實(shí)際操作,關(guān)鍵代碼:lock tables table_name read
create table tb_lock_isam( id bigint primary key auto_increment, t_name varchar(20) ) engine=MyISAM;
開(kāi)啟兩個(gè)窗口來(lái)進(jìn)行操作:
session1 | session2 |
---|---|
set autocommit=0; | set autocommit=0; |
LOCK TABLES tb_lock_isam READ; | |
select * from tb_lock_isam; | |
select * from tb_lock; | |
select * from tb_lock_isam; | |
LOCK TABLES tb_lock_isam READ; | |
select * from tb_lock_isam; | |
select * from tb_lock; | |
unlock tables; | insert into tb_lock_isam(t_name) values(‘ll’); |
通過(guò)以上實(shí)戰(zhàn),驗(yàn)證以下結(jié)論:
- 在當(dāng)前事務(wù)下,獲取到讀鎖,直接查詢(xún)鎖定表是沒(méi)有問(wèn)題的,但是如果想要讀取其他表下的數(shù)據(jù),那么就會(huì)出現(xiàn)以下異常:因?yàn)槠渌聿](méi)有LOCK在其中
Table 'tb_lock' was not locked with LOCK TABLES
- 事務(wù)A獲取到讀鎖之后,在其他事務(wù)中是可以正常讀取的,并且也可以再次獲取讀鎖。
- 在讀鎖中如果想要進(jìn)行插入操作是不會(huì)成功的,出現(xiàn)以下異常:
Table 'tb_lock_isam' was locked with a READ lock and can't be updated
- 當(dāng)前表獲取到讀鎖之后,在當(dāng)前表沒(méi)有釋放讀鎖之前,再獲取寫(xiě)鎖會(huì)一直進(jìn)入到阻塞狀態(tài)。
- 可以通過(guò)非加鎖方式來(lái)讀取數(shù)據(jù),但是要注意:一定是在不同的事務(wù)下
表獨(dú)占寫(xiě)鎖
WRITE鎖
的特性和排它鎖
的特性非常相似,都特別霸道:
- 持有鎖的會(huì)話可以讀寫(xiě)表
- 只有持有鎖的會(huì)話才能訪問(wèn)該表。在釋放鎖之前,沒(méi)有其他會(huì)話可以訪問(wèn)它
- 其他會(huì)話對(duì)表的鎖請(qǐng)求在
WRITE
持有鎖時(shí)被阻塞
還是通過(guò)具體實(shí)戰(zhàn)來(lái)進(jìn)行演示效果,關(guān)鍵代碼:lock tables table_name write
session1 | session2 |
---|---|
select * from tb_lock_isam; | select * from tb_lock_isam; |
lock table tb_lock_isam write; | |
select * from tb_lock_isam; | |
insert into tb_lock_isam(t_name) values(‘66’); | |
select * from tb_lock_isam; | |
unlock tables; |
通過(guò)以上實(shí)戰(zhàn),驗(yàn)證以下結(jié)論:
- 當(dāng)事務(wù)獲取到當(dāng)前表的
WRITE鎖
的時(shí)候,在當(dāng)前事務(wù)下可以對(duì)獲取鎖的表進(jìn)行任何操作,其他事務(wù)無(wú)法對(duì)表進(jìn)行任意操作。 - 在不同事務(wù)下不會(huì)對(duì)其他表的操作有影響
- 在當(dāng)前事務(wù)獲取到
WRITE鎖
之后,只能在當(dāng)前事務(wù)下操作獲取鎖的表,無(wú)法操作其他表,否則會(huì)出現(xiàn)以下異常
Table 'tb_index' was not locked with LOCK TABLES'
【注意】
MyISAM
在執(zhí)行查詢(xún)語(yǔ)句之前,會(huì)自動(dòng)給涉及的所有表加讀鎖,在執(zhí)行更新操作前,會(huì)自動(dòng)給涉及的表加寫(xiě)鎖,這個(gè)過(guò)程并不需要用戶(hù)干預(yù),因此用戶(hù)一般不需要使用命令來(lái)顯式加鎖
分布式鎖實(shí)現(xiàn)
既然已經(jīng)了解到了MySQL鎖相關(guān)內(nèi)容,那么我們就來(lái)看看如何實(shí)現(xiàn),首先我們需要?jiǎng)?chuàng)建一張數(shù)據(jù)表
當(dāng)然,只需要初始化創(chuàng)建一次
create table if not exists fud_distribute_lock( id bigint unsigned primary key auto_increment, biz varchar(50) comment '業(yè)務(wù)Key' unique(biz) ) engine=innodb;
在其中,biz
是為了區(qū)分不同的業(yè)務(wù),也可以理解為資源隔離,并且對(duì)biz
設(shè)置唯一索引,也能夠防止其鎖級(jí)別變?yōu)楸砑?jí)鎖
既然for udpate
就是加鎖成功,事務(wù)提交就自動(dòng)釋放鎖,那么這個(gè)事情就非常好辦了:
// 省略了構(gòu)造方法,需要傳入DataSource和biz ? private static final String SELECT_SQL = "SELECT * FROM fud_distribute_lock WHERE `biz` = ? for update"; private static final String INSERT_SQL = "INSERT INTO fud_distribute_lock(`biz`) values(?)"; ? // 從構(gòu)造方法中傳入 private final DataSource source; private Connection connection; ? public void lock() { PreparedStatement psmt = null; ResultSet rs = null; ? try { // while(true); for (; ; ) { connection = this.source.getConnection(); // 關(guān)閉自動(dòng)提交事務(wù) connection.setAutoCommit(false); psmt = connection.prepareStatement(SELECT_SQL); psmt.setString(1, biz); rs = psmt.executeQuery(); if (rs.next()) { return; } connection.commit(); close(connection, psmt, rs); // 如果沒(méi)有相關(guān)查詢(xún),需要插入 Connection updConnection = this.source.getConnection(); PreparedStatement insertStatement = null; try { insertStatement = updConnection.prepareStatement(INSERT_SQL); insertStatement.setString(1, biz); if (insertStatement.executeUpdate() == 1) { LOGGER.info("創(chuàng)建鎖記錄成功"); } } catch (Exception e) { LOGGER.error("創(chuàng)建鎖記錄異常:{}", e.getMessage()); } finally { close(insertStatement, updConnection); } } } catch (Exception e) { LOGGER.error("lock異常信息:{}", e.getMessage()); throw new BusException(e); } finally { close(psmt, rs); } } ? public void unlock() { try { // 事務(wù)提交之后自動(dòng)解鎖 connection.commit(); close(connection); } catch (Exception e) { LOGGER.error("unlock異常信息:{}", e.getMessage()); throw new BusException(e); } } ? public void close(AutoCloseable... closeables) { Arrays.stream(closeables).forEach(closeable -> { if (null != closeable) { try { closeable.close(); } catch (Exception e) { LOGGER.error("close關(guān)閉異常:{}", e.getMessage()); } } }); }
難點(diǎn):為什么需要for(;
如果一個(gè)請(qǐng)求是第一次進(jìn)來(lái)的,比如biz=order
,在這個(gè)表中是不會(huì)存儲(chǔ)order
這條記錄,那么select ...for update
就不會(huì)生效,所以就需要先將order
插入到表記錄中,也就是執(zhí)行insert
操作。
insert
執(zhí)行成功之后,記錄select...for update
,這樣獲取鎖才能生效
總結(jié)
基于MySQL的分布式鎖在實(shí)際開(kāi)發(fā)過(guò)程中很少使用,但是我們還是要有一個(gè)思路在。那么本節(jié)針對(duì)MySQL的分布式鎖實(shí)現(xiàn)到這里就結(jié)束了,掌握了MySQL的基礎(chǔ)鎖,那么就會(huì)非常簡(jiǎn)單了。
到此這篇關(guān)于MySQL實(shí)現(xiàn)分布式鎖的文章就介紹到這了,更多相關(guān)MySQL分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL外鍵級(jí)聯(lián)的實(shí)現(xiàn)
本文主要介紹了MySQL外鍵級(jí)聯(lián)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07一次mysql的.ibd文件過(guò)大處理過(guò)程記錄
mysql數(shù)據(jù)庫(kù)的每個(gè)表都有ibd和frm兩種格式的文件,ibd文件是表的數(shù)據(jù)文件,存放表的數(shù)據(jù),修改文件后綴,無(wú)法在MySQL數(shù)據(jù)庫(kù)中查詢(xún)表數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于一次mysql的.ibd文件過(guò)大處理過(guò)程的相關(guān)資料,需要的朋友可以參考下2022-06-06MySQL中表復(fù)制:create table like 與 create table as select
這篇文章主要介紹了MySQL中表復(fù)制:create table like 與 create table as select,需要的朋友可以參考下2014-12-12mysql alter添加列的實(shí)現(xiàn)方式
這篇文章主要介紹了mysql alter添加列的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01阿里云配置MySQL-server?8.0遠(yuǎn)程登錄的實(shí)現(xiàn)
我們經(jīng)常會(huì)碰到需要遠(yuǎn)程訪問(wèn)數(shù)據(jù)庫(kù)的場(chǎng)景,本文主要介紹了阿里云配置MySQL-server?8.0遠(yuǎn)程登錄的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08mySQL count多個(gè)表的數(shù)據(jù)實(shí)例詳解
這篇文章通過(guò)實(shí)例給大家介紹了mySQL中count多個(gè)表的數(shù)據(jù),也就是多個(gè)表如何聯(lián)合查詢(xún),文中通過(guò)項(xiàng)目中遇到的一個(gè)問(wèn)題進(jìn)行分析和實(shí)現(xiàn),給出了詳細(xì)的示例代碼,相信對(duì)大家的理解和學(xué)習(xí)很有幫助,有需要的朋友們下面來(lái)一起看看吧。2016-11-11使用JDBC在MySQL數(shù)據(jù)庫(kù)中如何快速批量插入數(shù)據(jù)
這篇文章主要介紹了使用JDBC在MySQL數(shù)據(jù)庫(kù)中如何快速批量插入數(shù)據(jù),可以有效的解決一次插入大數(shù)據(jù)的方法,2016-11-11MySQL中Like模糊查詢(xún)速度太慢該如何進(jìn)行優(yōu)化
在業(yè)務(wù)場(chǎng)景中經(jīng)常會(huì)用到like模糊查詢(xún),但是大家都知道,like是用不到索引的,所以當(dāng)數(shù)據(jù)量非常大時(shí),速度會(huì)非常慢,這篇文章主要給大家介紹了關(guān)于MySQL中Like模糊查詢(xún)速度太慢該如何進(jìn)行優(yōu)化的相關(guān)資料,需要的朋友可以參考下2021-12-12