外包兼職做圖的網(wǎng)站百度免費(fèi)推廣怎么操作
前言
? 在分布式系統(tǒng)中,當(dāng)不同進(jìn)程或線程一起訪問(wèn)共享資源時(shí),會(huì)造成資源爭(zhēng)搶,如果不加以控制的話,就會(huì)引發(fā)程序錯(cuò)亂。而分布式鎖它采用了一種互斥機(jī)制來(lái)防止線程或進(jìn)程間相互干擾,從而保證了數(shù)據(jù)的一致性。
常見(jiàn)的分布式鎖實(shí)現(xiàn)方案
- 基于 Redis 實(shí)現(xiàn)分布式鎖
- 基于 Zookeeper 實(shí)現(xiàn)分布式鎖
介紹
? **Redission是一個(gè)基于Redis實(shí)現(xiàn)的Java分布式對(duì)象存儲(chǔ)和緩存框架。它提供了豐富的分布式數(shù)據(jù)結(jié)構(gòu)和服務(wù)。**例如:分布式鎖、分布式隊(duì)列、分布式Rate Limiter等。
分布式鎖的特點(diǎn)
- 互斥性是分布式鎖的重要特點(diǎn),在任意時(shí)刻,只有一個(gè)線程能夠持有鎖
- **鎖的超時(shí)時(shí)間。**一個(gè)線程在持鎖期間掛掉了而沒(méi)主動(dòng)釋放鎖,此時(shí)通過(guò)超時(shí)時(shí)間來(lái)保證該線程在超時(shí)后可以釋放鎖,這樣其他線程才可以繼續(xù)獲取鎖;
- 加鎖和解鎖必須是由同一個(gè)線程來(lái)設(shè)置
- Redis 是緩存型數(shù)據(jù)庫(kù),擁有很高的性能,因此加鎖和釋放鎖開(kāi)銷較小,并且能夠很輕易地實(shí)現(xiàn)分布式鎖。
特性
高性能
? Redission是基于Redis的,因此它繼承了Redis的高性能和低延遲的特性。同時(shí),它采用了Netty的NIO框架,能夠并發(fā)地處理大量的請(qǐng)求,使得應(yīng)用程序的響應(yīng)速度得到了極大的提升。
易用性
? Redission提供了豐富的API和方法,同時(shí)還提供了文檔和示例,讓開(kāi)發(fā)者易于上手和使用。此外,它支持自動(dòng)配置和靈活的配置方式,使得開(kāi)發(fā)者可以根據(jù)自己的需求進(jìn)行配置和調(diào)整。
可擴(kuò)展性
? Redission的分布式架構(gòu)使得它支持水平擴(kuò)展,可以將數(shù)據(jù)和請(qǐng)求分散到更多的節(jié)點(diǎn)上進(jìn)行處理。這也使得它具備了更好的容錯(cuò)能力和可靠性。
常用場(chǎng)景
緩存
? Redission支持不同的數(shù)據(jù)存儲(chǔ)類型,例如:String、List、Set、Map、BloomFilter、HyperLogLog等,它可以將這些數(shù)據(jù)存儲(chǔ)在Redis中,以實(shí)現(xiàn)數(shù)據(jù)緩存的功能,從而提高應(yīng)用程序的性能和響應(yīng)速度。
分布式鎖
? Redission提供了可重入鎖、公平鎖等常用的分布式鎖,還支持異步執(zhí)行、鎖的自動(dòng)續(xù)期、鎖的等待等特性。
分布式隊(duì)列
? Redission提供了分布式隊(duì)列的實(shí)現(xiàn),我們可以在不同的節(jié)點(diǎn)之間快速、可靠地實(shí)現(xiàn)任務(wù)的傳遞和處理。
Redis分布式鎖命令
常用命令
? Redis 分布式鎖常用命令如下所示:
- SETNX key val:僅當(dāng)key不存在時(shí),設(shè)置一個(gè) key 為 value 的字符串,返回1;若 key 存在,設(shè)置失敗,返回 0;
- Expire key timeout:為 key 設(shè)置一個(gè)超時(shí)時(shí)間,以 second 秒為單位,超過(guò)這個(gè)時(shí)間鎖會(huì)自動(dòng)釋放,避免死鎖;
- DEL key:刪除 key。
**上述 SETNX 命令相當(dāng)于搶占鎖操作,EXPIRE 是為避免出現(xiàn)意外用來(lái)設(shè)置鎖的過(guò)期時(shí)間,也就是說(shuō)到了指定的過(guò)期時(shí)間,該客戶端必須讓出鎖,讓其他客戶端去持有。**
? 但還有一種情況,如果在 SETNX 和 EXPIRE 之間服務(wù)器進(jìn)程突然掛掉,也就是還未設(shè)置過(guò)期時(shí)間,這樣就會(huì)導(dǎo)致 EXPIRE 執(zhí)行不了,因此還是會(huì)造成“死鎖”的問(wèn)題。為了避免這個(gè)問(wèn)題,Redis 作者在 2.6.12 版本后,對(duì) SET 命令參數(shù)做了擴(kuò)展,使它可以同時(shí)執(zhí)行 SETNX 和 EXPIRE 命令,從而解決了死鎖的問(wèn)題。
直接使用 SET 命令實(shí)現(xiàn),語(yǔ)法格式如下:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
- EX second:設(shè)置鍵的過(guò)期時(shí)間為 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
- PX millisecond:設(shè)置鍵的過(guò)期時(shí)間為毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecondvalue 。
- NX:只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。 SET key value NX 效果等同于 SETNX key value 。
- XX:只在鍵已經(jīng)存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
原理
使用
使用redisson實(shí)現(xiàn)分布式鎖的操作步驟,三部曲
- 第一步: 獲取鎖 RLock redissonLock = redisson.getLock(lockKey);
- 第二步: 加鎖,實(shí)現(xiàn)鎖續(xù)命功能 redissonLock.lock();
- 第三步: 釋放鎖 redissonLock.unlock();
Redis 實(shí)現(xiàn)分布式鎖主要步驟
- 指定一個(gè) key 作為鎖標(biāo)記,存入 Redis 中,指定一個(gè) 唯一的用戶標(biāo)識(shí) 作為 value。
- 當(dāng) key 不存在時(shí)才能設(shè)置值,確保同一時(shí)間只有一個(gè)客戶端進(jìn)程獲得鎖,滿足 互斥性 特性。
- 設(shè)置一個(gè)過(guò)期時(shí)間,防止因系統(tǒng)異常導(dǎo)致沒(méi)能刪除這個(gè) key,滿足 防死鎖 特性。
- 當(dāng)處理完業(yè)務(wù)之后需要清除這個(gè) key 來(lái)釋放鎖,清除 key 時(shí)需要校驗(yàn) value 值,需要滿足 只有加鎖的人才能釋放鎖 。
注意:
? 以上實(shí)現(xiàn)步驟考慮到了使用分布式鎖需要考慮的互斥性、防死鎖、加鎖和解鎖必須為同一個(gè)進(jìn)程等問(wèn)題,但是鎖的續(xù)期無(wú)法實(shí)現(xiàn)。所以通常情況都是采用 Redisson 實(shí)現(xiàn) Redis 的分布式鎖,借助 Redisson 的 WatchDog 機(jī)制 能夠很好的解決鎖續(xù)期的問(wèn)題。
? Watch Dog 機(jī)制其實(shí)就是一個(gè)后臺(tái)定時(shí)任務(wù)線程,獲取鎖成功之后,會(huì)將持有鎖的線程放入到一個(gè)
RedissonLock.EXPIRATION_RENEWAL_MAP
里面,然后每隔 10 秒(internalLockLeaseTime / 3)
檢查一下,如果客戶端 1 還持有鎖 key(判斷客戶端是否還持有 key,其實(shí)就是遍歷EXPIRATION_RENEWAL_MAP
里面線程 id 然后根據(jù)線程 id 去 Redis 中查,如果存在就會(huì)延長(zhǎng) key 的時(shí)間),那么就會(huì)不斷的延長(zhǎng)鎖 key 的生存時(shí)間。
Redisson的鎖機(jī)制
1)加鎖機(jī)制
? 加鎖其實(shí)是通過(guò)一段 lua 腳本實(shí)現(xiàn)的。這里 KEYS[1]
代表的是你加鎖的 key,比如你自己設(shè)置了加鎖的那個(gè)鎖 key 就是 “myLock”。
if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +
"return redis.call('pttl', KEYS[1]);"
? ARGV[1]
代表的是鎖 key 的默認(rèn)生存時(shí)間,默認(rèn) 30 秒。ARGV[2]
代表的是加鎖的客戶端的 ID,類似于下面這樣:285475da-9152-4c83-822a-67ee2f116a79:52。至于最后面的一個(gè) 1 是為了后面可重入做的計(jì)數(shù)統(tǒng)計(jì).
? 第一段 if 判斷語(yǔ)句,就是用 exists myLock
命令判斷一下,如果你要加鎖的那個(gè)鎖 key 不存在的話,你就進(jìn)行加鎖。如何加鎖呢?使用 hincrby
命令設(shè)置一個(gè) hash 結(jié)構(gòu)。接著會(huì)執(zhí)行 pexpire myLock 30000
命令,設(shè)置 myLock 這個(gè)鎖 key 的生存時(shí)間是 30 秒。到此為止,加鎖完成。
2)鎖互斥機(jī)制
? 第二個(gè) if 判斷,判斷一下,myLock 鎖 key 的 hash 數(shù)據(jù)結(jié)構(gòu)中,是否包含客戶端 2 的 ID,這里明顯不是,因?yàn)槟抢锇氖强蛻舳?1 的 ID。所以,客戶端 2 會(huì)執(zhí)行:
return redis.call('pttl', KEYS[1]);
返回的一個(gè)數(shù)字,這個(gè)數(shù)字代表了 myLock 這個(gè)鎖 key 的剩余生存時(shí)間。
3)Watch dog 機(jī)制
? 主要用于鎖續(xù)費(fèi)服務(wù)。只要客戶端 1 一旦加鎖成功,就會(huì)啟動(dòng)一個(gè) Watch Dog。也就是說(shuō)leaseTime
必須是 -1 才會(huì)開(kāi)啟 Watch Dog 機(jī)制,也就是如果你想開(kāi)啟 Watch Dog 機(jī)制必須使用默認(rèn)的加鎖時(shí)間為 30s。
4)可重入加鎖機(jī)制
? 當(dāng)鎖key已經(jīng)存在,第二個(gè) if 判斷會(huì)成立,因?yàn)?myLock 的 hash 數(shù)據(jù)結(jié)構(gòu)中包含的那個(gè) ID 即客戶端 1 的 ID,此時(shí)就會(huì)執(zhí)行可重入加鎖的邏輯,
5)鎖釋放機(jī)制
- 刪除鎖。
- 廣播釋放鎖的消息,通知阻塞等待的進(jìn)程(向通道名為
redisson_lock__channel
publish 一條UNLOCK_MESSAGE
信息)。 - 取消 Watch Dog 機(jī)制,即將
RedissonLock.EXPIRATION_RENEWAL_MAP
里面的線程 id 刪除,并且 cancel 掉 Netty 的那個(gè)定時(shí)任務(wù)線程。
源碼分析
? 重點(diǎn)主要是依賴lua腳本的原子性,實(shí)現(xiàn)加鎖和釋放鎖的功能。
實(shí)例化RedissonLock
- super(commandExecutor, name); 父類name賦值,后續(xù)通過(guò)getName()獲取
- commandExecutor: 執(zhí)行l(wèi)ua腳本的executor
- id 是個(gè)UUID, 后面被用來(lái)當(dāng)做和threadId組成 value值**,用作判斷加鎖和釋放鎖是否是同一個(gè)線程的校驗(yàn)。**
- internalLockLeaseTime : 取自 Config#lockWatchdogTimeout,默認(rèn)30秒,這個(gè)參數(shù)還有另外一個(gè)作用,鎖續(xù)命的執(zhí)行周期 internalLockLeaseTime/3 = 10秒
加鎖和鎖的續(xù)命redissonLock.lock()
-
**Thread.currentThread().interrupt()。**當(dāng)發(fā)生異常,則通知線程進(jìn)行中斷并釋放鎖。
-
lockInterruptibly(-1, null); 此環(huán)節(jié)會(huì)先獲取當(dāng)前線程的線程ID,之后會(huì)嘗試獲取鎖的剩余時(shí)間。
? 如果當(dāng)前鎖的剩余時(shí)間為null,說(shuō)明沒(méi)有線程持有該鎖,直接返回讓當(dāng)前線程加鎖成功。如果當(dāng)前線程的剩余時(shí)間不為null,就會(huì)一直嘗試獲取鎖。
? Redisson 提供了一個(gè)續(xù)期機(jī)制, 只要客戶端 1 一旦加鎖成功,就會(huì)啟動(dòng)一個(gè) Watch Dog。**也就是說(shuō)
leaseTime
必須是 -1 才會(huì)開(kāi)啟 Watch Dog 機(jī)制,也就是如果你想開(kāi)啟 Watch Dog 機(jī)制必須使用默認(rèn)的加鎖時(shí)間為 30s。**如果你自己自定義時(shí)間,超過(guò)這個(gè)時(shí)間,鎖就會(huì)自定釋放,并不會(huì)延長(zhǎng)。 -
**tryAcquireAsync(leaseTime, unit, threadId)(自旋獲取鎖的方法)。**其內(nèi)部專門(mén)創(chuàng)建異步任務(wù)用于嘗試獲取鎖。其任務(wù)內(nèi)部會(huì)注冊(cè)監(jiān)聽(tīng)事件,當(dāng)剩余時(shí)間為null,就會(huì)再次去獲取鎖,并給鎖延長(zhǎng)過(guò)期時(shí)間。
-
tryLockInnerAsync 其內(nèi)部實(shí)現(xiàn)是lua腳本。
-
scheduleExpirationRenewal(final long threadId)。又是lua腳本 判斷是否存在,存在就調(diào)用pexpire
釋放鎖redissonLock.unlock()
- unlock()。先獲取鎖狀態(tài),如果鎖狀態(tài)為null,則拋出異常。否則就釋放鎖,并刪除key。
- unlockInnerAsync(long threadId)。unlock的內(nèi)部實(shí)現(xiàn),也是lua腳本,用于獲取當(dāng)前線程的鎖狀態(tài)。
Redissson tryLock源碼
@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();// 1.嘗試獲取鎖Long ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}// 申請(qǐng)鎖的耗時(shí)如果大于等于最大等待時(shí)間,則申請(qǐng)鎖失敗.time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(threadId);return false;}current = System.currentTimeMillis();/*** 2.訂閱鎖釋放事件,并通過(guò) await 方法阻塞等待鎖釋放,有效的解決了無(wú)效的鎖申請(qǐng)浪費(fèi)資源的問(wèn)題:* 基于信息量,當(dāng)鎖被其它資源占用時(shí),當(dāng)前線程通過(guò) Redis 的 channel 訂閱鎖的釋放事件,一旦鎖釋放會(huì)發(fā)消息通知待等待的線程進(jìn)行競(jìng)爭(zhēng).** 當(dāng) this.await 返回 false,說(shuō)明等待時(shí)間已經(jīng)超出獲取鎖最大等待時(shí)間,取消訂閱并返回獲取鎖失敗.* 當(dāng) this.await 返回 true,進(jìn)入循環(huán)嘗試獲取鎖.*/RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);// await 方法內(nèi)部是用 CountDownLatch 來(lái)實(shí)現(xiàn)阻塞,獲取 subscribe 異步執(zhí)行的結(jié)果(應(yīng)用了 Netty 的 Future)if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {unsubscribe(subscribeFuture, threadId);}});}acquireFailed(threadId);return false;}try {// 計(jì)算獲取鎖的總耗時(shí),如果大于等于最大等待時(shí)間,則獲取鎖失敗.time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(threadId);return false;}/*** 3.收到鎖釋放的信號(hào)后,在最大等待時(shí)間之內(nèi),循環(huán)一次接著一次的嘗試獲取鎖* 獲取鎖成功,則立馬返回 true,* 若在最大等待時(shí)間之內(nèi)還沒(méi)獲取到鎖,則認(rèn)為獲取鎖失敗,返回 false 結(jié)束循環(huán)*/while (true) {long currentTime = System.currentTimeMillis();// 再次嘗試獲取鎖ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}// 超過(guò)最大等待時(shí)間則返回 false 結(jié)束循環(huán),獲取鎖失敗time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(threadId);return false;}/*** 6.阻塞等待鎖(通過(guò)信號(hào)量(共享鎖)阻塞,等待解鎖消息):*/currentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {//如果剩余時(shí)間(ttl)小于wait time ,就在 ttl 時(shí)間內(nèi),從Entry的信號(hào)量獲取一個(gè)許可(除非被中斷或者一直沒(méi)有可用的許可)。getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {//則就在wait time 時(shí)間范圍內(nèi)等待可以通過(guò)信號(hào)量getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}// 更新剩余的等待時(shí)間(最大等待時(shí)間-已經(jīng)消耗的阻塞時(shí)間)time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(threadId);return false;}}} finally {// 7.無(wú)論是否獲得鎖,都要取消訂閱解鎖消息unsubscribe(subscribeFuture, threadId);}
// return get(tryLockAsync(waitTime, leaseTime, unit));}
流程分析:
- 嘗試獲取鎖,返回 null 則說(shuō)明加鎖成功,返回一個(gè)數(shù)值,則說(shuō)明已經(jīng)存在該鎖,ttl 為鎖的剩余存活時(shí)間。
- 如果此時(shí)客戶端 2 進(jìn)程獲取鎖失敗,那么使用客戶端 2 的線程 id(其實(shí)本質(zhì)上就是進(jìn)程 id)通過(guò) Redis 的 channel 訂閱鎖釋放的事件,。如果等待的過(guò)程中一直未等到鎖的釋放事件通知,當(dāng)超過(guò)最大等待時(shí)間則獲取鎖失敗,返回 false,也就是第 39 行代碼。如果等到了鎖的釋放事件的通知,則開(kāi)始進(jìn)入一個(gè)不斷重試獲取鎖的循環(huán)。
- 循環(huán)中每次都先試著獲取鎖,并得到已存在的鎖的剩余存活時(shí)間。如果在重試中拿到了鎖,則直接返回。如果鎖當(dāng)前還是被占用的,那么等待釋放鎖的消息,具體實(shí)現(xiàn)使用了 JDK 的信號(hào)量 Semaphore 來(lái)阻塞線程,當(dāng)鎖釋放并發(fā)布釋放鎖的消息后,信號(hào)量的
release()
方法會(huì)被調(diào)用,此時(shí)被信號(hào)量阻塞的等待隊(duì)列中的一個(gè)線程就可以繼續(xù)嘗試獲取鎖了。
特別注意:
? 以上過(guò)程存在一個(gè)細(xì)節(jié),這里有必要說(shuō)明一下,也是分布式鎖的一個(gè)關(guān)鍵點(diǎn):當(dāng)鎖正在被占用時(shí),等待獲取鎖的進(jìn)程并不是通過(guò)一個(gè)
while(true)
死循環(huán)去獲取鎖,而是利用了 Redis 的發(fā)布訂閱機(jī)制,通過(guò) await 方法阻塞等待鎖的進(jìn)程,有效的解決了無(wú)效的鎖申請(qǐng)浪費(fèi)資源的問(wèn)題。
優(yōu)缺點(diǎn)
方案優(yōu)點(diǎn)
- Redisson 通過(guò) Watch Dog 機(jī)制很好的解決了鎖的續(xù)期問(wèn)題。
- 和 Zookeeper 相比較,Redisson 基于 Redis 性能更高,適合對(duì)性能要求高的場(chǎng)景。
- 通過(guò) Redisson 實(shí)現(xiàn)分布式可重入鎖,比原生的
SET mylock userId NX PX milliseconds
+ lua 實(shí)現(xiàn)的效果更好些,雖然基本原理都一樣,但是它幫我們屏蔽了內(nèi)部的執(zhí)行細(xì)節(jié)。 - 在等待申請(qǐng)鎖資源的進(jìn)程等待申請(qǐng)鎖的實(shí)現(xiàn)上也做了一些優(yōu)化,減少了無(wú)效的鎖申請(qǐng),提升了資源的利用率。
方案缺點(diǎn)
? **使用 Redisson 實(shí)現(xiàn)分布式鎖方案最大的問(wèn)題就是如果你對(duì)某個(gè) Redis Master 實(shí)例完成了加鎖,此時(shí) Master 會(huì)異步復(fù)制給其對(duì)應(yīng)的 slave 實(shí)例。**但是這個(gè)過(guò)程中一旦 Master 宕機(jī),主備切換,slave 變?yōu)榱?Master。接著就會(huì)導(dǎo)致,客戶端 2 來(lái)嘗試加鎖的時(shí)候,在新的 Master 上完成了加鎖,而客戶端 1 也以為自己成功加了鎖,此時(shí)就會(huì)導(dǎo)致多個(gè)客戶端對(duì)一個(gè)分布式鎖完成了加鎖,這時(shí)系統(tǒng)在業(yè)務(wù)語(yǔ)義上一定會(huì)出現(xiàn)問(wèn)題,導(dǎo)致各種臟數(shù)據(jù)的產(chǎn)生。
? 所以這個(gè)就是 Redis Cluster 或者說(shuō)是 Redis Master-Slave 架構(gòu)的主從異步復(fù)制導(dǎo)致的 Redis 分布式鎖的最大缺陷(在 Redis Master 實(shí)例宕機(jī)的時(shí)候,可能導(dǎo)致多個(gè)客戶端同時(shí)完成加鎖)。
?