java實現(xiàn)分布式鎖的常用三種方式
分布式鎖概述
我們的系統(tǒng)都是分布式部署的,日常開發(fā)中,秒殺下單、搶購商品等等業(yè)務(wù)場景,為了防?庫存超賣,都需要用到分布式鎖。
分布式鎖其實就是,控制分布式系統(tǒng)不同進(jìn)程共同訪問共享資源的一種鎖的實現(xiàn)。如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機(jī)之間共享了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
業(yè)界流行的分布式鎖實現(xiàn),一般有這3種方式:
基于數(shù)據(jù)庫實現(xiàn)的分布式鎖
基于Redis實現(xiàn)的分布式鎖
基于Zookeeper實現(xiàn)的分布式鎖
分布式鎖:基于數(shù)據(jù)庫實現(xiàn)
主要有兩種方式:
1、悲觀鎖
2、樂觀鎖
A. 悲觀鎖(排他鎖)
利用select … where xx=yy for update排他鎖
注意:這里需要注意的是where xx=yy,xx字段必須要走索引,否則會鎖表。有些情況下,比如表不大,mysql優(yōu)化器會不走這個索引,導(dǎo)致鎖表問題。
核心思想:以「悲觀的心態(tài)」操作資源,無法獲得鎖成功,就一直阻塞著等待。
注意:該方式有很多缺陷,一般不建議使用。
實現(xiàn):
創(chuàng)建一張資源鎖表:
CREATE TABLE `resource_lock` ( `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的資源名', `owner` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖擁有者', `desc` varchar(1024) NOT NULL DEFAULT '備注信息', `update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時間,自動生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的資源';
注意:resource_name 鎖資源名稱必須有唯一索引
使用事務(wù)查詢更新:
@Transaction
public void lock(String name) {
ResourceLock rlock = exeSql("select * from resource_lock where resource_name = name for update");
if (rlock == null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}使用 for update 鎖定的資源。如果執(zhí)行成功,會立即返回,執(zhí)行插入數(shù)據(jù)庫,后續(xù)再執(zhí)行一些其他業(yè)務(wù)邏輯,直到事務(wù)提交,執(zhí)行結(jié)束;如果執(zhí)行失敗,就會一直阻塞著。
可以在數(shù)據(jù)庫客戶端工具上測試出來這個效果,當(dāng)在一個終端執(zhí)行了 for update,不提交事務(wù)。在另外的終端上執(zhí)行相同條件的 for update,會一直卡著
雖然也能實現(xiàn)分布式鎖的效果,但是會存在性能瓶頸。
優(yōu)點(diǎn):
簡單易用,好理解,保障數(shù)據(jù)強(qiáng)一致性。
缺點(diǎn):
1)在 RR 事務(wù)級別,select 的 for update 操作是基于間隙鎖(gap lock) 實現(xiàn)的,是一種悲觀鎖的實現(xiàn)方式,所以存在阻塞問題。
2)高并發(fā)情況下,大量請求進(jìn)來,會導(dǎo)致大部分請求進(jìn)行排隊,影響數(shù)據(jù)庫穩(wěn)定性,也會耗費(fèi)服務(wù)的CPU等資源。
當(dāng)獲得鎖的客戶端等待時間過長時,會提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并發(fā)情況下,也會造成占用過多的應(yīng)用線程,導(dǎo)致業(yè)務(wù)無法正常響應(yīng)。
3)如果優(yōu)先獲得鎖的線程因為某些原因,一直沒有釋放掉鎖,可能會導(dǎo)致死鎖的發(fā)生。
4)鎖的長時間不釋放,會一直占用數(shù)據(jù)庫連接,可能會將數(shù)據(jù)庫連接池?fù)伪?,影響其他服?wù)。
5)MySql數(shù)據(jù)庫會做查詢優(yōu)化,即便使用了索引,優(yōu)化時發(fā)現(xiàn)全表掃效率更高,則可能會將行鎖升級為表鎖,此時可能就更悲劇了。
6)不支持可重入特性,并且超時等待時間是全局的,不能隨便改動。
B. 樂觀鎖
所謂樂觀鎖與悲觀鎖最大區(qū)別在于基于CAS思想,表中添加一個時間戳或者是版本號的字段來實現(xiàn),update xx set version=new_version where xx=yy and version=Old_version,通過增加遞增的版本號字段實現(xiàn)樂觀鎖。
不具有互斥性,不會產(chǎn)生鎖等待而消耗資源,操作過程中認(rèn)為不存在并發(fā)沖突,只有update version失敗后才能覺察到。
搶購、秒殺就是用了這種實現(xiàn)以防止超賣。
實現(xiàn):
創(chuàng)建一張資源鎖表:
CREATE TABLE `resource` ( `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '資源名', `share` varchar(64) NOT NULL DEFAULT '' COMMENT '狀態(tài)', `version` int(4) NOT NULL DEFAULT '' COMMENT '版本號', `desc` varchar(1024) NOT NULL DEFAULT '備注信息', `update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時間,自動生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='資源';
為表添加一個字段,版本號或者時間戳都可以。通過版本號或者時間戳,來保證多線程同時間操作共享資源的有序性和正確性。
偽代碼實現(xiàn):
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
if (!succ) {
// 發(fā)起重試
}實際代碼中可以寫個while循環(huán)不斷重試,版本號不一致,更新失敗,重新獲取新的版本號,直到更新成功。
優(yōu)點(diǎn):
實現(xiàn)簡單,復(fù)雜度低
保障數(shù)據(jù)一致性
缺點(diǎn):
性能低,并且有鎖表的風(fēng)險
可靠性差
非阻塞操作失敗后,需要輪詢,占用CPU資源
長時間不commit或者是長時間輪詢,可能會占用較多的連接資源
分布式鎖:基于Redis實現(xiàn)
原理與實現(xiàn)
Redis提供了多種命令支持實現(xiàn)分布式鎖,其中最常用的是SETNX(Set if Not eXists)和GETSET結(jié)合使用,或者使用更高級的SET命令配合NX(Only set the key if it does not already exist)和PX或EX(為key設(shè)置過期時間)選項。
優(yōu)點(diǎn):
性能高效,Redis本身為內(nèi)存數(shù)據(jù)庫,操作速度快。
實現(xiàn)簡單,通過幾個命令即可完成鎖的獲取與釋放。
支持自動過期,降低死鎖風(fēng)險。
缺點(diǎn):
單點(diǎn)問題,依賴單一Redis實例可能成為瓶頸。
網(wǎng)絡(luò)分區(qū)可能導(dǎo)致鎖的不一致狀態(tài)。
示例代碼(偽代碼):
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private Jedis jedis;
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
public RedisDistributedLock(Jedis jedis) {
this.jedis = jedis;
}
public boolean lock(String lockKey, int expireTime) {
String result = jedis.set(lockKey, "locked", "NX", "PX", expireTime * 1000);
return LOCK_SUCCESS.equals(result);
}
public boolean unlock(String lockKey) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, 1, lockKey, "locked");
return RELEASE_SUCCESS.equals(result);
}
}代碼注解:上例中,
lock方法嘗試使用NX(只在鍵不存在時設(shè)置)和PX(設(shè)置過期時間,單位毫秒)參數(shù)設(shè)置鎖,返回OK表示成功獲取鎖。unlock方法使用Lua腳本確保解鎖操作的原子性,只有當(dāng)鎖的持有者與當(dāng)前客戶端匹配時才執(zhí)行刪除操作。
分布式鎖:基于Zookeeper實現(xiàn)
在學(xué)習(xí)Zookeeper分布式鎖之前,我們復(fù)習(xí)一下Zookeeper的節(jié)點(diǎn)哈。
Zookeeper的節(jié)點(diǎn)Znode有四種類型:
持久節(jié)點(diǎn):默認(rèn)的節(jié)點(diǎn)類型。創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開連接后,該節(jié)點(diǎn)依舊存在。
持久節(jié)點(diǎn)順序節(jié)點(diǎn):所謂順序節(jié)點(diǎn),就是在創(chuàng)建節(jié)點(diǎn)時,Zookeeper根據(jù)創(chuàng)建的時間順序給該節(jié)點(diǎn)名稱進(jìn)行編號,持久節(jié)點(diǎn)順序節(jié)點(diǎn)就是有順序的持久節(jié)點(diǎn)。
臨時節(jié)點(diǎn):和持久節(jié)點(diǎn)相反,當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開連接后,臨時節(jié)點(diǎn)會被刪除。
臨時順序節(jié)點(diǎn):有順序的臨時節(jié)點(diǎn)。
Zookeeper分布式鎖實現(xiàn)應(yīng)用了臨時順序節(jié)點(diǎn)。這里不貼代碼啦,來講下zk分布式鎖的實現(xiàn)原理吧。
zk獲取鎖過程
當(dāng)?shù)谝粋€客戶端請求過來時,Zookeeper客戶端會創(chuàng)建一個持久節(jié)點(diǎn)locks。如果它(Client1)想獲得鎖,需要在locks節(jié)點(diǎn)下創(chuàng)建一個順序節(jié)點(diǎn)lock1.如圖

接著,客戶端Client1會查找locks下面的所有臨時順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock1是不是排序最小的那一個,如果是,則成功獲得鎖。

這時候如果又來一個客戶端client2前來嘗試獲得鎖,它會在locks下再創(chuàng)建一個臨時節(jié)點(diǎn)lock2

客戶端client2一樣也會查找locks下面的所有臨時順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock2是不是最小的,此時,發(fā)現(xiàn)lock1才是最小的,于是獲取鎖失敗。獲取鎖失敗,它是不會甘心的,client2向它排序靠前的節(jié)點(diǎn)lock1注冊Watcher事件,用來監(jiān)聽lock1是否存在,也就是說client2搶鎖失敗進(jìn)入等待狀態(tài)。

此時,如果再來一個客戶端Client3來嘗試獲取鎖,它會在locks下再創(chuàng)建一個臨時節(jié)點(diǎn)lock3

同樣的,client3一樣也會查找locks下面的所有臨時順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock3是不是最小的,發(fā)現(xiàn)自己不是最小的,就獲取鎖失敗。它也是不會甘心的,它會向在它前面的節(jié)點(diǎn)lock2注冊Watcher事件,以監(jiān)聽lock2節(jié)點(diǎn)是否存在。

釋放鎖
我們再來看看釋放鎖的流程,Zookeeper的客戶端業(yè)務(wù)完成或者發(fā)生故障,都會刪除臨時節(jié)點(diǎn),釋放鎖。如果是任務(wù)完成,Client1會顯式調(diào)用刪除lock1的指令

如果是客戶端故障了,根據(jù)臨時節(jié)點(diǎn)得特性,lock1是會自動刪除的

lock1節(jié)點(diǎn)被刪除后,Client2可開心了,因為它一直監(jiān)聽著lock1。lock1節(jié)點(diǎn)刪除,Client2立刻收到通知,也會查找locks下面的所有臨時順序子節(jié)點(diǎn),發(fā)下lock2是最小,就獲得鎖。

同理,Client2獲得鎖之后,Client3也對它虎視眈眈,啊哈哈~
Zookeeper設(shè)計定位就是分布式協(xié)調(diào),簡單易用。如果獲取不到鎖,只需添加一個監(jiān)聽器即可,很適合做分布式鎖。
Zookeeper作為分布式鎖也缺點(diǎn):如果有很多的客戶端頻繁的申請加鎖、釋放鎖,對于Zookeeper集群的壓力會比較大。
到此這篇關(guān)于java實現(xiàn)分布式鎖的常用三種方式的文章就介紹到這了,更多相關(guān)java 分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Boot?+?Mybatis?Plus實現(xiàn)樹狀菜單的方法
這篇文章主要介紹了Spring?Boot?+?Mybatis?Plus實現(xiàn)樹狀菜單,包括實體類中添加子菜單列表和集合及構(gòu)建菜單樹的詳細(xì)代碼,代碼簡單易懂,需要的朋友可以參考下2021-12-12
詳解在idea 中使用Mybatis Generator逆向工程生成代碼
這篇文章主要介紹了在idea 中使用Mybatis Generator逆向工程生成代碼,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
解決SpringCloud Gateway配置自定義路由404的坑
這篇文章主要介紹了解決SpringCloud Gateway配置自定義路由404的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
關(guān)于spring data jpa一級緩存的問題
這篇文章主要介紹了關(guān)于spring data jpa一級緩存的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11
JDK的一個Bug監(jiān)聽文件變更的初步實現(xiàn)思路
這篇文章主要介紹了JDK的一個Bug監(jiān)聽文件變更要小心了,本篇文章就帶大家簡單實現(xiàn)一個對應(yīng)的功能,并分析一下對應(yīng)的Bug和優(yōu)缺點(diǎn),需要的朋友可以參考下2022-05-05

