精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

聊聊Redis分布式鎖的八大坑

開(kāi)發(fā) 前端 分布式 Redis
在分布式系統(tǒng)中,由于redis分布式鎖相對(duì)于更簡(jiǎn)單和高效,成為了分布式鎖的首先,被我們用到了很多實(shí)際業(yè)務(wù)場(chǎng)景當(dāng)中。今天我們就一起聊聊redis分布式鎖的一些坑,給有需要的朋友一個(gè)參考。

[[425706]]

前言

在分布式系統(tǒng)中,由于redis分布式鎖相對(duì)于更簡(jiǎn)單和高效,成為了分布式鎖的首先,被我們用到了很多實(shí)際業(yè)務(wù)場(chǎng)景當(dāng)中。

但不是說(shuō)用了redis分布式鎖,就可以高枕無(wú)憂了,如果沒(méi)有用好或者用對(duì),也會(huì)引來(lái)一些意想不到的問(wèn)題。

今天我們就一起聊聊redis分布式鎖的一些坑,給有需要的朋友一個(gè)參考。

1 非原子操作

使用redis的分布式鎖,我們首先想到的可能是setNx命令。

  1. if (jedis.setnx(lockKey, val) == 1) { 
  2.    jedis.expire(lockKey, timeout); 

容易,三下五除二,我們就可以把代碼寫(xiě)好。

這段代碼確實(shí)可以加鎖成功,但你有沒(méi)有發(fā)現(xiàn)什么問(wèn)題?

加鎖操作和后面的設(shè)置超時(shí)時(shí)間是分開(kāi)的,并非原子操作。

假如加鎖成功,但是設(shè)置超時(shí)時(shí)間失敗了,該lockKey就變成永不失效。假如在高并發(fā)場(chǎng)景中,有大量的lockKey加鎖成功了,但不會(huì)失效,有可能直接導(dǎo)致redis內(nèi)存空間不足。

那么,有沒(méi)有保證原子性的加鎖命令呢?

答案是:有,請(qǐng)看下面。

2 忘了釋放鎖

上面說(shuō)到使用setNx命令加鎖操作和設(shè)置超時(shí)時(shí)間是分開(kāi)的,并非原子操作。

而在redis中還有set命令,該命令可以指定多個(gè)參數(shù)。

  1. String result = jedis.set(lockKey, requestId, "NX""PX", expireTime); 
  2. if ("OK".equals(result)) { 
  3.     return true
  4. return false

其中:

  • lockKey:鎖的標(biāo)識(shí)
  • requestId:請(qǐng)求id
  • NX:只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
  • PX:設(shè)置鍵的過(guò)期時(shí)間為 millisecond 毫秒。
  • expireTime:過(guò)期時(shí)間

set命令是原子操作,加鎖和設(shè)置超時(shí)時(shí)間,一個(gè)命令就能輕松搞定。

nice

使用set命令加鎖,表面上看起來(lái)沒(méi)有問(wèn)題。但如果仔細(xì)想想,加鎖之后,每次都要達(dá)到了超時(shí)時(shí)間才釋放鎖,會(huì)不會(huì)有點(diǎn)不合理?加鎖后,如果不及時(shí)釋放鎖,會(huì)有很多問(wèn)題。

分布式鎖更合理的用法是:

  1. 手動(dòng)加鎖
  2. 業(yè)務(wù)操作
  3. 手動(dòng)釋放鎖
  4. 如果手動(dòng)釋放鎖失敗了,則達(dá)到超時(shí)時(shí)間,redis會(huì)自動(dòng)釋放鎖。

大致流程圖如下:

那么問(wèn)題來(lái)了,如何釋放鎖呢?

偽代碼如下:

  1. try{ 
  2.   String result = jedis.set(lockKey, requestId, "NX""PX", expireTime); 
  3.   if ("OK".equals(result)) { 
  4.       return true
  5.   } 
  6.   return false
  7. } finally { 
  8.     unlock(lockKey); 
  9. }   

需要捕獲業(yè)務(wù)代碼的異常,然后在finally中釋放鎖。換句話說(shuō)就是:無(wú)論代碼執(zhí)行成功或失敗了,都需要釋放鎖。

此時(shí),有些朋友可能會(huì)問(wèn):假如剛好在釋放鎖的時(shí)候,系統(tǒng)被重啟了,或者網(wǎng)絡(luò)斷線了,或者機(jī)房斷點(diǎn)了,不也會(huì)導(dǎo)致釋放鎖失敗?

這是一個(gè)好問(wèn)題,因?yàn)檫@種小概率問(wèn)題確實(shí)存在。

但還記得前面我們給鎖設(shè)置過(guò)超時(shí)時(shí)間嗎?即使出現(xiàn)異常情況造成釋放鎖失敗,但到了我們?cè)O(shè)定的超時(shí)時(shí)間,鎖還是會(huì)被redis自動(dòng)釋放。

但只在finally中釋放鎖,就夠了嗎?

3 釋放了別人的鎖

做人要厚道,先回答上面的問(wèn)題:只在finally中釋放鎖,當(dāng)然是不夠的,因?yàn)獒尫沛i的姿勢(shì),還是不對(duì)。

哪里不對(duì)?

答:在多線程場(chǎng)景中,可能會(huì)出現(xiàn)釋放了別人的鎖的情況。

有些朋友可能會(huì)反駁:假設(shè)在多線程場(chǎng)景中,線程A獲取到了鎖,但如果線程A沒(méi)有釋放鎖,此時(shí),線程B是獲取不到鎖的,何來(lái)釋放了別人鎖之說(shuō)?

答:假如線程A和線程B,都使用lockKey加鎖。線程A加鎖成功了,但是由于業(yè)務(wù)功能耗時(shí)時(shí)間很長(zhǎng),超過(guò)了設(shè)置的超時(shí)時(shí)間。這時(shí)候,redis會(huì)自動(dòng)釋放lockKey鎖。此時(shí),線程B就能給lockKey加鎖成功了,接下來(lái)執(zhí)行它的業(yè)務(wù)操作。恰好這個(gè)時(shí)候,線程A執(zhí)行完了業(yè)務(wù)功能,接下來(lái),在finally方法中釋放了鎖lockKey。這不就出問(wèn)題了,線程B的鎖,被線程A釋放了。

我想這個(gè)時(shí)候,線程B肯定哭暈在廁所里,并且嘴里還振振有詞。

那么,如何解決這個(gè)問(wèn)題呢?

不知道你們注意到?jīng)]?在使用set命令加鎖時(shí),除了使用lockKey鎖標(biāo)識(shí),還多設(shè)置了一個(gè)參數(shù):requestId,為什么要需要記錄requestId呢?

答:requestId是在釋放鎖的時(shí)候用的。

偽代碼如下:

  1. if (jedis.get(lockKey).equals(requestId)) { 
  2.     jedis.del(lockKey); 
  3.     return true
  4. return false

在釋放鎖的時(shí)候,先獲取到該鎖的值(之前設(shè)置值就是requestId),然后判斷跟之前設(shè)置的值是否相同,如果相同才允許刪除鎖,返回成功。如果不同,則直接返回失敗。

換句話說(shuō)就是:自己只能釋放自己加的鎖,不允許釋放別人加的鎖。

這里為什么要用requestId,用userId不行嗎?

答:如果用userId的話,對(duì)于請(qǐng)求來(lái)說(shuō)并不唯一,多個(gè)不同的請(qǐng)求,可能使用同一個(gè)userId。而requestId是全局唯一的,不存在加鎖和釋放鎖亂掉的情況。

此外,使用lua腳本,也能解決釋放了別人的鎖的問(wèn)題:

  1. if redis.call('get', KEYS[1]) == ARGV[1] then  
  2.  return redis.call('del', KEYS[1])  
  3. else  
  4.   return 0  
  5. end 

lua腳本能保證查詢鎖是否存在和刪除鎖是原子操作,用它來(lái)釋放鎖效果更好一些。

說(shuō)到lua腳本,其實(shí)加鎖操作也建議使用lua腳本:

  1. if (redis.call('exists', KEYS[1]) == 0) then 
  2.     redis.call('hset', KEYS[1], ARGV[2], 1);  
  3.     redis.call('pexpire', KEYS[1], ARGV[1]);  
  4.  return nil;  
  5. end 
  6. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
  7.    redis.call('hincrby', KEYS[1], ARGV[2], 1);  
  8.    redis.call('pexpire', KEYS[1], ARGV[1]);  
  9.   return nil;  
  10. end;  
  11. return redis.call('pttl', KEYS[1]); 

這是redisson框架的加鎖代碼,寫(xiě)的不錯(cuò),大家可以借鑒一下。

有趣,下面還有哪些好玩的東西?

4 大量失敗請(qǐng)求

上面的加鎖方法看起來(lái)好像沒(méi)有問(wèn)題,但如果你仔細(xì)想想,如果有1萬(wàn)的請(qǐng)求同時(shí)去競(jìng)爭(zhēng)那把鎖,可能只有一個(gè)請(qǐng)求是成功的,其余的9999個(gè)請(qǐng)求都會(huì)失敗。

在秒殺場(chǎng)景下,會(huì)有什么問(wèn)題?

答:每1萬(wàn)個(gè)請(qǐng)求,有1個(gè)成功。再1萬(wàn)個(gè)請(qǐng)求,有1個(gè)成功。如此下去,直到庫(kù)存不足。這就變成均勻分布的秒殺了,跟我們想象中的不一樣。

如何解決這個(gè)問(wèn)題呢?

此外,還有一種場(chǎng)景:

比如,有兩個(gè)線程同時(shí)上傳文件到sftp,上傳文件前先要?jiǎng)?chuàng)建目錄。假設(shè)兩個(gè)線程需要?jiǎng)?chuàng)建的目錄名都是當(dāng)天的日期,比如:20210920,如果不做任何控制,直接并發(fā)的創(chuàng)建目錄,第二個(gè)線程必然會(huì)失敗。

這時(shí)候有些朋友可能會(huì)說(shuō):這還不容易,加一個(gè)redis分布式鎖就能解決問(wèn)題了,此外再判斷一下,如果目錄已經(jīng)存在就不創(chuàng)建,只有目錄不存在才需要?jiǎng)?chuàng)建。

偽代碼如下:

  1. try { 
  2.   String result = jedis.set(lockKey, requestId, "NX""PX", expireTime); 
  3.   if ("OK".equals(result)) { 
  4.     if(!exists(path)) { 
  5.        mkdir(path); 
  6.     } 
  7.     return true
  8.   } 
  9. } finally{ 
  10.     unlock(lockKey,requestId); 
  11. }   
  12. return false

一切看似美好,但經(jīng)不起仔細(xì)推敲。

來(lái)自靈魂的一問(wèn):第二個(gè)請(qǐng)求如果加鎖失敗了,接下來(lái),是返回失敗,還是返回成功呢?

主要流程圖如下:

顯然第二個(gè)請(qǐng)求,肯定是不能返回失敗的,如果返回失敗了,這個(gè)問(wèn)題還是沒(méi)有被解決。如果文件還沒(méi)有上傳成功,直接返回成功會(huì)有更大的問(wèn)題。頭疼,到底該如何解決呢?

答:使用自旋鎖。

  1. try { 
  2.   Long start = System.currentTimeMillis(); 
  3.   while(true) { 
  4.      String result = jedis.set(lockKey, requestId, "NX""PX", expireTime); 
  5.      if ("OK".equals(result)) { 
  6.         if(!exists(path)) { 
  7.            mkdir(path); 
  8.         } 
  9.         return true
  10.      } 
  11.       
  12.      long time = System.currentTimeMillis() - start; 
  13.       if (time>=timeout) { 
  14.           return false
  15.       } 
  16.       try { 
  17.           Thread.sleep(50); 
  18.       } catch (InterruptedException e) { 
  19.           e.printStackTrace(); 
  20.       } 
  21.   } 
  22. } finally{ 
  23.     unlock(lockKey,requestId); 
  24. }   
  25. return false

在規(guī)定的時(shí)間,比如500毫秒內(nèi),自旋不斷嘗試加鎖(說(shuō)白了,就是在死循環(huán)中,不斷嘗試加鎖),如果成功則直接返回。如果失敗,則休眠50毫秒,再發(fā)起新一輪的嘗試。如果到了超時(shí)時(shí)間,還未加鎖成功,則直接返回失敗。

好吧,學(xué)到一招了,還有嗎?

5 鎖重入問(wèn)題

我們都知道redis分布式鎖是互斥的。假如我們對(duì)某個(gè)key加鎖了,如果該key對(duì)應(yīng)的鎖還沒(méi)失效,再用相同key去加鎖,大概率會(huì)失敗。

沒(méi)錯(cuò),大部分場(chǎng)景是沒(méi)問(wèn)題的。

為什么說(shuō)是大部分場(chǎng)景呢?

因?yàn)檫€有這樣的場(chǎng)景:

假設(shè)在某個(gè)請(qǐng)求中,需要獲取一顆滿足條件的菜單樹(shù)或者分類樹(shù)。我們以菜單為例,這就需要在接口中從根節(jié)點(diǎn)開(kāi)始,遞歸遍歷出所有滿足條件的子節(jié)點(diǎn),然后組裝成一顆菜單樹(shù)。

需要注意的是菜單不是一成不變的,在后臺(tái)系統(tǒng)中運(yùn)營(yíng)同學(xué)可以動(dòng)態(tài)添加、修改和刪除菜單。為了保證在并發(fā)的情況下,每次都可能獲取最新的數(shù)據(jù),這里可以加redis分布式鎖。

加redis分布式鎖的思路是對(duì)的。但接下來(lái)問(wèn)題來(lái)了,在遞歸方法中遞歸遍歷多次,每次都是加的同一把鎖。遞歸第一層當(dāng)然是可以加鎖成功的,但遞歸第二層、第三層...第N層,不就會(huì)加鎖失敗了?

遞歸方法中加鎖的偽代碼如下:

  1. private int expireTime = 1000; 
  2.  
  3. public void fun(int level,String lockKey,String requestId){ 
  4.   try{ 
  5.      String result = jedis.set(lockKey, requestId, "NX""PX", expireTime); 
  6.      if ("OK".equals(result)) { 
  7.         if(level<=10){ 
  8.            this.fun(++level,lockKey,requestId); 
  9.         } else { 
  10.            return
  11.         } 
  12.      } 
  13.      return
  14.   } finally { 
  15.      unlock(lockKey,requestId); 
  16.   } 

如果你直接這么用,看起來(lái)好像沒(méi)有問(wèn)題。但最終執(zhí)行程序之后發(fā)現(xiàn),等待你的結(jié)果只有一個(gè):出現(xiàn)異常。

因?yàn)閺母?jié)點(diǎn)開(kāi)始,第一層遞歸加鎖成功,還沒(méi)釋放鎖,就直接進(jìn)入第二層遞歸。因?yàn)殒i名為lockKey,并且值為requestId的鎖已經(jīng)存在,所以第二層遞歸大概率會(huì)加鎖失敗,然后返回到第一層。第一層接下來(lái)正常釋放鎖,然后整個(gè)遞歸方法直接返回了。

這下子,大家知道出現(xiàn)什么問(wèn)題了吧?

沒(méi)錯(cuò),遞歸方法其實(shí)只執(zhí)行了第一層遞歸就返回了,其他層遞歸由于加鎖失敗,根本沒(méi)法執(zhí)行。

那么這個(gè)問(wèn)題該如何解決呢?

答:使用可重入鎖。

我們以redisson框架為例,它的內(nèi)部實(shí)現(xiàn)了可重入鎖的功能。

古時(shí)候有句話說(shuō)得好:為人不識(shí)陳近南,便稱英雄也枉然。

我說(shuō):分布式鎖不識(shí)redisson,便稱好鎖也枉然。哈哈哈,只是自?shī)首詷?lè)一下。

由此可見(jiàn),redisson在redis分布式鎖中的江湖地位很高。

偽代碼如下:

  1. private int expireTime = 1000; 
  2.  
  3. public void run(String lockKey) { 
  4.   RLock lock = redisson.getLock(lockKey); 
  5.   this.fun(lock,1); 
  6.  
  7. public void fun(RLock lock,int level){ 
  8.   try{ 
  9.       lock.lock(5, TimeUnit.SECONDS); 
  10.       if(level<=10){ 
  11.          this.fun(lock,++level); 
  12.       } else { 
  13.          return
  14.       } 
  15.   } finally { 
  16.      lock.unlock(); 
  17.   } 

上面的代碼也許并不完美,這里只是給了一個(gè)大致的思路,如果大家有這方面需求的話,以上代碼僅供參考。

接下來(lái),聊聊redisson可重入鎖的實(shí)現(xiàn)原理。

加鎖主要是通過(guò)以下腳本實(shí)現(xiàn)的:

  1. if (redis.call('exists', KEYS[1]) == 0)  
  2. then   
  3.    redis.call('hset', KEYS[1], ARGV[2], 1);        redis.call('pexpire', KEYS[1], ARGV[1]);  
  4.    return nil;  
  5. end
  6. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)  
  7. then   
  8.   redis.call('hincrby', KEYS[1], ARGV[2], 1);  
  9.   redis.call('pexpire', KEYS[1], ARGV[1]);  
  10.   return nil;  
  11. end
  12. return redis.call('pttl', KEYS[1]); 

其中:

  • KEYS[1]:鎖名
  • ARGV[1]:過(guò)期時(shí)間
  • ARGV[2]:uuid + ":" + threadId,可認(rèn)為是requestId
  1. 先判斷如果鎖名不存在,則加鎖。
  2. 接下來(lái),判斷如果鎖名和requestId值都存在,則使用hincrby命令給該鎖名和requestId值計(jì)數(shù),每次都加1。注意一下,這里就是重入鎖的關(guān)鍵,鎖重入一次值就加1。
  3. 如果鎖名存在,但值不是requestId,則返回過(guò)期時(shí)間。

釋放鎖主要是通過(guò)以下腳本實(shí)現(xiàn)的:

  1. if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)  
  2. then  
  3.   return nil 
  4. end 
  5. local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
  6. if (counter > 0)  
  7. then  
  8.     redis.call('pexpire', KEYS[1], ARGV[2]);  
  9.     return 0;  
  10.  else  
  11.    redis.call('del', KEYS[1]);  
  12.    redis.call('publish', KEYS[2], ARGV[1]);  
  13.    return 1;  
  14. end;  
  15. return nil 
  1. 先判斷如果鎖名和requestId值不存在,則直接返回。
  2. 如果鎖名和requestId值存在,則重入鎖減1。
  3. 如果減1后,重入鎖的value值還大于0,說(shuō)明還有引用,則重試設(shè)置過(guò)期時(shí)間。
  4. 如果減1后,重入鎖的value值還等于0,則可以刪除鎖,然后發(fā)消息通知等待線程搶鎖。

再次強(qiáng)調(diào)一下,如果你們系統(tǒng)可以容忍數(shù)據(jù)暫時(shí)不一致,有些場(chǎng)景不加鎖也行,我在這里只是舉個(gè)例子,本節(jié)內(nèi)容并不適用于所有場(chǎng)景。

6 鎖競(jìng)爭(zhēng)問(wèn)題

如果有大量需要寫(xiě)入數(shù)據(jù)的業(yè)務(wù)場(chǎng)景,使用普通的redis分布式鎖是沒(méi)有問(wèn)題的。

但如果有些業(yè)務(wù)場(chǎng)景,寫(xiě)入的操作比較少,反而有大量讀取的操作。這樣直接使用普通的redis分布式鎖,會(huì)不會(huì)有點(diǎn)浪費(fèi)性能?

我們都知道,鎖的粒度越粗,多個(gè)線程搶鎖時(shí)競(jìng)爭(zhēng)就越激烈,造成多個(gè)線程鎖等待的時(shí)間也就越長(zhǎng),性能也就越差。

所以,提升redis分布式鎖性能的第一步,就是要把鎖的粒度變細(xì)。

6.1 讀寫(xiě)鎖

眾所周知,加鎖的目的是為了保證,在并發(fā)環(huán)境中讀寫(xiě)數(shù)據(jù)的安全性,即不會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)誤或者不一致的情況。

但在絕大多數(shù)實(shí)際業(yè)務(wù)場(chǎng)景中,一般是讀數(shù)據(jù)的頻率遠(yuǎn)遠(yuǎn)大于寫(xiě)數(shù)據(jù)。而線程間的并發(fā)讀操作是并不涉及并發(fā)安全問(wèn)題,我們沒(méi)有必要給讀操作加互斥鎖,只要保證讀寫(xiě)、寫(xiě)寫(xiě)并發(fā)操作上鎖是互斥的就行,這樣可以提升系統(tǒng)的性能。

我們以redisson框架為例,它內(nèi)部已經(jīng)實(shí)現(xiàn)了讀寫(xiě)鎖的功能。

讀鎖的偽代碼如下:

  1. RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock"); 
  2. RLock rLock = readWriteLock.readLock(); 
  3. try { 
  4.     rLock.lock(); 
  5.     //業(yè)務(wù)操作 
  6. } catch (Exception e) { 
  7.     log.error(e); 
  8. } finally { 
  9.     rLock.unlock(); 

寫(xiě)鎖的偽代碼如下:

  1. RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock"); 
  2. RLock rLock = readWriteLock.writeLock(); 
  3. try { 
  4.     rLock.lock(); 
  5.     //業(yè)務(wù)操作 
  6. } catch (InterruptedException e) { 
  7.    log.error(e); 
  8. } finally { 
  9.     rLock.unlock(); 

將讀鎖和寫(xiě)鎖分開(kāi),最大的好處是提升讀操作的性能,因?yàn)樽x和讀之間是共享的,不存在互斥性。而我們的實(shí)際業(yè)務(wù)場(chǎng)景中,絕大多數(shù)數(shù)據(jù)操作都是讀操作。所以,如果提升了讀操作的性能,也就會(huì)提升整個(gè)鎖的性能。

下面總結(jié)一個(gè)讀寫(xiě)鎖的特點(diǎn):

  • 讀與讀是共享的,不互斥
  • 讀與寫(xiě)互斥
  • 寫(xiě)與寫(xiě)互斥

6.2 鎖分段

此外,為了減小鎖的粒度,比較常見(jiàn)的做法是將大鎖:分段。

在java中ConcurrentHashMap,就是將數(shù)據(jù)分為16段,每一段都有單獨(dú)的鎖,并且處于不同鎖段的數(shù)據(jù)互不干擾,以此來(lái)提升鎖的性能。

放在實(shí)際業(yè)務(wù)場(chǎng)景中,我們可以這樣做:

比如在秒殺扣庫(kù)存的場(chǎng)景中,現(xiàn)在的庫(kù)存中有2000個(gè)商品,用戶可以秒殺。為了防止出現(xiàn)超賣的情況,通常情況下,可以對(duì)庫(kù)存加鎖。如果有1W的用戶競(jìng)爭(zhēng)同一把鎖,顯然系統(tǒng)吞吐量會(huì)非常低。

為了提升系統(tǒng)性能,我們可以將庫(kù)存分段,比如:分為100段,這樣每段就有20個(gè)商品可以參與秒殺。

在秒殺的過(guò)程中,先把用戶id獲取hash值,然后除以100取模。模為1的用戶訪問(wèn)第1段庫(kù)存,模為2的用戶訪問(wèn)第2段庫(kù)存,模為3的用戶訪問(wèn)第3段庫(kù)存,后面以此類推,到最后模為100的用戶訪問(wèn)第100段庫(kù)存。

如此一來(lái),在多線程環(huán)境中,可以大大的減少鎖的沖突。以前多個(gè)線程只能同時(shí)競(jìng)爭(zhēng)1把鎖,尤其在秒殺的場(chǎng)景中,競(jìng)爭(zhēng)太激烈了,簡(jiǎn)直可以用慘絕人寰來(lái)形容,其后果是導(dǎo)致絕大數(shù)線程在鎖等待。現(xiàn)在多個(gè)線程同時(shí)競(jìng)爭(zhēng)100把鎖,等待的線程變少了,從而系統(tǒng)吞吐量也就提升了。

需要注意的地方是:將鎖分段雖說(shuō)可以提升系統(tǒng)的性能,但它也會(huì)讓系統(tǒng)的復(fù)雜度提升不少。因?yàn)樗枰腩~外的路由算法,跨段統(tǒng)計(jì)等功能。我們?cè)趯?shí)際業(yè)務(wù)場(chǎng)景中,需要綜合考慮,不是說(shuō)一定要將鎖分段。

7 鎖超時(shí)問(wèn)題

我在前面提到過(guò),如果線程A加鎖成功了,但是由于業(yè)務(wù)功能耗時(shí)時(shí)間很長(zhǎng),超過(guò)了設(shè)置的超時(shí)時(shí)間,這時(shí)候redis會(huì)自動(dòng)釋放線程A加的鎖。

有些朋友可能會(huì)說(shuō):到了超時(shí)時(shí)間,鎖被釋放了就釋放了唄,對(duì)功能又沒(méi)啥影響。

答:錯(cuò),錯(cuò),錯(cuò)。對(duì)功能其實(shí)有影響。

通常我們加鎖的目的是:為了防止訪問(wèn)臨界資源時(shí),出現(xiàn)數(shù)據(jù)異常的情況。比如:線程A在修改數(shù)據(jù)C的值,線程B也在修改數(shù)據(jù)C的值,如果不做控制,在并發(fā)情況下,數(shù)據(jù)C的值會(huì)出問(wèn)題。

為了保證某個(gè)方法,或者段代碼的互斥性,即如果線程A執(zhí)行了某段代碼,是不允許其他線程在某一時(shí)刻同時(shí)執(zhí)行的,我們可以用synchronized關(guān)鍵字加鎖。

但這種鎖有很大的局限性,只能保證單個(gè)節(jié)點(diǎn)的互斥性。如果需要在多個(gè)節(jié)點(diǎn)中保持互斥性,就需要用redis分布式鎖。

做了這么多鋪墊,現(xiàn)在回到正題。

假設(shè)線程A加redis分布式鎖的代碼,包含代碼1和代碼2兩段代碼。

由于該線程要執(zhí)行的業(yè)務(wù)操作非常耗時(shí),程序在執(zhí)行完代碼1的時(shí),已經(jīng)到了設(shè)置的超時(shí)時(shí)間,redis自動(dòng)釋放了鎖。而代碼2還沒(méi)來(lái)得及執(zhí)行。

此時(shí),代碼2相當(dāng)于裸奔的狀態(tài),無(wú)法保證互斥性。假如它里面訪問(wèn)了臨界資源,并且其他線程也訪問(wèn)了該資源,可能就會(huì)出現(xiàn)數(shù)據(jù)異常的情況。(PS:我說(shuō)的訪問(wèn)臨界資源,不單單指讀取,還包含寫(xiě)入)

那么,如何解決這個(gè)問(wèn)題呢?

答:如果達(dá)到了超時(shí)時(shí)間,但業(yè)務(wù)代碼還沒(méi)執(zhí)行完,需要給鎖自動(dòng)續(xù)期。

我們可以使用TimerTask類,來(lái)實(shí)現(xiàn)自動(dòng)續(xù)期的功能:

  1. Timer timer = new Timer();  
  2. timer.schedule(new TimerTask() { 
  3.     @Override 
  4.     public void run(Timeout timeout) throws Exception { 
  5.       //自動(dòng)續(xù)期邏輯 
  6.     } 
  7. }, 10000, TimeUnit.MILLISECONDS); 
  8.          

獲取鎖之后,自動(dòng)開(kāi)啟一個(gè)定時(shí)任務(wù),每隔10秒鐘,自動(dòng)刷新一次過(guò)期時(shí)間。這種機(jī)制在redisson框架中,有個(gè)比較霸氣的名字:watch dog,即傳說(shuō)中的看門(mén)狗。

當(dāng)然自動(dòng)續(xù)期功能,我們還是優(yōu)先推薦使用lua腳本實(shí)現(xiàn),比如:

  1. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then  
  2.    redis.call('pexpire', KEYS[1], ARGV[1]); 
  3.   return 1;  
  4. end
  5. return 0; 

需要注意的地方是:在實(shí)現(xiàn)自動(dòng)續(xù)期功能時(shí),還需要設(shè)置一個(gè)總的過(guò)期時(shí)間,可以跟redisson保持一致,設(shè)置成30秒。如果業(yè)務(wù)代碼到了這個(gè)總的過(guò)期時(shí)間,還沒(méi)有執(zhí)行完,就不再自動(dòng)續(xù)期了。

自動(dòng)續(xù)期的功能是獲取鎖之后開(kāi)啟一個(gè)定時(shí)任務(wù),每隔10秒判斷一下鎖是否存在,如果存在,則刷新過(guò)期時(shí)間。如果續(xù)期3次,也就是30秒之后,業(yè)務(wù)方法還是沒(méi)有執(zhí)行完,就不再續(xù)期了。

8 主從復(fù)制的問(wèn)題

上面花了這么多篇幅介紹的內(nèi)容,對(duì)單個(gè)redis實(shí)例是沒(méi)有問(wèn)題的。

but,如果redis存在多個(gè)實(shí)例。比如:做了主從,或者使用了哨兵模式,基于redis的分布式鎖的功能,就會(huì)出現(xiàn)問(wèn)題。

具體是什么問(wèn)題?

假設(shè)redis現(xiàn)在用的主從模式,1個(gè)master節(jié)點(diǎn),3個(gè)slave節(jié)點(diǎn)。master節(jié)點(diǎn)負(fù)責(zé)寫(xiě)數(shù)據(jù),slave節(jié)點(diǎn)負(fù)責(zé)讀數(shù)據(jù)。

本來(lái)是和諧共處,相安無(wú)事的。redis加鎖操作,都在master上進(jìn)行,加鎖成功后,再異步同步給所有的slave。

突然有一天,master節(jié)點(diǎn)由于某些不可逆的原因,掛掉了。

這樣需要找一個(gè)slave升級(jí)為新的master節(jié)點(diǎn),假如slave1被選舉出來(lái)了。

如果有個(gè)鎖A比較悲催,剛加鎖成功master就掛了,還沒(méi)來(lái)得及同步到slave1。

這樣會(huì)導(dǎo)致新master節(jié)點(diǎn)中的鎖A丟失了。后面,如果有新的線程,使用鎖A加鎖,依然可以成功,分布式鎖失效了。

那么,如何解決這個(gè)問(wèn)題呢?

答:redisson框架為了解決這個(gè)問(wèn)題,提供了一個(gè)專門(mén)的類:RedissonRedLock,使用了Redlock算法。

RedissonRedLock解決問(wèn)題的思路如下:

  1. 需要搭建幾套相互獨(dú)立的redis環(huán)境,假如我們?cè)谶@里搭建了5套。
  2. 每套環(huán)境都有一個(gè)redisson node節(jié)點(diǎn)。
  3. 多個(gè)redisson node節(jié)點(diǎn)組成了RedissonRedLock。
  4. 環(huán)境包含:?jiǎn)螜C(jī)、主從、哨兵和集群模式,可以是一種或者多種混合。

在這里我們以主從為例,架構(gòu)圖如下:

RedissonRedLock加鎖過(guò)程如下:

  1. 獲取所有的redisson node節(jié)點(diǎn)信息,循環(huán)向所有的redisson node節(jié)點(diǎn)加鎖,假設(shè)節(jié)點(diǎn)數(shù)為N,例子中N等于5。
  2. 如果在N個(gè)節(jié)點(diǎn)當(dāng)中,有N/2 + 1個(gè)節(jié)點(diǎn)加鎖成功了,那么整個(gè)RedissonRedLock加鎖是成功的。
  3. 如果在N個(gè)節(jié)點(diǎn)當(dāng)中,小于N/2 + 1個(gè)節(jié)點(diǎn)加鎖成功,那么整個(gè)RedissonRedLock加鎖是失敗的。
  4. 如果中途發(fā)現(xiàn)各個(gè)節(jié)點(diǎn)加鎖的總耗時(shí),大于等于設(shè)置的最大等待時(shí)間,則直接返回失敗。

從上面可以看出,使用Redlock算法,確實(shí)能解決多實(shí)例場(chǎng)景中,假如master節(jié)點(diǎn)掛了,導(dǎo)致分布式鎖失效的問(wèn)題。

但也引出了一些新問(wèn)題,比如:

需要額外搭建多套環(huán)境,申請(qǐng)更多的資源,需要評(píng)估一下成本和性價(jià)比。

如果有N個(gè)redisson node節(jié)點(diǎn),需要加鎖N次,最少也需要加鎖N/2+1次,才知道redlock加鎖是否成功。顯然,增加了額外的時(shí)間成本,有點(diǎn)得不償失。

由此可見(jiàn),在實(shí)際業(yè)務(wù)場(chǎng)景,尤其是高并發(fā)業(yè)務(wù)中,RedissonRedLock其實(shí)使用的并不多。

在分布式環(huán)境中,CAP是繞不過(guò)去的。

CAP指的是在一個(gè)分布式系統(tǒng)中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分區(qū)容錯(cuò)性(Partition tolerance)

這三個(gè)要素最多只能同時(shí)實(shí)現(xiàn)兩點(diǎn),不可能三者兼顧。

如果你的實(shí)際業(yè)務(wù)場(chǎng)景,更需要的是保證數(shù)據(jù)一致性。那么請(qǐng)使用CP類型的分布式鎖,比如:zookeeper,它是基于磁盤(pán)的,性能可能沒(méi)那么好,但數(shù)據(jù)一般不會(huì)丟。

如果你的實(shí)際業(yè)務(wù)場(chǎng)景,更需要的是保證數(shù)據(jù)高可用性。那么請(qǐng)使用AP類型的分布式鎖,比如:redis,它是基于內(nèi)存的,性能比較好,但有丟失數(shù)據(jù)的風(fēng)險(xiǎn)。

其實(shí),在我們絕大多數(shù)分布式業(yè)務(wù)場(chǎng)景中,使用redis分布式鎖就夠了,真的別太較真。因?yàn)閿?shù)據(jù)不一致問(wèn)題,可以通過(guò)最終一致性方案解決。但如果系統(tǒng)不可用了,對(duì)用戶來(lái)說(shuō)是暴擊一萬(wàn)點(diǎn)傷害。

本文轉(zhuǎn)載自微信公眾號(hào)「蘇三說(shuō)技術(shù)」

 

責(zé)任編輯:姜華 來(lái)源: 蘇三說(shuō)技術(shù)
相關(guān)推薦

2024-05-29 11:24:27

2021-09-17 07:51:24

RedissonRedis分布式

2022-12-18 20:07:55

Redis分布式

2019-06-19 15:40:06

分布式鎖RedisJava

2017-07-21 07:37:20

2022-04-08 08:27:08

分布式鎖系統(tǒng)

2022-03-07 08:14:27

并發(fā)分布式

2025-09-29 00:00:00

2020-04-23 11:18:14

Redis分布式

2022-06-16 08:01:24

redis分布式鎖

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2016-09-27 11:22:28

云計(jì)算

2022-11-14 07:23:32

RedisJedis分布式鎖

2023-11-03 14:42:36

異步執(zhí)行開(kāi)發(fā)架構(gòu)

2023-03-01 08:07:51

2024-10-07 10:07:31

2020-11-16 12:55:41

Redis分布式鎖Zookeeper

2022-09-19 08:17:09

Redis分布式
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

欧美在线观看网站| 91精品一区二区三区在线观看| 国产在线精品日韩| 精品久久久久久久久久久久久久久久| 中文字幕中文字幕精品| 欧美在线免费播放| 大桥未久一区二区| 亚洲 美腿 欧美 偷拍| 首页国产欧美久久| 欧美插天视频在线播放| 中文字幕日韩三级片| 青青热久免费精品视频在线18| 国产精品久久久久影院亚瑟| 成人xxxxx色| 无码人妻熟妇av又粗又大| 99久久夜色精品国产亚洲狼| 999久久精品| 久久久久久综合| 成人性生交xxxxx网站| 久久精品性爱视频| 精品久久影院| 亚洲国产精品高清久久久| 亚洲少妇久久久| 2021天堂中文幕一二区在线观| 日本一区二区在线不卡| 51国偷自产一区二区三区的来源 | 青青久久aⅴ北条麻妃| 欧美日韩色视频| 经典一区二区| 亚洲国产小视频| 中文字幕一区二区在线观看视频| 黄色亚洲网站| 午夜视频久久久久久| 自拍另类欧美| 国产色a在线| 91在线高清观看| 成人在线观看91| 国产精品久久久久久久久久久久久久久久| 国产亚洲综合精品| 久久全球大尺度高清视频| 亚洲AV成人无码精电影在线| 久久99国产精一区二区三区| 亚洲国产精品yw在线观看 | 91黑人精品一区二区三区| 18成人免费观看视频| 欧美成人四级hd版| 永久av免费网站| 成人av动漫在线观看| 精品一区二区三区四区在线| 漂亮人妻被黑人久久精品| 国产午夜久久av| 欧美喷潮久久久xxxxx| 人妻无码视频一区二区三区| 日本不良网站在线观看| 一区二区三区在线视频免费观看| 国产av第一区| 99福利在线| 亚洲欧美另类久久久精品 | 国产精品久久国产精品| 亚洲天堂中文网| 久久婷婷影院| 国产精品久久精品| 中国精品一区二区| 日本女优在线视频一区二区| 国产精品福利网站| 国内av在线播放| 男人操女人的视频在线观看欧美| 国产精品久久久久国产a级| 91丨九色丨海角社区| 日日摸夜夜添夜夜添国产精品| 国产97在线播放| 国产成人麻豆免费观看| 美女视频黄 久久| 国产精品一区二区三区免费视频| 夜夜狠狠擅视频| 国产精品自拍一区| 国产伦理久久久| 日韩三级电影网| 久久久国际精品| 亚洲免费在线精品一区| 搞黄视频免费在线观看| 久久久综合激的五月天| 快播日韩欧美| melody高清在线观看| 中文字幕不卡三区| 久久免费视频2| 好吊日av在线| 欧美性猛交xxxxx免费看| 亚洲五月天综合| 99久热在线精品视频观看| 亚洲黄色大片| 亚洲韩国欧洲国产日产av| 北岛玲一区二区| 欧美少妇性xxxx| 久久av在线看| 日韩在线视频免费播放| 美女视频一区在线观看| 国产66精品久久久久999小说| 四虎精品在永久在线观看| 国产三级欧美三级| 超碰97免费观看| 丁香花高清在线观看完整版| 色综合 综合色| 中文字幕 欧美日韩| 999精品视频在这里| 亚洲人成绝费网站色www| 国产又粗又猛又爽又黄的视频四季 | 久草免费资源站| 嫩草影视亚洲| 久久91超碰青草是什么| 亚洲成熟少妇视频在线观看| 狠狠色综合播放一区二区| 久久精品ww人人做人人爽| www在线播放| 亚洲国产精品久久人人爱| 国产视频一区二区视频| 伊人精品综合| 最近2019年中文视频免费在线观看| 精品无码一区二区三区电影桃花| 日韩高清国产一区在线| 豆国产97在线| 婷婷成人激情| 亚洲黄色录像| 欧美精品1区2区3区| 欧亚乱熟女一区二区在线| 999国产精品视频| 日本久久久久久久| 亚洲精品国产一区二| 国产精品久久久久久户外露出| 水蜜桃色314在线观看| 亚洲我射av| 中文字幕av一区二区| 日本学生初尝黑人巨免费视频| 久久精品国产精品青草| 欧美激情导航| 末成年女av片一区二区下载| 91精品国产欧美日韩| 免费福利视频网站| 亚洲一区成人| 国产视频99| 性直播体位视频在线观看| 欧美年轻男男videosbes| 中文字幕免费视频| 久久不射中文字幕| 九9re精品视频在线观看re6| 国内高清免费在线视频| 日韩天堂在线观看| 暗呦丨小u女国产精品| 久久99久久精品欧美| 色综合久久久久久久久五月| 26uuu亚洲电影| 亚洲国产精品电影| 91看片在线播放| 不卡区在线中文字幕| 丁香色欲久久久久久综合网| 亚洲自拍第三页| 精品久久久久中文字幕小说| 国产成人亚洲精品| 欧美视频免费一区二区三区| 精品日韩中文字幕| 亚洲精品乱码久久久久久不卡| 亚洲无毛电影| 国产伦精品一区二区三区高清版| xxxx视频在线| 国产丝袜精品第一页| 日韩精品人妻中文字幕| 99精品一区二区三区| 成 年 人 黄 色 大 片大 全| 精品久久97| 777777777亚洲妇女| 亚洲 欧美 自拍偷拍| 日韩欧美有码在线| 国产精成人品免费观看| 精品制服美女久久| 神马午夜伦理影院| 9l视频自拍九色9l视频成人| 992tv成人免费视频| 免费在线稳定资源站| 欧美特级限制片免费在线观看| 四虎影视一区二区| 国产黑丝在线一区二区三区| 欧美午夜性视频| 国产一区二区高清在线| 久久国产精品影片| 神马久久久久久久久久| 色老头久久综合| 精品国产视频在线观看| 成人精品视频一区二区三区| 麻豆av免费在线| 日本电影一区二区| 99re国产| 92国产精品| 久久久国产精品免费| 蜜臀av中文字幕| 91黄色小视频| 成人免费视频网站入口::| 99久久免费精品高清特色大片| 国产精品视频黄色| 97久久视频| 91精品视频播放| 国产精品高颜值在线观看| 亚洲精品一区久久久久久| 亚洲天堂网视频| 精品女厕一区二区三区| 99热99这里只有精品| av午夜一区麻豆| 天堂av8在线| 久久精品女人天堂| www.亚洲成人网| 欧美日一区二区| 国产视频在线观看一区| 久久精品xxxxx| 久久久久久有精品国产| av中文字幕一区二区三区| 精品粉嫩超白一线天av| 中文字幕无码乱码人妻日韩精品| 亚洲国产综合人成综合网站| 91香蕉国产视频| 91在线视频官网| 1314成人网| 天堂成人国产精品一区| www.亚洲视频.com| 欧美激情理论| 日本午夜精品一区二区| 国产精品sss在线观看av| 国产欧亚日韩视频| 成人自拍av| 欧美一区亚洲一区| 性欧美videoshd高清| 色播久久人人爽人人爽人人片视av| 婷婷丁香花五月天| 精品久久一区二区三区| 97人妻一区二区精品免费视频| 一本久久精品一区二区| 国产奶水涨喷在线播放| 一区二区高清在线| 超碰手机在线观看| 国产精品久久久久久久岛一牛影视 | 免费观看一级特黄欧美大片| 免费在线a视频| 亚洲国产综合在线看不卡| 99久热在线精品视频| 天天揉久久久久亚洲精品| 日韩久久久久久久| 国产精品一线天粉嫩av| 欧美日韩精品久久| 伊人精品一区| 欧美一区二区高清在线观看| 偷拍自拍一区| 欧美久久久久久久| 最新亚洲精品| 欧美日韩国产精品一卡| 国产一区三区在线播放| 日本精品一区二区三区视频 | 日本一区免费观看| 亚洲人成网www| 欧美日韩中文国产一区发布 | 精品午夜久久福利影院| 91欧美视频在线| 麻豆精品精品国产自在97香蕉 | 精品欧美一区二区三区在线观看| 26uuu亚洲国产精品| 亚洲性色av| 国产精品成人久久久久| av在线不卡精品| 成人性生交大片免费观看嘿嘿视频| 高清久久一区| 国产精品国产亚洲精品看不卡15 | 九一九一国产精品| 古装做爰无遮挡三级聊斋艳谭| 国产成人精品免费网站| 韩国一区二区三区四区| 99精品在线免费| 女人黄色一级片| 亚洲男人的天堂在线观看| 欧美人妻精品一区二区免费看| 亚洲国产一区二区在线播放| 国产尤物在线视频| 欧美视频中文字幕| 国产特黄一级片| 日韩av网址在线观看| 精品亚洲综合| 久久夜色精品国产欧美乱| 国产又色又爽又黄刺激在线视频| 91高清视频免费| 日韩专区视频网站| 国产精品视频在线免费观看| 国产在视频线精品视频www666| 国产精品av免费| 99精品久久| 自拍偷拍一区二区三区四区| 懂色av一区二区夜夜嗨| 亚洲精品视频久久久| 亚洲日本va在线观看| 日韩欧美亚洲视频| 欧美精选午夜久久久乱码6080| www黄色网址| 亚洲欧美日韩一区在线| a级影片在线| 日韩美女免费视频| 精品视频一二| 欧美精品123| 欧美午夜a级限制福利片| 日日摸天天爽天天爽视频| 国产精品一品视频| 一级黄色片大全| 亚洲一区二区三区不卡国产欧美| 麻豆亚洲av熟女国产一区二 | 久久99视频精品| 忘忧草在线www成人影院| 国产精品亚洲综合| 天堂美国久久| 日韩欧美在线免费观看视频| 懂色中文一区二区在线播放| 国产精品视频看看| 欧美日韩国产在线播放| 国产黄色片免费| 中文字幕亚洲无线码在线一区| av漫画网站在线观看| 91九色单男在线观看| 欧美男男gaytwinkfreevideos| 成人午夜视频免费观看| 美女国产一区二区| 深爱五月激情网| 亚洲一卡二卡三卡四卡五卡| 91在线公开视频| 中文字幕国产亚洲2019| 中文在线а√天堂| 国产亚洲情侣一区二区无| 91精品啪在线观看国产18| 最近中文字幕一区二区| 久久综合九色综合欧美就去吻| 久久国产一级片| 欧美一区二区成人| 成人在线免费看片| 成人精品福利视频| 日韩一区电影| 韩国中文字幕av| 国产欧美精品一区| 伊人久久久久久久久久久久| 日韩精品高清在线观看| 国产精品论坛| 久草热久草热线频97精品| 亚洲免费成人| 国产精品嫩草av| 狠狠久久五月精品中文字幕| 天天射天天操天天干| 26uuu另类亚洲欧美日本老年| 欧美大胆a级| 久色视频在线播放| 99精品视频一区二区三区| 久久精品欧美一区二区| 精品1区2区在线观看| 国产福利电影在线播放| 精品久久久久久一区| 亚洲欧洲一区| www.欧美三级电影.com| 福利视频999| 99色在线观看| 中文字幕一区二区三区乱码在线| 成人黄色生活片| 中文字幕手机在线视频| 国产久卡久卡久卡久卡视频精品| 国产精品一区二区欧美黑人喷潮水| 欧美黄色成人| 最近中文字幕免费mv| 国产精品一区二区在线观看不卡| 波多野结衣亚洲色图| 精品1区2区在线观看| 中文在线中文资源| 日本一区二区三区视频在线观看| 美日韩一区二区三区| 一区二区国产精品精华液| 日韩欧美国产一区二区在线播放| 色呦呦在线免费观看| 久久久www免费人成黑人精品| 久久精品30| 日韩欧美国产成人精品免费| 亚洲电影天堂av| 亚洲成av在线| 国产一二三四五| 久久综合99re88久久爱| 中国一级片黄色一级片黄| 久久av.com| 天天躁日日躁狠狠躁欧美巨大小说| jizz欧美激情18| 亚洲乱码国产乱码精品精98午夜| 色婷婷av一区二区三| 国产精品美乳一区二区免费 | 成人午夜av电影| 91久久国产综合久久91| 久久亚洲私人国产精品va| 久久国产精品免费精品3p| wwwwxxxx日韩| 亚洲午夜久久久久久久久久久 | 国产九色91回来了| 欧美日韩成人黄色| 国产成人1区| 亚洲精品鲁一鲁一区二区三区| 欧美午夜激情在线|