0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Redis分布式鎖的使用場(chǎng)景

jf_ro2CN3Fa ? 來(lái)源:芋道源碼 ? 作者:芋道源碼 ? 2022-11-02 14:13 ? 次閱讀

分布式鎖(SET NX)

分布式鎖Redlock

基于ZooKeeper的分布式鎖更安全嗎?

總結(jié)

今天我們來(lái)聊一聊Redis分布式鎖。

首先大家可以先思考一個(gè)簡(jiǎn)單的問題,為什么要使用分布式鎖?普通的jvm鎖為什么不可以?

這個(gè)時(shí)候,大家肯定會(huì)吧啦吧啦想到一堆,例如java應(yīng)用屬于進(jìn)程級(jí),不同的ecs中部署相同的應(yīng)用,他們之間相互獨(dú)立。

所以,在分布式系統(tǒng)中,當(dāng)有多個(gè)客戶端需要獲取鎖時(shí),我們需要分布式鎖。此時(shí),鎖是保存在一個(gè)共享存儲(chǔ)系統(tǒng)中的,可以被多個(gè)客戶端共享訪問和獲取。

分布式鎖(SET NX)

知道了分布式鎖的使用場(chǎng)景,我們來(lái)自己簡(jiǎn)單的實(shí)現(xiàn)下分布式鎖:

publicclassIndexController{

publicStringdeductStock(){

StringlockKey="lock:product_101";

//setNx獲取分布式鎖
StringclientId=UUID.randomUUID().toString();
Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);//jedis.setnx(k,v)
if(!result){
return"error_code";
}
try{
intstock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock")
if(stock>0){
intrealStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
System.out.println("扣減成功,剩余庫(kù)存:"+realStock);
}else{
System.out.println("扣減失敗,庫(kù)存不足");
}
}finally{
//解鎖
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
}

以上代碼簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)扣減庫(kù)存的業(yè)務(wù)邏輯,我們拆開來(lái)說(shuō)下都做了什么事情:

1、首先聲明了lockkey,表示我們需要set的keyName

2、其次UUID.randomUUID().toString();生成該次請(qǐng)求的requestId,為什么需要生成這個(gè)唯一的UUID,后面在解鎖的時(shí)候會(huì)說(shuō)到

3、獲取分布式鎖,通過(guò)stringRedisTemplate.opsForValue().setIfAbsent來(lái)實(shí)現(xiàn),該語(yǔ)句的意思是如果存在該key則返回false,若不存在則進(jìn)行key的設(shè)置,設(shè)置成功后返回true,將當(dāng)前線程獲取的uuid設(shè)置成value,給定一個(gè)鎖的過(guò)期時(shí)間,防止該線程無(wú)限制持久鎖導(dǎo)致死鎖,也為了防止該服務(wù)器突然宕機(jī),導(dǎo)致其他機(jī)器的應(yīng)用無(wú)法獲取該鎖,這個(gè)是必須要做的設(shè)置,至于過(guò)期的時(shí)間,可以根據(jù)內(nèi)層業(yè)務(wù)邏輯的執(zhí)行時(shí)間來(lái)決定

4、執(zhí)行內(nèi)層的業(yè)務(wù)邏輯,進(jìn)行扣庫(kù)存的操作

5、業(yè)務(wù)邏輯執(zhí)行完成后,走到finally的解鎖操作,進(jìn)行解鎖操作時(shí),首先我們來(lái)判斷當(dāng)前鎖的值是否為該線程持有的,防止當(dāng)前線程執(zhí)行較慢,導(dǎo)致鎖過(guò)期,從而刪除了其他線程持有的分布式鎖,對(duì)于該操作,我來(lái)舉個(gè)例子:

時(shí)刻1:線程A獲取分布式鎖,開始執(zhí)行業(yè)務(wù)邏輯

時(shí)刻2:線程B等待分布式鎖釋放

時(shí)刻3:線程A所在機(jī)器IO處理緩慢、GC pause等問題導(dǎo)致處理緩慢

時(shí)刻4:線程A依舊處于block狀態(tài),鎖過(guò)期

時(shí)刻5:線程B獲取分布式鎖,開始執(zhí)行業(yè)務(wù)邏輯,此時(shí)線程A結(jié)束block,開始釋放鎖

時(shí)刻6:線程B處理業(yè)務(wù)邏輯緩慢,線程A釋放分布式鎖,但是此時(shí)釋放的是線程B的鎖,導(dǎo)致其他線程可以開始獲取鎖

看到這里,為什么每個(gè)請(qǐng)求需要requestId,并且在釋放鎖的情況下判斷是否是當(dāng)前的requestId是有必要的。

以上,就是一個(gè)簡(jiǎn)單的分布式鎖的實(shí)現(xiàn)過(guò)程。但是你覺得上述實(shí)現(xiàn)還存在問題嗎?

答案是肯定的。若是在判斷完分布式鎖的value與requestId之后,鎖過(guò)期了,依然會(huì)存在以上問題。

那么有沒有什么辦法可以規(guī)避以上問題,讓我們不需要去完成這些實(shí)現(xiàn),只需要專注于業(yè)務(wù)邏輯呢?

我們可以使用Redisson,并且Redisson有中文文檔,方便英文不好的同學(xué)查看(開發(fā)團(tuán)隊(duì)中有中國(guó)的jackygurui)。

接下來(lái)我們?cè)侔焉鲜龃a簡(jiǎn)單的改造下就可以規(guī)避這些問題:

publicclassIndexController{

publicStringdeductStock(){

StringlockKey="lock:product_101";

//setNx獲取分布式鎖
//StringclientId=UUID.randomUUID().toString();
//Booleanresult=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);//jedis.setnx(k,v)
//獲取鎖對(duì)象
RLockredissonLock=redisson.getLock(lockKey);
//加分布式鎖
redissonLock.lock();
try{
intstock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock")
if(stock>0){
intrealStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
System.out.println("扣減成功,剩余庫(kù)存:"+realStock);
}else{
System.out.println("扣減失敗,庫(kù)存不足");
}
}finally{
//解鎖
//if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
//stringRedisTemplate.delete(lockKey);
//}
//redisson分布式鎖解鎖
redissonLock.unlock();
}
}

可以看到,使用redisson分布式鎖會(huì)簡(jiǎn)單很多,我們通過(guò)redissonLock.lock()和redissonLock.unlock()解決了這個(gè)問題,看到這里,是不是有同學(xué)會(huì)問,如果服務(wù)器宕機(jī)了,分布式鎖會(huì)一直存在嗎,也沒有去指定過(guò)期時(shí)間?

redisson分布式鎖中有一個(gè)watchdog機(jī)制,即會(huì)給一個(gè)leaseTime,默認(rèn)為30s,到期后鎖自動(dòng)釋放,如果一直沒有解鎖,watchdog機(jī)制會(huì)一直重新設(shè)定鎖的過(guò)期時(shí)間,通過(guò)設(shè)置TimeTask,延遲10s再次執(zhí)行鎖續(xù)命,將鎖的過(guò)期時(shí)間重置為30s。下面就從redisson.lock()的源碼來(lái)看下:

lock的最終加鎖方法:

RFuturetryLockInnerAsync(longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommandcommand){
internalLockLeaseTime=unit.toMillis(leaseTime);

returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,command,
"if(redis.call('exists',KEYS[1])==0)then"+
"redis.call('hset',KEYS[1],ARGV[2],1);"+
"redis.call('pexpire',KEYS[1],ARGV[1]);"+
"returnnil;"+
"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]);"+
"returnnil;"+
"end;"+
"returnredis.call('pttl',KEYS[1]);",
Collections.singletonList(getName()),internalLockLeaseTime,getLockName(threadId));
}

可以看到lua腳本中redis.call('pexpire', KEYS[1], ARGV[1]);對(duì)key進(jìn)行設(shè)置,并給定了一個(gè)internalLockLeaseTime,給定的internalLockLeaseTime就是默認(rèn)的加鎖時(shí)間,為30s。

接下來(lái)我們?cè)诳聪骆i續(xù)命的源碼:

privatevoidscheduleExpirationRenewal(finallongthreadId){
if(!expirationRenewalMap.containsKey(this.getEntryName())){
Timeouttask=this.commandExecutor.getConnectionManager().newTimeout(newTimerTask(){
publicvoidrun(Timeouttimeout)throwsException{
//重新設(shè)置鎖過(guò)期時(shí)間
RFuturefuture=RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,"if(redis.call('hexists',KEYS[1],ARGV[2])==1)thenredis.call('pexpire',KEYS[1],ARGV[1]);return1;end;return0;",Collections.singletonList(RedissonLock.this.getName()),newObject[]{RedissonLock.this.internalLockLeaseTime,RedissonLock.this.getLockName(threadId)});
future.addListener(newFutureListener(){
publicvoidoperationComplete(Futurefuture)throwsException{
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if(!future.isSuccess()){
RedissonLock.log.error("Can'tupdatelock"+RedissonLock.this.getName()+"expiration",future.cause());
}else{
//獲取方法調(diào)用的結(jié)果
if((Boolean)future.getNow()){
//進(jìn)行遞歸調(diào)用
RedissonLock.this.scheduleExpirationRenewal(threadId);
}

}
}
});
}
//延遲this.internalLockLeaseTime/3L再執(zhí)行run方法
},this.internalLockLeaseTime/3L,TimeUnit.MILLISECONDS);
if(expirationRenewalMap.putIfAbsent(this.getEntryName(),task)!=null){
task.cancel();
}

}
}

從源碼層可以看到,加鎖成功后,會(huì)延遲10s執(zhí)行task中的run方法,然后在run方法里面執(zhí)行鎖過(guò)期時(shí)間的重置,如果時(shí)間重置成功,則繼續(xù)遞歸調(diào)用該方法,延遲10s后進(jìn)行鎖續(xù)命,若重置鎖時(shí)間失敗,則可能表示鎖已釋放,退出該方法。

以上,就是關(guān)于一個(gè)redis分布式鎖的說(shuō)明,看到這里,大家應(yīng)該對(duì)分布式鎖有一個(gè)大致的了解了。

但是盡管使用了redisson完成分布式鎖的實(shí)現(xiàn),對(duì)于分布式鎖是否還存在問題,分布式鎖真的安全嗎?

一般的,線上的環(huán)境肯定使用redis cluster,如果數(shù)據(jù)量不大,也會(huì)使用的redis sentinal。那么就存在主從復(fù)制的問題,那么是否會(huì)存在這種情況,在主庫(kù)設(shè)置了分布式鎖,但是可能由于網(wǎng)絡(luò)或其他原因?qū)е聰?shù)據(jù)還沒有同步到從庫(kù),此時(shí)主庫(kù)宕機(jī),選擇從庫(kù)作為主庫(kù),新主庫(kù)中并沒有該鎖的信息,其他線程又可以進(jìn)行鎖申請(qǐng),造成了發(fā)生線程安全問題的可能。

為了解決這個(gè)問題,redis的作者實(shí)現(xiàn)了redlock,基于redlock的實(shí)現(xiàn)有很大的爭(zhēng)論,并且現(xiàn)在已經(jīng)棄用了,但是我們還是需要了解下原理,以及之后基于這些問題的解決方案。

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

視頻教程:https://doc.iocoder.cn/video/

分布式鎖Redlock

Redlock是基于單Redis節(jié)點(diǎn)的分布式鎖在failover的時(shí)候會(huì)產(chǎn)生解決不了的安全性問題而產(chǎn)生的,基于N個(gè)完全獨(dú)立的Redis節(jié)點(diǎn)。

下面我來(lái)看下redlock獲取鎖的過(guò)程:

運(yùn)行Redlock算法的客戶端依次執(zhí)行下面各個(gè)步驟,來(lái)完成獲取鎖 的操作:

獲取當(dāng)前時(shí)間(毫秒數(shù))。

按順序依次向N個(gè)Redis節(jié)點(diǎn)執(zhí)行獲取鎖 的操作。這個(gè)獲取操作跟前面基于單Redis節(jié)點(diǎn)的獲取鎖 的過(guò)程相同,包含隨機(jī)字符串my_random_value,也包含過(guò)期時(shí)間(比如PX 30000,即鎖的有效時(shí)間)。為了保證在某個(gè)Redis節(jié)點(diǎn)不可用的時(shí)候算法能夠繼續(xù)運(yùn)行,這個(gè)獲取鎖 的操作還有一個(gè)超時(shí)時(shí)間(time out),它要遠(yuǎn)小于鎖的有效時(shí)間(幾十毫秒量級(jí))。客戶端在向某個(gè)Redis節(jié)點(diǎn)獲取鎖失敗以后,應(yīng)該立即嘗試下一個(gè)Redis節(jié)點(diǎn)。這里的失敗,應(yīng)該包含任何類型的失敗,比如該Redis節(jié)點(diǎn)不可用,或者該Redis節(jié)點(diǎn)上的鎖已經(jīng)被其它客戶端持有

計(jì)算整個(gè)獲取鎖的過(guò)程總共消耗了多長(zhǎng)時(shí)間,計(jì)算方法是用當(dāng)前時(shí)間減去第1步記錄的時(shí)間。如果客戶端從大多數(shù)Redis節(jié)點(diǎn)(>= N/2+1)成功獲取到了鎖,并且獲取鎖總共消耗的時(shí)間沒有超過(guò)鎖的有效時(shí)間(lock validity time),那么這時(shí)客戶端才認(rèn)為最終獲取鎖成功;否則,認(rèn)為最終獲取鎖失敗。

如果最終獲取鎖成功了,那么這個(gè)鎖的有效時(shí)間應(yīng)該重新計(jì)算,它等于最初的鎖的有效時(shí)間減去第3步計(jì)算出來(lái)的獲取鎖消耗的時(shí)間。

如果最終獲取鎖失敗了(可能由于獲取到鎖的Redis節(jié)點(diǎn)個(gè)數(shù)少于N/2+1,或者整個(gè)獲取鎖的過(guò)程消耗的時(shí)間超過(guò)了鎖的最初有效時(shí)間),那么客戶端應(yīng)該立即向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖 的操作。

好了,了解了redlock獲取鎖的機(jī)制之后,我們?cè)賮?lái)討論下redlock會(huì)有哪些問題:

問題一:

假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A, B, C, D, E。設(shè)想發(fā)生了如下的事件序列:

客戶端1成功鎖住了A, B, C,獲取鎖 成功(但D和E沒有鎖住)。

節(jié)點(diǎn)C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來(lái),丟失了。

節(jié)點(diǎn)C重啟后,客戶端2鎖住了C, D, E,獲取鎖 成功。

這樣,客戶端1和客戶端2同時(shí)獲得了鎖(針對(duì)同一資源)。

在默認(rèn)情況下,Redis的AOF持久化方式是每秒寫一次磁盤(即執(zhí)行fsync),因此最壞情況下可能丟失1秒的數(shù)據(jù)。為了盡可能不丟數(shù)據(jù),Redis允許設(shè)置成每次修改數(shù)據(jù)都進(jìn)行fsync,但這會(huì)降低性能。當(dāng)然,即使執(zhí)行了fsync也仍然有可能丟失數(shù)據(jù)(這取決于系統(tǒng)而不是Redis的實(shí)現(xiàn))。所以,上面分析的由于節(jié)點(diǎn)重啟引發(fā)的鎖失效問題,總是有可能出現(xiàn)的。為了應(yīng)對(duì)這一問題,Redis作者antirez又提出了延遲重啟 (delayed restarts)的概念。也就是說(shuō),一個(gè)節(jié)點(diǎn)崩潰后,先不立即重啟它,而是等待一段時(shí)間再重啟,這段時(shí)間應(yīng)該大于鎖的有效時(shí)間(lock validity time)。這樣的話,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會(huì)過(guò)期,它在重啟后就不會(huì)對(duì)現(xiàn)有的鎖造成影響。

關(guān)于Redlock還有一點(diǎn)細(xì)節(jié)值得拿出來(lái)分析一下:在最后釋放鎖 的時(shí)候,antirez在算法描述中特別強(qiáng)調(diào),客戶端應(yīng)該向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖 的操作。也就是說(shuō),即使當(dāng)時(shí)向某個(gè)節(jié)點(diǎn)獲取鎖沒有成功,在釋放鎖的時(shí)候也不應(yīng)該漏掉這個(gè)節(jié)點(diǎn)。這是為什么呢?設(shè)想這樣一種情況,客戶端發(fā)給某個(gè)Redis節(jié)點(diǎn)的獲取鎖 的請(qǐng)求成功到達(dá)了該Redis節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)也成功執(zhí)行了SET操作,但是它返回給客戶端的響應(yīng)包卻丟失了。這在客戶端看來(lái),獲取鎖的請(qǐng)求由于超時(shí)而失敗了,但在Redis這邊看來(lái),加鎖已經(jīng)成功了。因此,釋放鎖的時(shí)候,客戶端也應(yīng)該對(duì)當(dāng)時(shí)獲取鎖失敗的那些Redis節(jié)點(diǎn)同樣發(fā)起請(qǐng)求。實(shí)際上,這種情況在異步通信模型中是有可能發(fā)生的:客戶端向服務(wù)器通信是正常的,但反方向卻是有問題的。

所以,如果不進(jìn)行延遲重啟,或者對(duì)于同一個(gè)主節(jié)點(diǎn)進(jìn)行多個(gè)從節(jié)點(diǎn)的備份,并要求從節(jié)點(diǎn)的同步必須實(shí)時(shí)跟住主節(jié)點(diǎn),也就是說(shuō)需要配置redis從庫(kù)的同步策略,將延遲設(shè)置為最?。ㄖ鲝耐绞钱惒竭M(jìn)行的),通過(guò)min-replicas-max-lag(舊版本的redis使用min-slaves-max-lag)來(lái)設(shè)置主從庫(kù)間進(jìn)行數(shù)據(jù)復(fù)制時(shí),從庫(kù)給主庫(kù)發(fā)送 ACK 消息的最大延遲(以秒為單位),也就是說(shuō),這個(gè)值需要設(shè)置為0,否則都有可能出現(xiàn)延遲,但是這個(gè)實(shí)際上在redis中是不存在的,min-replicas-max-lag設(shè)置為0,就代表著這個(gè)配置不生效。redis本身是為了高效而存在的,如果因?yàn)樾枰WC業(yè)務(wù)的準(zhǔn)確性而使用,大大降低了redis的性能,建議使用的別的方式。

問題二:

如果客戶端長(zhǎng)期阻塞導(dǎo)致鎖過(guò)期,那么它接下來(lái)訪問共享資源就不安全了(沒有了鎖的保護(hù))。在RedLock中還是存在該問題的。

雖然在獲取鎖之后Redlock會(huì)去判斷鎖的有效性,如果鎖過(guò)期了,則會(huì)再去重新拿鎖。但是如果發(fā)生在獲取鎖之后,那么該有效性都得不到保障了。

06a163dc-5a74-11ed-a3b6-dac502259ad0.jpg

在上面的時(shí)序圖中,假設(shè)鎖服務(wù)本身是沒有問題的,它總是能保證任一時(shí)刻最多只有一個(gè)客戶端獲得鎖。上圖中出現(xiàn)的lease這個(gè)詞可以暫且認(rèn)為就等同于一個(gè)帶有自動(dòng)過(guò)期功能的鎖。客戶端1在獲得鎖之后發(fā)生了很長(zhǎng)時(shí)間的GC pause,在此期間,它獲得的鎖過(guò)期了,而客戶端2獲得了鎖。當(dāng)客戶端1從GC pause中恢復(fù)過(guò)來(lái)的時(shí)候,它不知道自己持有的鎖已經(jīng)過(guò)期了,它依然向共享資源(上圖中是一個(gè)存儲(chǔ)服務(wù))發(fā)起了寫數(shù)據(jù)請(qǐng)求,而這時(shí)鎖實(shí)際上被客戶端2持有,因此兩個(gè)客戶端的寫請(qǐng)求就有可能沖突(鎖的互斥作用失效了)。

初看上去,有人可能會(huì)說(shuō),既然客戶端1從GC pause中恢復(fù)過(guò)來(lái)以后不知道自己持有的鎖已經(jīng)過(guò)期了,那么它可以在訪問共享資源之前先判斷一下鎖是否過(guò)期。但仔細(xì)想想,這絲毫也沒有幫助。因?yàn)镚C pause可能發(fā)生在任意時(shí)刻,也許恰好在判斷完之后。

也有人會(huì)說(shuō),如果客戶端使用沒有GC的語(yǔ)言來(lái)實(shí)現(xiàn),是不是就沒有這個(gè)問題呢?質(zhì)疑者M(jìn)artin指出,系統(tǒng)環(huán)境太復(fù)雜,仍然有很多原因?qū)е逻M(jìn)程的pause,比如虛存造成的缺頁(yè)故障(page fault),再比如CPU資源的競(jìng)爭(zhēng)。即使不考慮進(jìn)程pause的情況,網(wǎng)絡(luò)延遲也仍然會(huì)造成類似的結(jié)果。

總結(jié)起來(lái)就是說(shuō),即使鎖服務(wù)本身是沒有問題的,而僅僅是客戶端有長(zhǎng)時(shí)間的pause或網(wǎng)絡(luò)延遲,仍然會(huì)造成兩個(gè)客戶端同時(shí)訪問共享資源的沖突情況發(fā)生。

那怎么解決這個(gè)問題呢?Martin給出了一種方法,稱為fencing token。fencing token是一個(gè)單調(diào)遞增的數(shù)字,當(dāng)客戶端成功獲取鎖的時(shí)候它隨同鎖一起返回給客戶端。而客戶端訪問共享資源的時(shí)候帶著這個(gè)fencing token,這樣提供共享資源的服務(wù)就能根據(jù)它進(jìn)行檢查,拒絕掉延遲到來(lái)的訪問請(qǐng)求(避免了沖突)。如下圖:

在上圖中,客戶端1先獲取到的鎖,因此有一個(gè)較小的fencing token,等于33,而客戶端2后獲取到的鎖,有一個(gè)較大的fencing token,等于34??蛻舳?從GC pause中恢復(fù)過(guò)來(lái)之后,依然是向存儲(chǔ)服務(wù)發(fā)送訪問請(qǐng)求,但是帶了fencing token = 33。存儲(chǔ)服務(wù)發(fā)現(xiàn)它之前已經(jīng)處理過(guò)34的請(qǐng)求,所以會(huì)拒絕掉這次33的請(qǐng)求。這樣就避免了沖突。

但是,對(duì)于客戶端和資源服務(wù)器之間的延遲(即發(fā)生在算法第3步之后的延遲),antirez是承認(rèn)所有的分布式鎖的實(shí)現(xiàn),包括Redlock,是沒有什么好辦法來(lái)應(yīng)對(duì)的。包括在我們到生產(chǎn)環(huán)境中,無(wú)法避免分布式鎖超時(shí)。

在討論中,有人提出客戶端1和客戶端2都發(fā)生了GC pause,兩個(gè)fencing token都延遲了,它們幾乎同時(shí)到達(dá)了文件服務(wù)器,而且保持了順序。那么,我們新加入的判斷邏輯,即判斷fencing token的合理性,應(yīng)該對(duì)兩個(gè)請(qǐng)求都會(huì)放過(guò),而放過(guò)之后它們幾乎同時(shí)在操作文件,還是沖突了。既然Martin宣稱fencing token能保證分布式鎖的正確性,那么上面這種可能的猜測(cè)也許是我們理解錯(cuò)了。但是Martin并沒有在后面做出解釋。

問題三:

Redlock對(duì)系統(tǒng)記時(shí)(timing)的過(guò)分依賴,下面給出一個(gè)例子(還是假設(shè)有5個(gè)Redis節(jié)點(diǎn)A, B, C, D, E):

客戶端1從Redis節(jié)點(diǎn)A, B, C成功獲取了鎖(多數(shù)節(jié)點(diǎn))。由于網(wǎng)絡(luò)問題,與D和E通信失敗。

節(jié)點(diǎn)C上的時(shí)鐘發(fā)生了向前跳躍,導(dǎo)致它上面維護(hù)的鎖快速過(guò)期。

客戶端2從Redis節(jié)點(diǎn)C, D, E成功獲取了同一個(gè)資源的鎖(多數(shù)節(jié)點(diǎn))。

客戶端1和客戶端2現(xiàn)在都認(rèn)為自己持有了鎖。

上面這種情況之所以有可能發(fā)生,本質(zhì)上是因?yàn)镽edlock的安全性(safety property)對(duì)系統(tǒng)的時(shí)鐘有比較強(qiáng)的依賴,一旦系統(tǒng)的時(shí)鐘變得不準(zhǔn)確,算法的安全性也就保證不了了。

但是作者反駁到,通過(guò)恰當(dāng)?shù)倪\(yùn)維,完全可以避免時(shí)鐘發(fā)生大的跳動(dòng),而Redlock對(duì)于時(shí)鐘的要求在現(xiàn)實(shí)系統(tǒng)中是完全可以滿足的。哪怕是手動(dòng)修改時(shí)鐘這種人為原因,不要那么做就是了。否則的話,都會(huì)出現(xiàn)問題。

說(shuō)了這么多關(guān)于Redlock的問題,到底有沒有什么分布式鎖能保證安全性呢?我們接下來(lái)再來(lái)看看ZooKeeper分布式鎖。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://gitee.com/zhijiantianya/yudao-cloud

視頻教程:https://doc.iocoder.cn/video/

基于ZooKeeper的分布式鎖更安全嗎?

很多人(也包括Martin在內(nèi))都認(rèn)為,如果你想構(gòu)建一個(gè)更安全的分布式鎖,那么應(yīng)該使用ZooKeeper,而不是Redis。那么,為了對(duì)比的目的,讓我們先暫時(shí)脫離開本文的題目,討論一下基于ZooKeeper的分布式鎖能提供絕對(duì)的安全嗎?它需要fencing token機(jī)制的保護(hù)嗎?

Flavio Junqueira是ZooKeeper的作者之一,他的這篇blog就寫在Martin和antirez發(fā)生爭(zhēng)論的那幾天。他在文中給出了一個(gè)基于ZooKeeper構(gòu)建分布式鎖的描述(當(dāng)然這不是唯一的方式):

客戶端嘗試創(chuàng)建一個(gè)znode節(jié)點(diǎn),比如/lock。那么第一個(gè)客戶端就創(chuàng)建成功了,相當(dāng)于拿到了鎖;而其它的客戶端會(huì)創(chuàng)建失?。▃node已存在),獲取鎖失敗。

持有鎖的客戶端訪問共享資源完成后,將znode刪掉,這樣其它客戶端接下來(lái)就能來(lái)獲取鎖了。

znode應(yīng)該被創(chuàng)建成ephemeral的。這是znode的一個(gè)特性,它保證如果創(chuàng)建znode的那個(gè)客戶端崩潰了,那么相應(yīng)的znode會(huì)被自動(dòng)刪除。這保證了鎖一定會(huì)被釋放。

看起來(lái)這個(gè)鎖相當(dāng)完美,沒有Redlock過(guò)期時(shí)間的問題,而且能在需要的時(shí)候讓鎖自動(dòng)釋放。但仔細(xì)考察的話,并不盡然。

ZooKeeper是怎么檢測(cè)出某個(gè)客戶端已經(jīng)崩潰了呢?實(shí)際上,每個(gè)客戶端都與ZooKeeper的某臺(tái)服務(wù)器維護(hù)著一個(gè)Session,這個(gè)Session依賴定期的心跳(heartbeat)來(lái)維持。如果ZooKeeper長(zhǎng)時(shí)間收不到客戶端的心跳(這個(gè)時(shí)間稱為Sesion的過(guò)期時(shí)間),那么它就認(rèn)為Session過(guò)期了,通過(guò)這個(gè)Session所創(chuàng)建的所有的ephemeral類型的znode節(jié)點(diǎn)都會(huì)被自動(dòng)刪除。

設(shè)想如下的執(zhí)行序列:

客戶端1創(chuàng)建了znode節(jié)點(diǎn)/lock,獲得了鎖。

客戶端1進(jìn)入了長(zhǎng)時(shí)間的GC pause。

客戶端1連接到ZooKeeper的Session過(guò)期了。znode節(jié)點(diǎn)/lock被自動(dòng)刪除。

客戶端2創(chuàng)建了znode節(jié)點(diǎn)/lock,從而獲得了鎖。

客戶端1從GC pause中恢復(fù)過(guò)來(lái),它仍然認(rèn)為自己持有鎖。

最后,客戶端1和客戶端2都認(rèn)為自己持有了鎖,沖突了。這與之前Martin在文章中描述的由于GC pause導(dǎo)致的分布式鎖失效的情況類似。

看起來(lái),用ZooKeeper實(shí)現(xiàn)的分布式鎖也不一定就是安全的。該有的問題它還是有。但是,ZooKeeper作為一個(gè)專門為分布式應(yīng)用提供方案的框架,它提供了一些非常好的特性,是Redis之類的方案所沒有的。像前面提到的ephemeral類型的znode自動(dòng)刪除的功能就是一個(gè)例子。

還有一個(gè)很有用的特性是ZooKeeper的watch機(jī)制。這個(gè)機(jī)制可以這樣來(lái)使用,比如當(dāng)客戶端試圖創(chuàng)建/lock的時(shí)候,發(fā)現(xiàn)它已經(jīng)存在了,這時(shí)候創(chuàng)建失敗,但客戶端不一定就此對(duì)外宣告獲取鎖失敗??蛻舳丝梢赃M(jìn)入一種等待狀態(tài),等待當(dāng)/lock節(jié)點(diǎn)被刪除的時(shí)候,ZooKeeper通過(guò)watch機(jī)制通知它,這樣它就可以繼續(xù)完成創(chuàng)建操作(獲取鎖)。這可以讓分布式鎖在客戶端用起來(lái)就像一個(gè)本地的鎖一樣:加鎖失敗就阻塞住,直到獲取到鎖為止。這樣的特性Redlock就無(wú)法實(shí)現(xiàn)。

小結(jié)一下,基于ZooKeeper的鎖和基于Redis的鎖相比在實(shí)現(xiàn)特性上有兩個(gè)不同:

在正常情況下,客戶端可以持有鎖任意長(zhǎng)的時(shí)間,這可以確保它做完所有需要的資源訪問操作之后再釋放鎖。這避免了基于Redis的鎖對(duì)于有效時(shí)間(lock validity time)到底設(shè)置多長(zhǎng)的兩難問題。實(shí)際上,基于ZooKeeper的鎖是依靠Session(心跳)來(lái)維持鎖的持有狀態(tài)的,而Redis不支持Session。

基于ZooKeeper的鎖支持在獲取鎖失敗之后等待鎖重新釋放的事件。這讓客戶端對(duì)鎖的使用更加靈活。

總結(jié)

綜上所述,我們可以得出兩種結(jié)論:

如果僅是為了效率(efficiency),那么你可以自己選擇你喜歡的一種分布式鎖的實(shí)現(xiàn)。當(dāng)然,你需要清楚地知道它在安全性上有哪些不足,以及它會(huì)帶來(lái)什么后果,這也是為什么我們需要了解實(shí)現(xiàn)原理的原因,大多數(shù)情況下不會(huì)出問題,但是就萬(wàn)一的情況,處理起來(lái)可能需要大量的時(shí)間定位問題。

如果你是為了正確性(correctness),那么請(qǐng)慎之又慎。就目前來(lái)說(shuō)ZooKeeper的分布鎖相對(duì)于redlock更加合理。

最后,由于redlock的出現(xiàn)其實(shí)是為了保證分布式鎖的可靠性,但是由于實(shí)現(xiàn)的種種問題其可靠性并沒有ZooKeeper分布式鎖來(lái)的高,對(duì)于可容錯(cuò)的希望效率的場(chǎng)景下,redis分布式鎖又可以完全滿足,這也是導(dǎo)致了redlock被棄用的原因。

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2947

    瀏覽量

    104373
  • 機(jī)器
    +關(guān)注

    關(guān)注

    0

    文章

    773

    瀏覽量

    40661
  • Redis
    +關(guān)注

    關(guān)注

    0

    文章

    370

    瀏覽量

    10811

原文標(biāo)題:Redis分布式鎖真的安全嗎?

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    redis分布式場(chǎng)景實(shí)現(xiàn)

    今天帶大家深入剖析一下Redis分布式,徹底搞懂它。 場(chǎng)景 既然要搞懂Redis分布式
    的頭像 發(fā)表于 09-25 17:09 ?666次閱讀

    在 Java 中利用 redis 實(shí)現(xiàn)一個(gè)分布式服務(wù)

    在 Java 中利用 redis 實(shí)現(xiàn)一個(gè)分布式服務(wù)
    發(fā)表于 07-05 13:14

    Redis 分布式的正確實(shí)現(xiàn)方式

    分布式一般有三種實(shí)現(xiàn)方式:1. 數(shù)據(jù)庫(kù)樂觀;2. 基于Redis分布式;3. 基于Zoo
    的頭像 發(fā)表于 05-31 14:19 ?3538次閱讀

    使用Redis作為分布式的詳細(xì)方案

    一個(gè)很簡(jiǎn)單的答案就是去使用 Redission 客戶端。Redission 中的方案就是 Redis 分布式的比較完美的詳細(xì)方案。
    的頭像 發(fā)表于 04-10 17:27 ?1880次閱讀

    Redis分布式真的安全嗎?

    今天我們來(lái)聊一聊Redis分布式。
    的頭像 發(fā)表于 11-02 14:07 ?948次閱讀

    如何使用注解實(shí)現(xiàn)redis分布式!

    使用 Redis 作為分布式,將的狀態(tài)放到 Redis 統(tǒng)一維護(hù),解決集群中單機(jī) JVM 信息不互通的問題,規(guī)定操作順序,保護(hù)用戶的數(shù)據(jù)
    發(fā)表于 04-25 12:42 ?611次閱讀
    如何使用注解實(shí)現(xiàn)<b class='flag-5'>redis</b><b class='flag-5'>分布式</b><b class='flag-5'>鎖</b>!

    深入理解redis分布式

    深入理解redis分布式 哈嘍,大家好,我是指北君。 本篇文件我們來(lái)介紹如何Redis實(shí)現(xiàn)分布式
    的頭像 發(fā)表于 10-08 14:13 ?853次閱讀
    深入理解<b class='flag-5'>redis</b><b class='flag-5'>分布式</b><b class='flag-5'>鎖</b>

    redis分布式如何實(shí)現(xiàn)

    Redis分布式是一種基于Redis實(shí)現(xiàn)的機(jī)制,可以用于多個(gè)進(jìn)程或多臺(tái)服務(wù)器之間對(duì)共享資源的并發(fā)訪問控制。在分布式系統(tǒng)中,由于多個(gè)進(jìn)程或多
    的頭像 發(fā)表于 11-16 11:29 ?473次閱讀

    redis分布式可能出現(xiàn)的問題

    Redis分布式是一種常用的機(jī)制,用于解決多個(gè)進(jìn)程或多臺(tái)服務(wù)器對(duì)共享資源的并發(fā)訪問問題。然而,由于分布式環(huán)境的復(fù)雜性,使用
    的頭像 發(fā)表于 11-16 11:40 ?1280次閱讀

    redis分布式死鎖處理方案

    引言: 隨著分布式系統(tǒng)的廣泛應(yīng)用,尤其是在大規(guī)模并發(fā)操作下,對(duì)并發(fā)控制的需求越來(lái)越高。Redis分布式作為一種常見的分布式
    的頭像 發(fā)表于 11-16 11:44 ?1569次閱讀

    redis分布式的應(yīng)用場(chǎng)景有哪些

    Redis分布式是一種基于Redis實(shí)現(xiàn)的分布式機(jī)制,可以在
    的頭像 發(fā)表于 12-04 11:21 ?1320次閱讀

    redis分布式三個(gè)方法

    Redis是一種高性能的分布式緩存和鍵值存儲(chǔ)系統(tǒng),它提供了一種可靠的分布式解決方案。在分布式系統(tǒng)中,由于多個(gè)節(jié)點(diǎn)之間的并發(fā)訪問,需要使用
    的頭像 發(fā)表于 12-04 11:22 ?1324次閱讀

    如何實(shí)現(xiàn)Redis分布式

    Redis是一個(gè)開源的內(nèi)存數(shù)據(jù)存儲(chǔ)系統(tǒng),可用于高速讀寫操作。在分布式系統(tǒng)中,為了保證數(shù)據(jù)的一致性和避免競(jìng)態(tài)條件,常常需要使用分布式來(lái)對(duì)共享資源進(jìn)行加鎖操作。
    的頭像 發(fā)表于 12-04 11:24 ?608次閱讀

    redis分布式可能出現(xiàn)的問題及解決方案

    Redis分布式是一種常見的解決分布式系統(tǒng)中并發(fā)問題的方案。雖然Redis分布式鎖具有許多優(yōu)點(diǎn)
    的頭像 發(fā)表于 12-04 11:29 ?871次閱讀

    redis分布式的缺點(diǎn)

    Redis分布式是一種常見的用于解決分布式系統(tǒng)中資源爭(zhēng)用問題的解決方案。盡管Redis分布式
    的頭像 發(fā)表于 12-04 14:05 ?1139次閱讀