自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

對(duì)不起,網(wǎng)上找的Redis分布式鎖都有漏洞!

安全 漏洞 分布式 Redis
基于 Redis 的分布式鎖對(duì)大家來(lái)說(shuō)并不陌生,可是你的分布式鎖有失敗的時(shí)候嗎?在失敗的時(shí)候可曾懷疑過(guò)你在用的分布式鎖真的靠譜嗎?以下是結(jié)合自己的踩坑經(jīng)驗(yàn)總結(jié)的一些經(jīng)驗(yàn)之談。

 基于 Redis 的分布式鎖對(duì)大家來(lái)說(shuō)并不陌生,可是你的分布式鎖有失敗的時(shí)候嗎?在失敗的時(shí)候可曾懷疑過(guò)你在用的分布式鎖真的靠譜嗎?以下是結(jié)合自己的踩坑經(jīng)驗(yàn)總結(jié)的一些經(jīng)驗(yàn)之談。

[[334734]]

 

圖片來(lái)自 Pexels

你真的需要分布式鎖嗎?

用到分布式鎖說(shuō)明遇到了多個(gè)進(jìn)程共同訪問(wèn)同一個(gè)資源的問(wèn)題。

一般是在兩個(gè)場(chǎng)景下會(huì)防止對(duì)同一個(gè)資源的重復(fù)訪問(wèn):

  • 提高效率。比如多個(gè)節(jié)點(diǎn)計(jì)算同一批任務(wù),如果某個(gè)任務(wù)已經(jīng)有節(jié)點(diǎn)在計(jì)算了,那其他節(jié)點(diǎn)就不用重復(fù)計(jì)算了,以免浪費(fèi)計(jì)算資源。不過(guò)重復(fù)計(jì)算也沒(méi)事,不會(huì)造成其他更大的損失。也就是允許偶爾的失敗。
  • 保證正確性。這種情況對(duì)鎖的要求就很高了,如果重復(fù)計(jì)算,會(huì)對(duì)正確性造成影響。這種不允許失敗。

引入分布式鎖勢(shì)必要引入一個(gè)第三方的基礎(chǔ)設(shè)施,比如 MySQL,Redis,Zookeeper 等。

這些實(shí)現(xiàn)分布式鎖的基礎(chǔ)設(shè)施出問(wèn)題了,也會(huì)影響業(yè)務(wù),所以在使用分布式鎖前可以考慮下是否可以不用加鎖的方式實(shí)現(xiàn)?

不過(guò)這個(gè)不在本文的討論范圍內(nèi),本文假設(shè)加鎖的需求是合理的,并且偏向于上面的第二種情況,為什么是偏向?因?yàn)椴淮嬖?100% 靠譜的分布式鎖,看完下面的內(nèi)容就明白了。

從一個(gè)簡(jiǎn)單的分布式鎖實(shí)現(xiàn)說(shuō)起

分布式鎖的 Redis 實(shí)現(xiàn)很常見,自己實(shí)現(xiàn)和使用第三方庫(kù)都很簡(jiǎn)單,至少看上去是這樣的,這里就介紹一個(gè)最簡(jiǎn)單靠譜的 Redis 實(shí)現(xiàn)。

最簡(jiǎn)單的實(shí)現(xiàn)

實(shí)現(xiàn)很經(jīng)典了,這里只提兩個(gè)要點(diǎn):

  • 加鎖和解鎖的鎖必須是同一個(gè),常見的解決方案是給每個(gè)鎖一個(gè)鑰匙(唯一 ID),加鎖時(shí)生成,解鎖時(shí)判斷。
  • 不能讓一個(gè)資源永久加鎖。常見的解決方案是給一個(gè)鎖的過(guò)期時(shí)間。當(dāng)然了還有其他方案,后面再說(shuō)。

一個(gè)可復(fù)制粘貼的實(shí)現(xiàn)方式如下:

加鎖:

  1. public static boolean tryLock(String key, String uniqueId, int seconds) { 
  2.     return "OK".equals(jedis.set(key, uniqueId, "NX""EX", seconds)); 

這里調(diào)用了 SET key value PX milliseoncds NX,不明白這個(gè)命令的可以參考 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]:

  1. https://redis.io/commands/set 

解鎖:

  1. public static boolean releaseLock(String key, String uniqueId) { 
  2.     String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + 
  3.             "return redis.call('del', KEYS[1]) else return 0 end"
  4.     return jedis.eval( 
  5.         luaScript,  
  6.         Collections.singletonList(key),  
  7.         Collections.singletonList(uniqueId) 
  8.     ).equals(1L); 

這段實(shí)現(xiàn)的精髓在那個(gè)簡(jiǎn)單的 Lua 腳本上,先判斷唯一 ID 是否相等再操作。

靠譜嗎?

這樣的實(shí)現(xiàn)有什么問(wèn)題呢?

  • 單點(diǎn)問(wèn)題。上面的實(shí)現(xiàn)只要一個(gè) Master 節(jié)點(diǎn)就能搞定,這里的單點(diǎn)指的是單 Master,就算是個(gè)集群,如果加鎖成功后,鎖從 Master 復(fù)制到 Slave 的時(shí)候掛了,也是會(huì)出現(xiàn)同一資源被多個(gè) Client 加鎖的。
  • 執(zhí)行時(shí)間超過(guò)了鎖的過(guò)期時(shí)間。上面寫到為了不出現(xiàn)一直上鎖的情況,加了一個(gè)兜底的過(guò)期時(shí)間,時(shí)間到了鎖自動(dòng)釋放,但是,如果在這期間任務(wù)并沒(méi)有做完怎么辦?由于 GC 或者網(wǎng)絡(luò)延遲導(dǎo)致的任務(wù)時(shí)間變長(zhǎng),很難保證任務(wù)一定能在鎖的過(guò)期時(shí)間內(nèi)完成。

如何解決這兩個(gè)問(wèn)題呢?試試看更復(fù)雜的實(shí)現(xiàn)吧。

Redlock 算法

對(duì)于第一個(gè)單點(diǎn)問(wèn)題,順著 Redis 的思路,接下來(lái)想到的肯定是 Redlock 了。

Redlock 為了解決單機(jī)的問(wèn)題,需要多個(gè)(大于 2)Redis 的 Master 節(jié)點(diǎn),多個(gè) Master 節(jié)點(diǎn)互相獨(dú)立,沒(méi)有數(shù)據(jù)同步。

Redlock 的實(shí)現(xiàn)如下:

①獲取當(dāng)前時(shí)間。

②依次獲取 N 個(gè)節(jié)點(diǎn)的鎖。每個(gè)節(jié)點(diǎn)加鎖的實(shí)現(xiàn)方式同上。這里有個(gè)細(xì)節(jié),就是每次獲取鎖的時(shí)候的過(guò)期時(shí)間都不同,需要減去之前獲取鎖的操作的耗時(shí),

比如傳入的鎖的過(guò)期時(shí)間為 500ms,獲取第一個(gè)節(jié)點(diǎn)的鎖花了 1ms,那么第一個(gè)節(jié)點(diǎn)的鎖的過(guò)期時(shí)間就是 499ms;獲取第二個(gè)節(jié)點(diǎn)的鎖花了 2ms,那么第二個(gè)節(jié)點(diǎn)的鎖的過(guò)期時(shí)間就是 497ms。

如果鎖的過(guò)期時(shí)間小于等于 0 了,說(shuō)明整個(gè)獲取鎖的操作超時(shí)了,整個(gè)操作失敗。

③判斷是否獲取鎖成功。如果 Client 在上述步驟中獲取到了(N/2+1)個(gè)節(jié)點(diǎn)鎖,并且每個(gè)鎖的過(guò)期時(shí)間都是大于 0 的,則獲取鎖成功,否則失敗。失敗時(shí)釋放鎖。

④釋放鎖。對(duì)所有節(jié)點(diǎn)發(fā)送釋放鎖的指令,每個(gè)節(jié)點(diǎn)的實(shí)現(xiàn)邏輯和上面的簡(jiǎn)單實(shí)現(xiàn)一樣。

為什么要對(duì)所有節(jié)點(diǎn)操作?因?yàn)榉植际綀?chǎng)景下從一個(gè)節(jié)點(diǎn)獲取鎖失敗不代表在那個(gè)節(jié)點(diǎn)上加速失敗,可能實(shí)際上加鎖已經(jīng)成功了,但是返回時(shí)因?yàn)榫W(wǎng)絡(luò)抖動(dòng)超時(shí)了。

以上就是大家常見的 Redlock 實(shí)現(xiàn)的描述了,一眼看上去就是簡(jiǎn)單版本的多 Master 版本,如果真是這樣就太簡(jiǎn)單了,接下來(lái)分析下這個(gè)算法在各個(gè)場(chǎng)景下是怎樣被玩壞的。

分布式鎖的坑

高并發(fā)場(chǎng)景下的問(wèn)題

以下問(wèn)題不是說(shuō)在并發(fā)不高的場(chǎng)景下不容易出現(xiàn),只是在高并發(fā)場(chǎng)景下出現(xiàn)的概率更高些而已。

性能問(wèn)題來(lái)自于以下兩方面:

①獲取鎖的時(shí)間上。如果 Redlock 運(yùn)用在高并發(fā)的場(chǎng)景下,存在 N 個(gè) Master 節(jié)點(diǎn),一個(gè)一個(gè)去請(qǐng)求,耗時(shí)會(huì)比較長(zhǎng),從而影響性能。

這個(gè)好解決,通過(guò)上面描述不難發(fā)現(xiàn),從多個(gè)節(jié)點(diǎn)獲取鎖的操作并不是一個(gè)同步操作,可以是異步操作,這樣可以多個(gè)節(jié)點(diǎn)同時(shí)獲取。

即使是并行處理的,還是得預(yù)估好獲取鎖的時(shí)間,保證鎖的 TTL>獲取鎖的時(shí)間+任務(wù)處理時(shí)間。

②被加鎖的資源太大。加鎖的方案本身就是會(huì)為了正確性而犧牲并發(fā)的,犧牲和資源大小成正比,這個(gè)時(shí)候可以考慮對(duì)資源做拆分。

拆分的方式有如下兩種:

①?gòu)臉I(yè)務(wù)上將鎖住的資源拆分成多段,每段分開加鎖。比如,我要對(duì)一個(gè)商戶做若干個(gè)操作,操作前要鎖住這個(gè)商戶,這時(shí)我可以將若干個(gè)操作拆成多個(gè)獨(dú)立的步驟分開加鎖,提高并發(fā)。

②用分桶的思想,將一個(gè)資源拆分成多個(gè)桶,一個(gè)加鎖失敗立即嘗試下一個(gè)。比如批量任務(wù)處理的場(chǎng)景,要處理 200w 個(gè)商戶的任務(wù),為了提高處理速度,用多個(gè)線程,每個(gè)線程取 100 個(gè)商戶處理,就得給這 100 個(gè)商戶加鎖。

如果不加處理,很難保證同一時(shí)刻兩個(gè)線程加鎖的商戶沒(méi)有重疊,這時(shí)可以按一個(gè)維度。

比如某個(gè)標(biāo)簽,對(duì)商戶進(jìn)行分桶,然后一個(gè)任務(wù)處理一個(gè)分桶,處理完這個(gè)分桶再處理下一個(gè)分桶,減少競(jìng)爭(zhēng)。

重試的問(wèn)題:無(wú)論是簡(jiǎn)單實(shí)現(xiàn)還是 Redlock 實(shí)現(xiàn),都會(huì)有重試的邏輯。

如果直接按上面的算法實(shí)現(xiàn),是會(huì)存在多個(gè) Client 幾乎在同一時(shí)刻獲取同一個(gè)鎖,然后每個(gè) Client 都鎖住了部分節(jié)點(diǎn),但是沒(méi)有一個(gè) Client 獲取大多數(shù)節(jié)點(diǎn)的情況。

解決的方案也很常見,在重試的時(shí)候讓多個(gè)節(jié)點(diǎn)錯(cuò)開,錯(cuò)開的方式就是在重試時(shí)間中加一個(gè)隨機(jī)時(shí)間。這樣并不能根治這個(gè)問(wèn)題,但是可以有效緩解問(wèn)題,親試有效。

節(jié)點(diǎn)宕機(jī)

對(duì)于單 Master 節(jié)點(diǎn)且沒(méi)有做持久化的場(chǎng)景,宕機(jī)就掛了,這個(gè)就必須在實(shí)現(xiàn)上支持重復(fù)操作,自己做好冪等。

對(duì)于多 Master 的場(chǎng)景,比如 Redlock,我們來(lái)看這樣一個(gè)場(chǎng)景:

  • 假設(shè)有 5 個(gè) Redis 的節(jié)點(diǎn):A、B、C、D、E,沒(méi)有做持久化。
  • Client1 從 A、B、C 這3 個(gè)節(jié)點(diǎn)獲取鎖成功,那么 client1 獲取鎖成功。
  • 節(jié)點(diǎn) C 掛了。
  • Client2 從 C、D、E 獲取鎖成功,client2 也獲取鎖成功,那么在同一時(shí)刻 Client1 和 Client2 同時(shí)獲取鎖,Redlock 被玩壞了。

怎么解決呢?最容易想到的方案是打開持久化。持久化可以做到持久化每一條 Redis 命令,但這對(duì)性能影響會(huì)很大,一般不會(huì)采用,如果不采用這種方式,在節(jié)點(diǎn)掛的時(shí)候肯定會(huì)損失小部分的數(shù)據(jù),可能我們的鎖就在其中。

另一個(gè)方案是延遲啟動(dòng)。就是一個(gè)節(jié)點(diǎn)掛了修復(fù)后,不立即加入,而是等待一段時(shí)間再加入,等待時(shí)間要大于宕機(jī)那一刻所有鎖的最大 TTL。

但這個(gè)方案依然不能解決問(wèn)題,如果在上述步驟 3 中 B 和 C 都掛了呢,那么只剩 A、D、E 三個(gè)節(jié)點(diǎn),從 D 和 E 獲取鎖成功就可以了,還是會(huì)出問(wèn)題。

那么只能增加 Master 節(jié)點(diǎn)的總量,緩解這個(gè)問(wèn)題了。增加 Master 節(jié)點(diǎn)會(huì)提高穩(wěn)定性,但是也增加了成本,需要在兩者之間權(quán)衡。

任務(wù)執(zhí)行時(shí)間超過(guò)鎖的 TTL

之前產(chǎn)線上出現(xiàn)過(guò)因?yàn)榫W(wǎng)絡(luò)延遲導(dǎo)致任務(wù)的執(zhí)行時(shí)間遠(yuǎn)超預(yù)期,鎖過(guò)期,被多個(gè)線程執(zhí)行的情況。

這個(gè)問(wèn)題是所有分布式鎖都要面臨的問(wèn)題,包括基于 Zookeeper 和 DB 實(shí)現(xiàn)的分布式鎖,這是鎖過(guò)期了和 Client 不知道鎖過(guò)期了之間的矛盾。

在加鎖的時(shí)候,我們一般都會(huì)給一個(gè)鎖的 TTL,這是為了防止加鎖后 Client 宕機(jī),鎖無(wú)法被釋放的問(wèn)題。

但是所有這種姿勢(shì)的用法都會(huì)面臨同一個(gè)問(wèn)題,就是沒(méi)發(fā)保證 Client 的執(zhí)行時(shí)間一定小于鎖的 TTL。

雖然大多數(shù)程序員都會(huì)樂(lè)觀的認(rèn)為這種情況不可能發(fā)生,我也曾經(jīng)這么認(rèn)為,直到被現(xiàn)實(shí)一次又一次的打臉。

Martin Kleppmann 也質(zhì)疑過(guò)這一點(diǎn),這里直接用他的圖:

 

  • Client1 獲取到鎖。
  • Client1 開始任務(wù),然后發(fā)生了 STW 的 GC,時(shí)間超過(guò)了鎖的過(guò)期時(shí)間。
  • Client2 獲取到鎖,開始了任務(wù)。
  • Client1 的 GC 結(jié)束,繼續(xù)任務(wù),這個(gè)時(shí)候 Client1 和 Client2 都認(rèn)為自己獲取了鎖,都會(huì)處理任務(wù),從而發(fā)生錯(cuò)誤。

Martin Kleppmann 舉的是 GC 的例子,我碰到的是網(wǎng)絡(luò)延遲的情況。不管是哪種情況,不可否認(rèn)的是這種情況無(wú)法避免,一旦出現(xiàn)很容易懵逼。

如何解決呢?一種解決方案是不設(shè)置 TTL,而是在獲取鎖成功后,給鎖加一個(gè) watchdog,watchdog 會(huì)起一個(gè)定時(shí)任務(wù),在鎖沒(méi)有被釋放且快要過(guò)期的時(shí)候會(huì)續(xù)期。

這樣說(shuō)有些抽象,下面結(jié)合 Redisson 源碼說(shuō)下:

  1. public class RedissonLock extends RedissonExpirable implements RLock { 
  2.     ... 
  3.     @Override 
  4.     public void lock() { 
  5.         try { 
  6.             lockInterruptibly(); 
  7.         } catch (InterruptedException e) { 
  8.             Thread.currentThread().interrupt(); 
  9.         } 
  10.     } 
  11.  
  12.     @Override 
  13.     public void lock(long leaseTime, TimeUnit unit) { 
  14.         try { 
  15.             lockInterruptibly(leaseTime, unit); 
  16.         } catch (InterruptedException e) { 
  17.             Thread.currentThread().interrupt(); 
  18.         } 
  19.     } 
  20.     ... 
  21.  } 

Redisson 常用的加鎖 API 是上面兩個(gè),一個(gè)是不傳入 TTL,這時(shí)是 Redisson 自己維護(hù),會(huì)主動(dòng)續(xù)期。

另外一種是自己傳入 TTL,這種 Redisson 就不會(huì)幫我們自動(dòng)續(xù)期了,或者自己將 leaseTime 的值傳成 -1,但是不建議這種方式,既然已經(jīng)有現(xiàn)成的 API 了,何必還要用這種奇怪的寫法呢。

接下來(lái)分析下不傳參的方法的加鎖邏輯:

  1. public class RedissonLock extends RedissonExpirable implements RLock { 
  2.  
  3.     ... 
  4.  
  5.     public static final long LOCK_EXPIRATION_INTERVAL_SECONDS = 30; 
  6.     protected long internalLockLeaseTime = TimeUnit.SECONDS.toMillis(LOCK_EXPIRATION_INTERVAL_SECONDS); 
  7.  
  8.  
  9.     @Override 
  10.     public void lock() { 
  11.         try { 
  12.             lockInterruptibly(); 
  13.         } catch (InterruptedException e) { 
  14.             Thread.currentThread().interrupt(); 
  15.         } 
  16.     } 
  17.  
  18.     @Override 
  19.     public void lockInterruptibly() throws InterruptedException { 
  20.         lockInterruptibly(-1, null); 
  21.     } 
  22.  
  23.     @Override 
  24.     public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException { 
  25.         long threadId = Thread.currentThread().getId(); 
  26.         Long ttl = tryAcquire(leaseTime, unit, threadId); 
  27.         // lock acquired 
  28.         if (ttl == null) { 
  29.             return
  30.         } 
  31.  
  32.         RFuture<RedissonLockEntry> future = subscribe(threadId); 
  33.         commandExecutor.syncSubscription(future); 
  34.  
  35.         try { 
  36.             while (true) { 
  37.                 ttl = tryAcquire(leaseTime, unit, threadId); 
  38.                 // lock acquired 
  39.                 if (ttl == null) { 
  40.                     break; 
  41.                 } 
  42.  
  43.                 // waiting for message 
  44.                 if (ttl >= 0) { 
  45.                     getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); 
  46.                 } else { 
  47.                     getEntry(threadId).getLatch().acquire(); 
  48.                 } 
  49.             } 
  50.         } finally { 
  51.             unsubscribe(future, threadId); 
  52.         } 
  53. //        get(lockAsync(leaseTime, unit)); 
  54.     } 
  55.  
  56.     private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) { 
  57.         return get(tryAcquireAsync(leaseTime, unit, threadId)); 
  58.     } 
  59.  
  60.     private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) { 
  61.         if (leaseTime != -1) { 
  62.             return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); 
  63.         } 
  64.         RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG); 
  65.         ttlRemainingFuture.addListener(new FutureListener<Long>() { 
  66.             @Override 
  67.             public void operationComplete(Future<Long> future) throws Exception { 
  68.                 if (!future.isSuccess()) { 
  69.                     return
  70.                 } 
  71.  
  72.                 Long ttlRemaining = future.getNow(); 
  73.                 // lock acquired 
  74.                 if (ttlRemaining == null) { 
  75.                     scheduleExpirationRenewal(threadId); 
  76.                 } 
  77.             } 
  78.         }); 
  79.         return ttlRemainingFuture; 
  80.     } 
  81.  
  82.     private void scheduleExpirationRenewal(final long threadId) { 
  83.         if (expirationRenewalMap.containsKey(getEntryName())) { 
  84.             return
  85.         } 
  86.  
  87.         Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { 
  88.             @Override 
  89.             public void run(Timeout timeout) throws Exception { 
  90.  
  91.                 RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
  92.                         "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + 
  93.                             "redis.call('pexpire', KEYS[1], ARGV[1]); " + 
  94.                             "return 1; " + 
  95.                         "end; " + 
  96.                         "return 0;"
  97.                           Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); 
  98.  
  99.                 future.addListener(new FutureListener<Boolean>() { 
  100.                     @Override 
  101.                     public void operationComplete(Future<Boolean> future) throws Exception { 
  102.                         expirationRenewalMap.remove(getEntryName()); 
  103.                         if (!future.isSuccess()) { 
  104.                             log.error("Can't update lock " + getName() + " expiration", future.cause()); 
  105.                             return
  106.                         } 
  107.  
  108.                         if (future.getNow()) { 
  109.                             // reschedule itself 
  110.                             scheduleExpirationRenewal(threadId); 
  111.                         } 
  112.                     } 
  113.                 }); 
  114.             } 
  115.         }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); 
  116.  
  117.         if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) { 
  118.             task.cancel(); 
  119.         } 
  120.     } 
  121.  
  122.  
  123.     ... 

可以看到,最后加鎖的邏輯會(huì)進(jìn)入到 org.redisson.RedissonLock#tryAcquireAsync 中,在獲取鎖成功后,會(huì)進(jìn)入 scheduleExpirationRenewal。

這里面初始化了一個(gè)定時(shí)器,dely 的時(shí)間是 internalLockLeaseTime/3。

在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 續(xù)期一次,每次 30s。

如果是基于 Zookeeper 實(shí)現(xiàn)的分布式鎖,可以利用 Zookeeper 檢查節(jié)點(diǎn)是否存活,從而實(shí)現(xiàn)續(xù)期,Zookeeper 分布式鎖沒(méi)用過(guò),不詳細(xì)說(shuō)。

不過(guò)這種做法也無(wú)法百分百做到同一時(shí)刻只有一個(gè) Client 獲取到鎖,如果續(xù)期失敗,比如發(fā)生了 Martin Kleppmann 所說(shuō)的 STW 的 GC,或者 Client 和 Redis 集群失聯(lián)了,只要續(xù)期失敗,就會(huì)造成同一時(shí)刻有多個(gè) Client 獲得鎖了。

在我的場(chǎng)景下,我將鎖的粒度拆小了,Redisson 的續(xù)期機(jī)制已經(jīng)夠用了。如果要做得更嚴(yán)格,得加一個(gè)續(xù)期失敗終止任務(wù)的邏輯。

這種做法在以前 Python 的代碼中實(shí)現(xiàn)過(guò),Java 還沒(méi)有碰到這么嚴(yán)格的情況。

這里也提下 Martin Kleppmann 的解決方案,我自己覺(jué)得這個(gè)方案并不靠譜,原因后面會(huì)提到。

他的方案是讓加鎖的資源自己維護(hù)一套保證不會(huì)因加鎖失敗而導(dǎo)致多個(gè) Client 在同一時(shí)刻訪問(wèn)同一個(gè)資源的情況。

 

在客戶端獲取鎖的同時(shí),也獲取到一個(gè)資源的 Token,這個(gè) Token 是單調(diào)遞增的,每次在寫資源時(shí),都檢查當(dāng)前的 Token 是否是較老的 Token,如果是就不讓寫。

對(duì)于上面的場(chǎng)景,Client1 獲取鎖的同時(shí)分配一個(gè) 33 的 Token,Client2 獲取鎖的時(shí)候分配一個(gè) 34 的 Token。

在 Client1 GC 期間,Client2 已經(jīng)寫了資源,這時(shí)最大的 Token 就是 34 了,Client1 從 GC 中回來(lái),再帶著 33 的 Token 寫資源時(shí),會(huì)因?yàn)?Token 過(guò)期被拒絕。

這種做法需要資源那一邊提供一個(gè) Token 生成器。對(duì)于這種 fencing 的方案,我有幾點(diǎn)問(wèn)題:

①無(wú)法保證事務(wù)。示意圖中畫的只有 34 訪問(wèn)了 Storage,但是在實(shí)際場(chǎng)景中,可能出現(xiàn)在一個(gè)任務(wù)內(nèi)多次訪問(wèn) Storage 的情況,而且必須是原子的。

如果 Client1 帶著 33 的 Token 在 GC 前訪問(wèn)過(guò)一次 Storage,然后發(fā)生了 GC。

Client2 獲取到鎖,帶著 34 的 Token 也訪問(wèn)了 Storage,這時(shí)兩個(gè) Client 寫入的數(shù)據(jù)是否還能保證數(shù)據(jù)正確?

如果不能,那么這種方案就有缺陷,除非 Storage 自己有其他機(jī)制可以保證,比如事務(wù)機(jī)制;如果能,那么這里的 Token 就是多余的,fencing 的方案就是多此一舉。

②高并發(fā)場(chǎng)景不實(shí)用。因?yàn)槊看沃挥凶畲蟮?Token 能寫,這樣 Storage 的訪問(wèn)就是線性的,在高并發(fā)場(chǎng)景下,這種方式會(huì)極大的限制吞吐量,而分布式鎖也大多是在這種場(chǎng)景下用的,很矛盾的設(shè)計(jì)。

③這是所有分布式鎖的問(wèn)題。這個(gè)方案是一個(gè)通用的方案,可以和 Redlock 用,也可以和其他的 lock 用。所以我理解僅僅是一個(gè)和 Redlock 無(wú)關(guān)的解決方案。

系統(tǒng)時(shí)鐘漂移

這個(gè)問(wèn)題只是考慮過(guò),但在實(shí)際項(xiàng)目中并沒(méi)有碰到過(guò),因?yàn)槔碚撋鲜强赡艹霈F(xiàn)的,這里也說(shuō)下。

Redis 的過(guò)期時(shí)間是依賴系統(tǒng)時(shí)鐘的,如果時(shí)鐘漂移過(guò)大時(shí)會(huì)影響到過(guò)期時(shí)間的計(jì)算。

為什么系統(tǒng)時(shí)鐘會(huì)存在漂移呢?先簡(jiǎn)單說(shuō)下系統(tǒng)時(shí)間,Linux 提供了兩個(gè)系統(tǒng)時(shí)間:clock realtime 和 clock monotonic。

clock realtime 也就是 xtime/wall time,這個(gè)時(shí)間時(shí)可以被用戶改變的,被 NTP 改變,gettimeofday 拿的就是這個(gè)時(shí)間,Redis 的過(guò)期計(jì)算用的也是這個(gè)時(shí)間。

clock monotonic ,直譯過(guò)來(lái)時(shí)單調(diào)時(shí)間,不會(huì)被用戶改變,但是會(huì)被 NTP 改變。

最理想的情況時(shí),所有系統(tǒng)的時(shí)鐘都時(shí)時(shí)刻刻和NTP服務(wù)器保持同步,但這顯然時(shí)不可能的。

導(dǎo)致系統(tǒng)時(shí)鐘漂移的原因有兩個(gè):

  • 系統(tǒng)的時(shí)鐘和 NTP 服務(wù)器不同步。這個(gè)目前沒(méi)有特別好的解決方案,只能相信運(yùn)維同學(xué)了。
  • clock realtime 被人為修改。在實(shí)現(xiàn)分布式鎖時(shí),不要使用 clock realtime。

不過(guò)很可惜,Redis 使用的就是這個(gè)時(shí)間,我看了下 Redis 5.0 源碼,使用的還是 clock realtime。

Antirez 說(shuō)過(guò)改成 clock monotonic 的,不過(guò)大佬還沒(méi)有改。也就是說(shuō),人為修改 Redis 服務(wù)器的時(shí)間,就能讓 Redis 出問(wèn)題了。

總結(jié)

本文從一個(gè)簡(jiǎn)單的基于 Redis 的分布式鎖出發(fā),到更復(fù)雜的 Redlock 的實(shí)現(xiàn),介紹了在使用分布式鎖的過(guò)程中才踩過(guò)的一些坑以及解決方案。

作者:陳寒立

簡(jiǎn)介:一個(gè)不誤正業(yè)的程序員。先后在物流金融組、物流末端業(yè)務(wù)組和壓力平衡組打過(guò)雜,技術(shù)棧從 Python 玩到了 Java,依然沒(méi)學(xué)會(huì)好好寫業(yè)務(wù)代碼,夢(mèng)想著用抽象的模型拯救業(yè)務(wù)于水火之中。

編輯:陶家龍

出處:餓了么物流技術(shù)團(tuán)隊(duì)

 

責(zé)任編輯:武曉燕 來(lái)源: 餓了么物流技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2021-11-11 07:47:03

Redis分布式

2019-06-19 15:40:06

分布式鎖RedisJava

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

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分布式

2019-07-16 09:22:10

RedisZookeeper分布式鎖

2021-06-16 07:56:21

Redis分布式

2024-04-01 05:10:00

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

2019-12-25 14:35:33

分布式架構(gòu)系統(tǒng)

2023-01-13 07:39:07

2023-04-03 10:00:00

Redis分布式

2021-06-03 00:02:43

RedisRedlock算法

2022-12-18 20:07:55

Redis分布式

2021-07-30 00:09:21

Redlock算法Redis

2022-03-08 07:22:48

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

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