Java分布式鎖由淺入深介紹
一、分布式鎖介紹
單機(jī)多線程: 在 Java 中,我們通常使用 ReetrantLock 類、synchronized 關(guān)鍵字這類 本地鎖 來(lái)控制一個(gè) JVM 進(jìn)程內(nèi)的多個(gè)線程對(duì)本地共享資源的訪問(wèn)
分布式系統(tǒng): 不同的服務(wù)/客戶端通常運(yùn)行在獨(dú)立的 JVM 進(jìn)程上。如果多個(gè) JVM 進(jìn)程共享同一份資源的話,使用本地鎖就沒(méi)辦法實(shí)現(xiàn)資源的互斥訪問(wèn)了。于是,分布式鎖就誕生了。
舉個(gè)例子:系統(tǒng)的訂單服務(wù)一共部署了 3 份,都對(duì)外提供服務(wù)。用戶下訂單之前需要檢查庫(kù)存,為了防止超賣,這里需要加鎖以實(shí)現(xiàn)對(duì)檢查庫(kù)存操作的同步訪問(wèn)。由于訂單服務(wù)位于不同的 JVM 進(jìn)程中,本地鎖在這種情況下就沒(méi)辦法正常工作了。我們需要用到分布式鎖,這樣的話,即使多個(gè)線程不在同一個(gè) JVM 進(jìn)程中也能獲取到同一把鎖,進(jìn)而實(shí)現(xiàn)共享資源的互斥訪問(wèn)。
一個(gè)最基本的分布式鎖需要滿足:
- 互斥 :任意一個(gè)時(shí)刻,鎖只能被一個(gè)線程持有;
- 高可用 :鎖服務(wù)是高可用的。并且,即使客戶端的釋放鎖的代碼邏輯出現(xiàn)問(wèn)題,鎖最終一定還是會(huì)被釋放,不會(huì)影響其他線程對(duì)共享資源的訪問(wèn)。
- 可重入:一個(gè)節(jié)點(diǎn)獲取了鎖之后,還可以再次獲取鎖。
二、基于Redis實(shí)現(xiàn)分布式鎖
1. 如何基于 Redis 實(shí)現(xiàn)一個(gè)最簡(jiǎn)易的分布式鎖
不論是本地鎖還是分布式鎖,核心都在于==“互斥”==。
在 Redis 中, SETNX
命令是可以幫助我們實(shí)現(xiàn)互斥。SETNX
即 SET if Not eXists (對(duì)應(yīng) Java 中的 setIfAbsent 方法),如果 key 不存在的話,才會(huì)設(shè)置 key 的值。如果 key 已經(jīng)存在, SETNX 啥也不做。
> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
釋放鎖的話,直接通過(guò) DEL 命令刪除對(duì)應(yīng)的 key 即可
> DEL lockKey
(integer) 1
為了防止誤刪到其他的鎖,這里我們建議使用 Lua 腳本通過(guò) key 對(duì)應(yīng)的 value(唯一值)來(lái)判斷。
選用 Lua 腳本是為了保證解鎖操作的原子性。因?yàn)?Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,從而保證了鎖釋放操作的原子性。
// 釋放鎖時(shí),先比較鎖對(duì)應(yīng)的 value 值是否相等,避免鎖的誤釋放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
這是一種最簡(jiǎn)易的 Redis 分布式鎖實(shí)現(xiàn),實(shí)現(xiàn)方式比較簡(jiǎn)單,性能也很高效。不過(guò),這種方式實(shí)現(xiàn)分布式鎖存在一些問(wèn)題。就比如應(yīng)用程序遇到一些問(wèn)題比如釋放鎖的邏輯突然掛掉,可能會(huì)導(dǎo)致鎖無(wú)法被釋放,進(jìn)而造成共享資源無(wú)法再被其他線程/進(jìn)程訪問(wèn)。
2. 為什么要給鎖設(shè)置一個(gè)過(guò)期時(shí)間
主要為了避免鎖無(wú)法被釋放
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
- lockKey :加鎖的鎖名;
- uniqueValue :能夠唯一標(biāo)示鎖的隨機(jī)字符串;
- NX :只有當(dāng) lockKey 對(duì)應(yīng)的 key 值不存在的時(shí)候才能 SET 成功;
- EX :過(guò)期時(shí)間設(shè)置(秒為單位)EX 3 標(biāo)示這個(gè)鎖有一個(gè) 3 秒的自動(dòng)過(guò)期時(shí)間。與 EX 對(duì)應(yīng)的是 PX(毫秒為單位),這兩個(gè)都是過(guò)期時(shí)間設(shè)置。
一定要保證設(shè)置指定 key 的值和過(guò)期時(shí)間是一個(gè)原子操作?。。?不然的話,依然可能會(huì)出現(xiàn)鎖無(wú)法被釋放的問(wèn)題。
這種解決辦法同樣存在漏洞:
- 如果操作共享資源的時(shí)間大于過(guò)期時(shí)間,就會(huì)出現(xiàn)鎖提前過(guò)期的問(wèn)題,進(jìn)而導(dǎo)致分布式鎖直接失效
- 如果鎖的超時(shí)時(shí)間設(shè)置過(guò)長(zhǎng),又會(huì)影響到性能
3. 如何實(shí)現(xiàn)鎖的優(yōu)雅續(xù)期
Redisson 是一個(gè)開(kāi)源的 Java 語(yǔ)言 Redis 客戶端,提供了很多開(kāi)箱即用的功能,不僅僅包括多種分布式鎖的實(shí)現(xiàn)。并且,Redisson 還支持 Redis 單機(jī)、Redis Sentinel 、Redis Cluster 等多種部署架構(gòu)。
Redisson 中的分布式鎖自帶自動(dòng)續(xù)期機(jī)制,使用起來(lái)非常簡(jiǎn)單,原理也比較簡(jiǎn)單,其提供了一個(gè)專門用來(lái)監(jiān)控和續(xù)期鎖的 Watch Dog( 看門狗),如果操作共享資源的線程還未執(zhí)行完成的話,Watch Dog 會(huì)不斷地延長(zhǎng)鎖的過(guò)期時(shí)間,進(jìn)而保證鎖不會(huì)因?yàn)槌瑫r(shí)而被釋放。
使用方式舉例:
// 1.獲取指定的分布式鎖對(duì)象
RLock lock = redisson.getLock("lock");
// 2.拿鎖且不設(shè)置鎖超時(shí)時(shí)間,具備 Watch Dog 自動(dòng)續(xù)期機(jī)制
lock.lock();
// 3.執(zhí)行業(yè)務(wù)
...
// 4.釋放鎖
lock.unlock();
只有未指定鎖超時(shí)時(shí)間,才會(huì)使用到 Watch Dog 自動(dòng)續(xù)期機(jī)制。
// 手動(dòng)給鎖設(shè)置過(guò)期時(shí)間,不具備 Watch Dog 自動(dòng)續(xù)期機(jī)制 lock.lock(10, TimeUnit.SECONDS);
總的來(lái)說(shuō)就是使用Redisson,它帶有自動(dòng)的續(xù)期機(jī)制
4. 如何實(shí)現(xiàn)可重入鎖
所謂可重入鎖指的是在一個(gè)線程中可以多次獲取同一把鎖,比如一個(gè)線程在執(zhí)行一個(gè)帶鎖的方法,該方法中又調(diào)用了另一個(gè)需要相同鎖的方法,則該線程可以直接執(zhí)行調(diào)用的方法即可重入 ,而無(wú)需重新獲得鎖。像 Java 中的 synchronized 和 ReentrantLock 都屬于可重入鎖。
可重入分布式鎖的實(shí)現(xiàn)核心思路是線程在獲取鎖的時(shí)候判斷是否為自己的鎖,如果是的話,就不用再重新獲取了。為此,我們可以為每個(gè)鎖關(guān)聯(lián)一個(gè)可重入計(jì)數(shù)器和一個(gè)占有它的線程。當(dāng)可重入計(jì)數(shù)器大于 0 時(shí),則鎖被占有,需要判斷占有該鎖的線程和請(qǐng)求獲取鎖的線程是否為同一個(gè)。
到此這篇關(guān)于Java分布式鎖由淺入深介紹的文章就介紹到這了,更多相關(guān)Java分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis?if?test?判斷字符串相等不生效問(wèn)題
這篇文章主要介紹了MyBatis?if?test?判斷字符串相等不生效問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10Java substring方法實(shí)現(xiàn)原理解析
這篇文章主要介紹了Java substring方法實(shí)現(xiàn)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05運(yùn)用springboot搭建并部署web項(xiàng)目的示例
這篇文章主要介紹了運(yùn)用springboot搭建并部署web項(xiàng)目的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06