您好,登錄后才能下訂單哦!
這篇“Java分布式鎖的三種實(shí)現(xiàn)方式是什么”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“Java分布式鎖的三種實(shí)現(xiàn)方式是什么”文章吧。
Java中的鎖主要包括synchronized鎖和JUC包中的鎖,這些鎖都是針對(duì)單個(gè)JVM實(shí)例上的鎖,對(duì)于分布式環(huán)境如果我們需要加鎖就顯得無(wú)能為力。
在單個(gè)JVM實(shí)例上,鎖的競(jìng)爭(zhēng)者通常是一些不同的線程,而在分布式環(huán)境中,鎖的競(jìng)爭(zhēng)者通常是一些不同的線程或者進(jìn)程。如何實(shí)現(xiàn)在分布式環(huán)境中對(duì)一個(gè)對(duì)象進(jìn)行加鎖呢?答案就是分布式鎖。
目前分布式鎖的實(shí)現(xiàn)方案主要包括三種:
基于數(shù)據(jù)庫(kù)(唯一索引)
基于緩存(Redis,memcached,tair)
基于Zookeeper
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖:主要是利用數(shù)據(jù)庫(kù)的唯一索引來(lái)實(shí)現(xiàn),唯一索引天然具有排他性,這剛好符合我們對(duì)鎖的要求:同一時(shí)刻只能允許一個(gè)競(jìng)爭(zhēng)者獲取鎖。加鎖時(shí)我們?cè)跀?shù)據(jù)庫(kù)中插入一條鎖記錄,利用業(yè)務(wù)id進(jìn)行防重。當(dāng)?shù)谝粋€(gè)競(jìng)爭(zhēng)者加鎖成功后,第二個(gè)競(jìng)爭(zhēng)者再來(lái)加鎖就會(huì)拋出唯一索引沖突,如果拋出這個(gè)異常,我們就判定當(dāng)前競(jìng)爭(zhēng)者加鎖失敗。防重業(yè)務(wù)id需要我們自己來(lái)定義,例如我們的鎖對(duì)象是一個(gè)方法,則我們的業(yè)務(wù)防重id就是這個(gè)方法的名字,如果鎖定的對(duì)象是一個(gè)類(lèi),則業(yè)務(wù)防重id就是這個(gè)類(lèi)名。
基于緩存實(shí)現(xiàn)分布式鎖:理論上來(lái)說(shuō)使用緩存來(lái)實(shí)現(xiàn)分布式鎖的效率最高,加鎖速度最快,因?yàn)镽edis幾乎都是純內(nèi)存操作,而基于數(shù)據(jù)庫(kù)的方案和基于Zookeeper的方案都會(huì)涉及到磁盤(pán)文件IO,效率相對(duì)低下。一般使用Redis來(lái)實(shí)現(xiàn)分布式鎖都是利用Redis的SETNX key value這個(gè)命令,只有當(dāng)key不存在時(shí)才會(huì)執(zhí)行成功,如果key已經(jīng)存在則命令執(zhí)行失敗。
基于Zookeeper:Zookeeper一般用作配置中心,其實(shí)現(xiàn)分布式鎖的原理和Redis類(lèi)似,我們?cè)赯ookeeper中創(chuàng)建瞬時(shí)節(jié)點(diǎn),利用節(jié)點(diǎn)不能重復(fù)創(chuàng)建的特性來(lái)保證排他性。
在實(shí)現(xiàn)分布式鎖的時(shí)候我們需要考慮一些問(wèn)題,例如:分布式鎖是否可重入,分布式鎖的釋放時(shí)機(jī),分布式鎖服務(wù)端是否有單點(diǎn)問(wèn)題等。
上面已經(jīng)分析了基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的基本原理:通過(guò)唯一索引保持排他性,加鎖時(shí)插入一條記錄,解鎖是刪除這條記錄。下面我們就簡(jiǎn)要實(shí)現(xiàn)一下基于數(shù)據(jù)庫(kù)的分布式鎖。
表設(shè)計(jì)
CREATE TABLE `distributed_lock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `unique_mutex` varchar(255) NOT NULL COMMENT '業(yè)務(wù)防重id', `holder_id` varchar(255) NOT NULL COMMENT '鎖持有者id', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `mutex_index` (`unique_mutex`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
id字段是數(shù)據(jù)庫(kù)的自增id,unique_mutex字段就是我們的防重id,也就是加鎖的對(duì)象,此對(duì)象唯一。在這張表上我們加了一個(gè)唯一索引,保證unique_mutex唯一性。holder_id代表競(jìng)爭(zhēng)到鎖的持有者id。
加鎖
insert into distributed_lock(unique_mutex, holder_id) values (‘unique_mutex', ‘holder_id');
如果當(dāng)前sql執(zhí)行成功代表加鎖成功,如果拋出唯一索引異常(DuplicatedKeyException)則代表加鎖失敗,當(dāng)前鎖已經(jīng)被其他競(jìng)爭(zhēng)者獲取。
解鎖
delete from methodLock where unique_mutex=‘unique_mutex' and holder_id=‘holder_id';
解鎖很簡(jiǎn)單,直接刪除此條記錄即可。
分析
是否可重入:就以上的方案來(lái)說(shuō),我們實(shí)現(xiàn)的分布式鎖是不可重入的,即是是同一個(gè)競(jìng)爭(zhēng)者,在獲取鎖后未釋放鎖之前再來(lái)加鎖,一樣會(huì)加鎖失敗,因此是不可重入的。解決不可重入問(wèn)題也很簡(jiǎn)單:加鎖時(shí)判斷記錄中是否存在unique_mutex的記錄,如果存在且holder_id和當(dāng)前競(jìng)爭(zhēng)者id相同,則加鎖成功。這樣就可以解決不可重入問(wèn)題。
鎖釋放時(shí)機(jī):設(shè)想如果一個(gè)競(jìng)爭(zhēng)者獲取鎖時(shí)候,進(jìn)程掛了,此時(shí)distributed_lock表中的這條記錄就會(huì)一直存在,其他競(jìng)爭(zhēng)者無(wú)法加鎖。為了解決這個(gè)問(wèn)題,每次加鎖之前我們先判斷已經(jīng)存在的記錄的創(chuàng)建時(shí)間和當(dāng)前系統(tǒng)時(shí)間之間的差是否已經(jīng)超過(guò)超時(shí)時(shí)間,如果已經(jīng)超過(guò)則先刪除這條記錄,再插入新的記錄。另外在解鎖時(shí),必須是鎖的持有者來(lái)解鎖,其他競(jìng)爭(zhēng)者無(wú)法解鎖。這點(diǎn)可以通過(guò)holder_id字段來(lái)判定。
數(shù)據(jù)庫(kù)單點(diǎn)問(wèn)題:?jiǎn)蝹€(gè)數(shù)據(jù)庫(kù)容易產(chǎn)生單點(diǎn)問(wèn)題:如果數(shù)據(jù)庫(kù)掛了,我們的鎖服務(wù)就掛了。對(duì)于這個(gè)問(wèn)題,可以考慮實(shí)現(xiàn)數(shù)據(jù)庫(kù)的高可用方案,例如MySQL的MHA高可用解決方案。
前置知識(shí):
Zookeeper的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)就像一棵樹(shù),這棵樹(shù)由節(jié)點(diǎn)組成,這種節(jié)點(diǎn)叫做Znode。
Znode分為四種類(lèi)型:
持久節(jié)點(diǎn)(PERSISTENT):默認(rèn)的節(jié)點(diǎn)類(lèi)型。創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開(kāi)連接后,該節(jié)點(diǎn)依舊存在 。
持久節(jié)點(diǎn)順序節(jié)點(diǎn)(PERSISTENT_SEQUENTIAL): 所謂順序節(jié)點(diǎn),就是在創(chuàng)建節(jié)點(diǎn)時(shí),Zookeeper根據(jù)創(chuàng)建的時(shí)間順序給該節(jié)點(diǎn)名稱(chēng)進(jìn)行編號(hào):
臨時(shí)節(jié)點(diǎn)(EPHEMERAL) :和持久節(jié)點(diǎn)相反,當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開(kāi)連接后,臨時(shí)節(jié)點(diǎn)會(huì)被刪除。
臨時(shí)順序節(jié)點(diǎn)(EPHEMERAL_SEQUENTIAL) :顧名思義,臨時(shí)順序節(jié)點(diǎn)結(jié)合和臨時(shí)節(jié)點(diǎn)和順序節(jié)點(diǎn)的特點(diǎn):在創(chuàng)建節(jié)點(diǎn)時(shí),Zookeeper根據(jù)創(chuàng)建的時(shí)間順序給該節(jié)點(diǎn)名稱(chēng)進(jìn)行編號(hào);當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與Zookeeper斷開(kāi)連接后,臨時(shí)節(jié)點(diǎn)會(huì)被刪除。
Zookeeper分布式鎖恰恰應(yīng)用了臨時(shí)順序節(jié)點(diǎn)。具體如何實(shí)現(xiàn)呢?讓我們來(lái)看一看詳細(xì)步驟:
獲取鎖
首先,在Zookeeper當(dāng)中創(chuàng)建一個(gè)持久節(jié)點(diǎn)ParentLock。當(dāng)?shù)谝粋€(gè)客戶端想要獲得鎖時(shí),需要在ParentLock這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn) Lock1。
之后,Client1查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock1是不是順序最靠前的一個(gè)。如果是第一個(gè)節(jié)點(diǎn),則成功獲得鎖。
這時(shí)候,如果再有一個(gè)客戶端 Client2 前來(lái)獲取鎖,則在ParentLock下載再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock2。
Client2查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock2是不是順序最靠前的一個(gè),結(jié)果發(fā)現(xiàn)節(jié)點(diǎn)Lock2并不是最小的。
于是,Client2向排序僅比它靠前的節(jié)點(diǎn)Lock1注冊(cè)Watcher,用于監(jiān)聽(tīng)Lock1節(jié)點(diǎn)是否存在。這意味著Client2搶鎖失敗,進(jìn)入了等待狀態(tài)。
這時(shí)候,如果又有一個(gè)客戶端Client3前來(lái)獲取鎖,則在ParentLock下載再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock3。
Client3查找ParentLock下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn)Lock3是不是順序最靠前的一個(gè),結(jié)果同樣發(fā)現(xiàn)節(jié)點(diǎn)Lock3并不是最小的。
于是,Client3向排序僅比它靠前的節(jié)點(diǎn)Lock2注冊(cè)Watcher,用于監(jiān)聽(tīng)Lock2節(jié)點(diǎn)是否存在。這意味著Client3同樣搶鎖失敗,進(jìn)入了等待狀態(tài)。
這樣一來(lái),Client1得到了鎖,Client2監(jiān)聽(tīng)了Lock1,Client3監(jiān)聽(tīng)了Lock2。這恰恰形成了一個(gè)等待隊(duì)列,很像是Java當(dāng)中ReentrantLock(可重入鎖)所依賴(lài)的AQS(AbstractQueuedSynchronizer)。
獲得鎖的過(guò)程大致就是這樣,那么Zookeeper如何釋放鎖呢?
釋放鎖的過(guò)程很簡(jiǎn)單,只需要釋放對(duì)應(yīng)的子節(jié)點(diǎn)就好。
釋放鎖
釋放鎖分為兩種情況:
1.任務(wù)完成,客戶端顯示釋放
當(dāng)任務(wù)完成時(shí),Client1會(huì)顯示調(diào)用刪除節(jié)點(diǎn)Lock1的指令。
2.任務(wù)執(zhí)行過(guò)程中,客戶端崩潰
獲得鎖的Client1在任務(wù)執(zhí)行過(guò)程中,如果Duang的一聲崩潰,則會(huì)斷開(kāi)與Zookeeper服務(wù)端的鏈接。根據(jù)臨時(shí)節(jié)點(diǎn)的特性,相關(guān)聯(lián)的節(jié)點(diǎn)Lock1會(huì)隨之自動(dòng)刪除。
由于Client2一直監(jiān)聽(tīng)著Lock1的存在狀態(tài),當(dāng)Lock1節(jié)點(diǎn)被刪除,Client2會(huì)立刻收到通知。這時(shí)候Client2會(huì)再次查詢ParentLock下面的所有節(jié)點(diǎn),確認(rèn)自己創(chuàng)建的節(jié)點(diǎn)Lock2是不是目前最小的節(jié)點(diǎn)。如果是最小,則Client2順理成章獲得了鎖。
同理,如果Client2也因?yàn)槿蝿?wù)完成或者節(jié)點(diǎn)崩潰而刪除了節(jié)點(diǎn)Lock2,那么Client3就會(huì)接到通知。
最終,Client3成功得到了鎖。
使用Zookeeper實(shí)現(xiàn)分布式鎖的大致流程就是這樣。
分析
解決不可重入:客戶端加鎖時(shí)將主機(jī)和線程信息寫(xiě)入鎖中,下一次再來(lái)加鎖時(shí)直接和序列最小的節(jié)點(diǎn)對(duì)比,如果相同,則加鎖成功,鎖重入。
鎖釋放時(shí)機(jī):由于我們創(chuàng)建的節(jié)點(diǎn)是順序臨時(shí)節(jié)點(diǎn),當(dāng)客戶端獲取鎖成功之后突然session會(huì)話斷開(kāi),ZK會(huì)自動(dòng)刪除這個(gè)臨時(shí)節(jié)點(diǎn)。
單點(diǎn)問(wèn)題:ZK是集群部署的,主要一半以上的機(jī)器存活,就可以保證服務(wù)可用性。
Zookeeper第三方客戶端curator中已經(jīng)實(shí)現(xiàn)了基于Zookeeper的分布式鎖。利用curator加鎖和解鎖的代碼如下:
@Autowired private CuratorFramework curatorFramework; // 加鎖,支持超時(shí),可重入 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { // InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParenLock"); try { return interProcessMutex.acquire(timeout, unit); } catch (Exception e) { e.printStackTrace(); } return true; } // 解鎖 public boolean unlock() { InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParenLock"); try { interProcessMutex.release(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS); } return true; }
最常用的鎖:
InterProcessMutex
:分布式可重入排它鎖
InterProcessSemaphoreMutex
:分布式排它鎖
InterProcessReadWriteLock
:分布式讀寫(xiě)鎖
加鎖
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 加鎖 * @param stringRedisTemplate Redis客戶端 * @param lockKey 鎖的key * @param requestId 競(jìng)爭(zhēng)者id * @param expireTime 鎖超時(shí)時(shí)間,超時(shí)之后鎖自動(dòng)釋放 * @return */ public static boolean getDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String requestId, int expireTime) { return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); } }
可以看到,我們加鎖就一行代碼:
stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
這個(gè)setIfAbsent()方法一共五個(gè)形參:
第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。
第二個(gè)為value,這里寫(xiě)的是鎖競(jìng)爭(zhēng)者的id,在解鎖時(shí),我們需要判斷當(dāng)前解鎖的競(jìng)爭(zhēng)者id是否為鎖持有者。
第三個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期時(shí)間的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定;
第四個(gè)參數(shù)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。
總的來(lái)說(shuō),執(zhí)行上面的setIfAbsent()方法就只會(huì)導(dǎo)致兩種結(jié)果:
1.當(dāng)前沒(méi)有鎖(key不存在),那么就進(jìn)行加鎖操作,并對(duì)鎖設(shè)置一個(gè)有效期,同時(shí)value表示加鎖的客戶端。
2.已經(jīng)有鎖存在,不做任何操作。上述解鎖請(qǐng)求中,緩存超時(shí)機(jī)制保證了即使一個(gè)競(jìng)爭(zhēng)者加鎖之后掛了,也不會(huì)產(chǎn)生死鎖問(wèn)題:超時(shí)之后其他競(jìng)爭(zhēng)者依然可以獲取鎖。通過(guò)設(shè)置value為競(jìng)爭(zhēng)者的id,保證了只有鎖的持有者才能來(lái)解鎖,否則任何競(jìng)爭(zhēng)者都能解鎖,那豈不是亂套了。
解鎖
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分布式鎖 * @param stringRedisTemplate Redis客戶端 * @param lockKey 鎖 * @param requestId 鎖持有者id * @return 是否釋放成功 */ public static boolean releaseDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), requestId); return RELEASE_SUCCESS.equals(result); } }
解鎖的步驟:
1、判斷當(dāng)前解鎖的競(jìng)爭(zhēng)者id是否為鎖的持有者,如果不是直接返回失敗,如果是則進(jìn)入第2步。
2、刪除key,如果刪除成功,返回解鎖成功,否則解鎖失敗。
注意到這里解鎖其實(shí)是分為2個(gè)步驟,涉及到解鎖操作的一個(gè)原子性操作問(wèn)題。這也是為什么我們解鎖的時(shí)候用Lua腳本來(lái)實(shí)現(xiàn),因?yàn)長(zhǎng)ua腳本可以保證操作的原子性。那么這里為什么需要保證這兩個(gè)步驟的操作是原子操作呢?
設(shè)想:假設(shè)當(dāng)前鎖的持有者是競(jìng)爭(zhēng)者1,競(jìng)爭(zhēng)者1來(lái)解鎖,成功執(zhí)行第1步,判斷自己就是鎖持有者,這是還未執(zhí)行第2步。這是鎖過(guò)期了,然后競(jìng)爭(zhēng)者2對(duì)這個(gè)key進(jìn)行了加鎖。加鎖完成后,競(jìng)爭(zhēng)者1又來(lái)執(zhí)行第2步,此時(shí)錯(cuò)誤產(chǎn)生了:競(jìng)爭(zhēng)者1解鎖了不屬于自己持有的鎖。可能會(huì)有人問(wèn)為什么競(jìng)爭(zhēng)者1執(zhí)行完第1步之后突然停止了呢?這個(gè)問(wèn)題其實(shí)很好回答,例如競(jìng)爭(zhēng)者1所在的JVM發(fā)生了GC停頓,導(dǎo)致競(jìng)爭(zhēng)者1的線程停頓。這樣的情況發(fā)生的概率很低,但是請(qǐng)記住即使只有萬(wàn)分之一的概率,在線上環(huán)境中完全可能發(fā)生。因此必須保證這兩個(gè)步驟的操作是原子操作。
分析
是否可重入:以上實(shí)現(xiàn)的鎖是不可重入的,如果需要實(shí)現(xiàn)可重入,在SET_IF_NOT_EXIST之后,再判斷key對(duì)應(yīng)的value是否為當(dāng)前競(jìng)爭(zhēng)者id,如果是返回加鎖成功,否則失敗。
鎖釋放時(shí)機(jī):加鎖時(shí)我們?cè)O(shè)置了key的超時(shí),當(dāng)超時(shí)后,如果還未解鎖,則自動(dòng)刪除key達(dá)到解鎖的目的。如果一個(gè)競(jìng)爭(zhēng)者獲取鎖之后掛了,我們的鎖服務(wù)最多也就在超時(shí)時(shí)間的這段時(shí)間之內(nèi)不可用。
Redis單點(diǎn)問(wèn)題:如果需要保證鎖服務(wù)的高可用,可以對(duì)Redis做高可用方案:Redis集群+主從切換。目前都有比較成熟的解決方案。
redis分布式鎖,更詳細(xì)的可以參考:分布式鎖(Redisson)原理分析
方案 | 理解難易程度 | 實(shí)現(xiàn)的復(fù)雜度 | 性能 | 可靠性 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|---|---|---|---|
基于數(shù)據(jù)庫(kù) | 容易 | 復(fù)雜 | 差 | 不可靠 | ||
基于緩存(Redis) | 一般 | 一般 | 高 | 可靠 | Set和Del指令性能較高 | 1.實(shí)現(xiàn)復(fù)雜,需要考慮超時(shí),原子性,誤刪等情形。2.沒(méi)有等待鎖的隊(duì)列,只能在客戶端自旋來(lái)等待,效率低下。(但是現(xiàn)在有Redisson這兩缺點(diǎn)就相當(dāng)于沒(méi)有了) |
基于Zookeeper | 難 | 簡(jiǎn)單 | 一般 | 一般 | 1.有封裝好的框架,容易實(shí)現(xiàn)2.有等待鎖的隊(duì)列,大大提升搶鎖效率。 | 添加和刪除節(jié)點(diǎn)性能較低 |
以上就是關(guān)于“Java分布式鎖的三種實(shí)現(xiàn)方式是什么”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。