手把手教你實(shí)現(xiàn)一個(gè)基于Redis的分布式鎖

簡介

分布式鎖在分布式系統(tǒng)中非常常見亮蒋,比如對公共資源進(jìn)行操作妆毕,如賣車票,同一時(shí)刻只能有一個(gè)節(jié)點(diǎn)將某個(gè)特定座位的票賣出去趁怔;如避免緩存失效帶來的大量請求訪問數(shù)據(jù)庫的問題

設(shè)計(jì)

這非常像一道面試題:如何實(shí)現(xiàn)一個(gè)分布式鎖薪前?在簡介中,基本上已經(jīng)對這個(gè)分布式工具提出了一些需求铺浇,你可以不著急看下面的答案垛膝,自己思考一下分布式鎖應(yīng)該如何實(shí)現(xiàn)丁稀?

首先我們需要一個(gè)簡單的答題套路:需求分析线衫、系統(tǒng)設(shè)計(jì)惑折、實(shí)現(xiàn)方式、缺點(diǎn)不足

需求分析

能夠在高并發(fā)的分布式的系統(tǒng)中應(yīng)用

需要實(shí)現(xiàn)鎖的基本特性:一旦某個(gè)鎖被分配出去白热,那么其他的節(jié)點(diǎn)無法再進(jìn)入這個(gè)鎖所管轄范圍內(nèi)的資源敞咧;失效機(jī)制避免無限時(shí)長的鎖與死鎖

進(jìn)一步實(shí)現(xiàn)鎖的高級特性和JUC并發(fā)工具類似功能更好:可重入、阻塞與非阻塞乍恐、公平與非公平测砂、JUC的并發(fā)工具(Semaphore, CountDownLatch, CyclicBarrier)

系統(tǒng)設(shè)計(jì)

轉(zhuǎn)換成設(shè)計(jì)是如下幾個(gè)要求:

對加鎖、解鎖的過程需要是高性能呜投、原子性的

需要在某個(gè)分布式節(jié)點(diǎn)都能訪問到的公共平臺上進(jìn)行鎖狀態(tài)的操作

所以存璃,我們分析出系統(tǒng)的構(gòu)成應(yīng)該要有鎖狀態(tài)存儲模塊連接存儲模塊的連接池模塊粘招、鎖內(nèi)部邏輯模塊

鎖狀態(tài)存儲模塊

分布式鎖的存儲有三種常見實(shí)現(xiàn)偎球,因?yàn)槟軡M足實(shí)現(xiàn)鎖的這些條件:高性能加鎖解鎖、操作的原子性袍冷、是分布式系統(tǒng)中不同節(jié)點(diǎn)都可以訪問的公共平臺:

數(shù)據(jù)庫(利用主鍵唯一規(guī)則猫牡、MySQL行鎖)

基于Redis的NX、EX參數(shù)

Zookeeper臨時(shí)有序節(jié)點(diǎn)

由于鎖常常是在高并發(fā)的情況下才會使用到的分布式控制工具乃戈,所以使用數(shù)據(jù)庫實(shí)現(xiàn)會對數(shù)據(jù)庫造成一定的壓力,連接池爆滿問題缩歪,所以不推薦數(shù)據(jù)庫實(shí)現(xiàn)谍憔;我們還需要維護(hù)Zookeeper集群,實(shí)現(xiàn)起來還是比較復(fù)雜的逛球。如果不是原有系統(tǒng)就依賴Zookeeper苫昌,同時(shí)壓力不大的情況下。一般不使用Zookeeper實(shí)現(xiàn)分布式鎖奥务。所以緩存實(shí)現(xiàn)分布式鎖還是比較常見的袜硫,因?yàn)?b>緩存比較輕量、緩存的響應(yīng)快帚称、吞吐高秽澳、還有自動失效的機(jī)制保證鎖一定能釋放。

連接池模塊

可使用JedisPool實(shí)現(xiàn)瞻坝,如果后期性能不佳杏瞻,可考慮參照HikariCP自己實(shí)現(xiàn)

鎖內(nèi)部邏輯模塊

基本功能:加鎖捞挥、解鎖忧吟、超時(shí)釋放

高級功能:可重入、阻塞與非阻塞讹俊、公平與非公平、JUC并發(fā)工具功能

實(shí)現(xiàn)方式

存儲模塊使用Redis仍劈,連接池模塊暫時(shí)使用JedisPool贩疙,鎖的內(nèi)部邏輯將從基本功能開始,逐步實(shí)現(xiàn)高級功能这溅,下面就是各種功能實(shí)現(xiàn)的具體思路與代碼了。

加鎖悲靴、超時(shí)釋放

NX是Redis提供的一個(gè)原子操作癞尚,如果指定key存在,那么NX失敗否纬,如果不存在會進(jìn)行set操作并返回成功临燃。我們可以利用這個(gè)來實(shí)現(xiàn)一個(gè)分布式的鎖,主要思路就是膜廊,set成功表示獲取鎖爪瓜,set失敗表示獲取失敗,失敗后需要重試蝶缀。再加上EX參數(shù)可以讓該key在超時(shí)之后自動刪除薄货。

下面是一個(gè)阻塞鎖的加鎖操作,將循環(huán)去掉并返回執(zhí)行結(jié)果就能寫出非阻塞鎖(就不粘出來了):

public void lock(String key, String request, int timeout) throws InterruptedException {

? ? Jedis jedis = jedisPool.getResource();

? ? while (timeout >= 0) {

? ? ? ? String result = jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, DEFAULT_EXPIRE_TIME);

? ? ? ? if (LOCK_MSG.equals(result)) {

? ? ? ? ? ? jedis.close();

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? Thread.sleep(DEFAULT_SLEEP_TIME);

? ? ? ? timeout -= DEFAULT_SLEEP_TIME;

? ? }

}

但超時(shí)時(shí)間這個(gè)參數(shù)會引發(fā)一個(gè)問題柄慰,如果超過超時(shí)時(shí)間但是業(yè)務(wù)還沒執(zhí)行完會導(dǎo)致并發(fā)問題,其他進(jìn)程就會執(zhí)行業(yè)務(wù)代碼藏研,至于如何改進(jìn)蠢挡,下文會講到占锯。

解鎖

最常見的解鎖代碼就是直接使用jedis.del()方法刪除鎖消略,這種不先判斷鎖的擁有者而直接解鎖的方式,會導(dǎo)致任何客戶端都可以隨時(shí)進(jìn)行解鎖却紧,即使這把鎖不是它的胎撤。

比如可能存在這樣的情況:客戶端A加鎖,一段時(shí)間之后客戶端A解鎖巫俺,在執(zhí)行jedis.del()之前肿男,鎖突然過期了,此時(shí)客戶端B嘗試加鎖成功嘹承,然后客戶端A再執(zhí)行del()方法如庭,則將客戶端B的鎖給解除了。

所以我們需要一個(gè)具有原子性的方法來解鎖骤竹,并且要同時(shí)判斷這把鎖是不是自己的往毡。由于Lua腳本在Redis中執(zhí)行是原子性的卖擅,所以可以寫成下面這樣:

public boolean unlock(String key, String value) {

? ? Jedis jedis = jedisPool.getResource();

? ? 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, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(value));

? ? jedis.close();

? ? return UNLOCK_MSG.equals(result);

}

來用測試梭一把

此時(shí)我們可以來寫個(gè)測試來試試有沒有達(dá)到我們想要的效果,上面的代碼都寫在src/main/java下的RedisLock里挎狸,下面的測試代碼需要寫在src/test/java里断楷,因?yàn)閱卧獪y試只是測試代碼的邏輯,無法測試真實(shí)連接Redis之后的表現(xiàn)恐锣,也沒辦法體驗(yàn)到被鎖住帶來的緊張又刺激的快感舞痰,所以本項(xiàng)目中主要以集成測試為主,如果你想試試帶Mock的單元測試玷禽,可以看看這篇文章呀打。

那么集成測試會需要依賴一個(gè)Redis實(shí)例贬丛,為了避免你在本地去裝個(gè)Redis來跑測試,我用到了一個(gè)嵌入式的Redis工具以及如下代碼來幫我們New一個(gè)Redis實(shí)例额获,盡情去連接吧 ~ 代碼可參看EmbeddedRedis類焕阿。另外,集成測試使用到了Spring撤摸,是不是倍感親切褒纲?相當(dāng)于也提供了一個(gè)集成Spring的例子。

@Configuration

public class EmbeddedRedis implements ApplicationRunner {

? ? private static RedisServer redisServer;

? ? @PreDestroy

? ? public void stopRedis() {

? ? ? ? redisServer.stop();

? ? }

? ? @Override

? ? public void run(ApplicationArguments applicationArguments) {

? ? ? ? redisServer = RedisServer.builder().setting("bind 127.0.0.1").setting("requirepass test").build();

? ? ? ? redisServer.start();

? ? }

}

對于需要考慮并發(fā)的代碼下的測試是比較難且比較難以達(dá)到檢測代碼質(zhì)量的目的的衫嵌,因?yàn)闇y試用例會用到多線程的環(huán)境楔绞,不一定能百分百通過且難以重現(xiàn),但本項(xiàng)目的分布式鎖是一個(gè)比較簡單的并發(fā)場景桦锄,所以我會盡可能保證測試是有意義的蔫耽。

我第一個(gè)測試用例是想測試一下鎖的互斥能力,能否在A拿到鎖之后图甜,B就無法立即拿到鎖:

@Test

public void shouldWaitWhenOneUsingLockAndTheOtherOneWantToUse() throws InterruptedException {

? ? Thread t = new Thread(() -> {

? ? ? ? try {

? ? ? ? ? ? redisLock.lock(lock1Key, UUID.randomUUID().toString());

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? });

? ? t.start();

? ? t.join();

? ? long startTime = System.currentTimeMillis();

? ? redisLock.lock(lock1Key, UUID.randomUUID().toString(), 3000);

? ? assertThat(System.currentTimeMillis() - startTime).isBetween(2500L, 3500L);

}

但這僅僅測試了加鎖操作時(shí)候的互斥性鳖眼,但是沒有測試解鎖是否會成功以及解鎖之后原來等待鎖的進(jìn)程會繼續(xù)進(jìn)行具帮,所以你可以參看一下testLockAndUnlock方法是如何測試的。不要覺得寫測試很簡單匪凡,想清楚測試的各種情況掘猿,設(shè)計(jì)測試情景并實(shí)現(xiàn)并不容易。然而以后寫的測試不會單獨(dú)拿出來講衬衬,畢竟本文想關(guān)注的還是分布式鎖的實(shí)現(xiàn)嘛改橘。

超時(shí)釋放導(dǎo)致的并發(fā)問題

問題:如果A拿到鎖之后設(shè)置了超時(shí)時(shí)長,但是業(yè)務(wù)執(zhí)行的時(shí)長超過了超時(shí)時(shí)長狮惜,導(dǎo)致A還在執(zhí)行業(yè)務(wù)但是鎖已經(jīng)被釋放碾篡,此時(shí)其他進(jìn)程就會拿到鎖從而執(zhí)行相同的業(yè)務(wù)筏餐,此時(shí)因?yàn)椴l(fā)導(dǎo)致分布式鎖失去了意義。

如果你說我可以通過在key快要過期的時(shí)候判斷下任務(wù)有沒有執(zhí)行完畢穆律,如果還沒有那就自動延長過期時(shí)間,那么確實(shí)可以解決并發(fā)的問題罢杉,但是超時(shí)時(shí)長也就失去了意義贡歧,我設(shè)置超時(shí)時(shí)長就是為了想在超時(shí)的時(shí)候自動釋放鎖赋秀,避免其他進(jìn)程被阻塞猎莲。所以個(gè)人認(rèn)為最好的解決方式是在鎖超時(shí)的時(shí)候通知服務(wù)器去停掉超時(shí)任務(wù),但是結(jié)合上Redis的消息通知機(jī)制不免有些過重了樟遣;或者讓業(yè)務(wù)代碼自己去檢查是否執(zhí)行超時(shí)身笤,但是工具不就是讓業(yè)務(wù)實(shí)現(xiàn)人員更加關(guān)注業(yè)務(wù)嗎?

所以這個(gè)問題上瞻佛,分布式鎖的Redis實(shí)現(xiàn)并不靠譜

單點(diǎn)故障導(dǎo)致的并發(fā)問題

建立主從復(fù)制架構(gòu)伤柄,但是還是會由于主節(jié)點(diǎn)掛掉導(dǎo)致某些數(shù)據(jù)還沒同步就已經(jīng)丟失文搂,所以推薦多主架構(gòu),有N個(gè)獨(dú)立的master服務(wù)器煤蹭,客戶端會向所有的服務(wù)器發(fā)送獲取鎖的操作疯兼。

可以繼續(xù)優(yōu)化的地方

提供多主配置方式與加鎖解鎖實(shí)現(xiàn)

使用訂閱解鎖消息與Semaphore代替Thread.sleep()避免時(shí)間浪費(fèi),可參考Redisson中RedissonLock的lockInterruptibly方法

Java高架構(gòu)師待侵、分布式架構(gòu)姨裸、高可擴(kuò)展、高性能那先、高并發(fā)、性能優(yōu)化斤葱、Spring boot揖闸、Redis汤纸、ActiveMQ衩茸、Nginx、Mycat贮泞、Netty楞慈、Jvm大型分布式項(xiàng)目實(shí)戰(zhàn)學(xué)習(xí)架構(gòu)師視頻免費(fèi)學(xué)習(xí)加群:835638062 點(diǎn)擊鏈接加入群聊【Java高級架構(gòu)】:https://jq.qq.com/?_wv=1027&k=5S3kL3v

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市啃擦,隨后出現(xiàn)的幾起案子囊蓝,更是在濱河造成了極大的恐慌,老刑警劉巖议惰,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慎颗,死亡現(xiàn)場離奇詭異,居然都是意外死亡言询,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門运杭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夫啊,“玉大人,你說我怎么就攤上這事辆憔∑裁校” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵虱咧,是天一觀的道長熊榛。 經(jīng)常有香客問我,道長腕巡,這世上最難降的妖魔是什么玄坦? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上煎楣,老公的妹妹穿的比我還像新娘豺总。我一直安慰自己,他們只是感情好择懂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布喻喳。 她就那樣靜靜地躺著,像睡著了一般困曙。 火紅的嫁衣襯著肌膚如雪表伦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天慷丽,我揣著相機(jī)與錄音绑榴,去河邊找鬼。 笑死盈魁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窃诉。 我是一名探鬼主播杨耙,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼飘痛!你這毒婦竟也來了珊膜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤宣脉,失蹤者是張志新(化名)和其女友劉穎车柠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塑猖,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竹祷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了羊苟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塑陵。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蜡励,靈堂內(nèi)的尸體忽然破棺而出令花,到底是詐尸還是另有隱情,我是刑警寧澤凉倚,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布兼都,位于F島的核電站,受9級特大地震影響稽寒,放射性物質(zhì)發(fā)生泄漏扮碧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一瓦胎、第九天 我趴在偏房一處隱蔽的房頂上張望芬萍。 院中可真熱鬧尤揣,春花似錦、人聲如沸柬祠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漫蛔。三九已至嗜愈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莽龟,已是汗流浹背蠕嫁。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毯盈,地道東北人剃毒。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像搂赋,于是被迫代替她去往敵國和親赘阀。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容