1. 程式人生 > >Redlock:Redis分散式鎖最牛逼的實現

Redlock:Redis分散式鎖最牛逼的實現

普通實現

說道Redis分散式鎖大部分人都會想到: setnx+lua,或者知道 setkey value px milliseconds nx。後一種方式的核心實現命令如下:

 
  1. - 獲取鎖(unique_value可以是UUID等)

  2. SET resource_name unique_value NX PX 30000


  3. - 釋放鎖(lua指令碼中,一定要比較value,防止誤解鎖)

  4. if redis.call("get",KEYS[1]) == ARGV[1] then

  5. return redis.call("del",KEYS[1])

  6. else

  7. return 0

  8. end

這種實現方式有3大要點(也是面試概率非常高的地方):

  1. set命令要用 setkey value px milliseconds nx

  2. value要具有唯一性;

  3. 釋放鎖時要驗證value值,不能誤解鎖;

事實上這類瑣最大的缺點就是它加鎖時只作用在一個Redis節點上,即使Redis通過sentinel保證高可用,如果這個master節點由於某些原因發生了主從切換,那麼就會出現鎖丟失的情況:

  1. 在Redis的master節點上拿到了鎖;

  2. 但是這個加鎖的key還沒有同步到slave節點;

  3. master故障,發生故障轉移,slave節點升級為master節點;

  4. 導致鎖丟失。

正因為如此,Redis作者antirez基於分散式環境下提出了一種更高階的分散式鎖的實現方式:Redlock。筆者認為,Redlock也是Redis所有分散式鎖實現方式中唯一能讓面試官高潮的方式。

Redlock實現

antirez提出的redlock演算法大概是這樣的:

在Redis的分散式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從複製或者其他叢集協調機制。我們確保將在N個例項上使用與在Redis單例項下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺伺服器上面執行這些Redis例項,這樣保證他們不會同時都宕掉。

為了取到鎖,客戶端應該執行以下操作:

  • 獲取當前Unix時間,以毫秒為單位。

  • 依次嘗試從5個例項,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設定一個網路連線和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免伺服器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果伺服器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個Redis例項請求獲取鎖。

  • 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,這裡是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功

  • 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。

  • 如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis例項取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis例項上進行解鎖(即便某些Redis例項根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。

Redlock原始碼

redisson已經有對redlock演算法封裝,接下來對其用法進行簡單介紹,並對核心原始碼進行分析(假設5個redis例項)。

POM依賴

 
  1. <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->

  2. <dependency>

  3. <groupId>org.redisson</groupId>

  4. <artifactId>redisson</artifactId>

  5. <version>3.3.2</version>

  6. </dependency>

用法

首先,我們來看一下redission封裝的redlock演算法實現的分散式鎖用法,非常簡單,跟重入鎖(ReentrantLock)有點類似:

 
  1. Config config = new Config();

  2. config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")

  3. .setMasterName("masterName")

  4. .setPassword("password").setDatabase(0);

  5. RedissonClient redissonClient = Redisson.create(config);

  6. // 還可以getFairLock(), getReadWriteLock()

  7. RLock redLock = redissonClient.getLock("REDLOCK_KEY");

  8. boolean isLock;

  9. try {

  10. isLock = redLock.tryLock();

  11. // 500ms拿不到鎖, 就認為獲取鎖失敗。10000ms即10s是鎖失效時間。

  12. isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);

  13. if (isLock) {

  14. //TODO if get lock success, do something;

  15. }

  16. } catch (Exception e) {

  17. } finally {

  18. // 無論如何, 最後都要解鎖

  19. redLock.unlock();

  20. }

唯一ID

實現分散式鎖的一個非常重要的點就是set的value要具有唯一性,redisson的value是怎樣保證value的唯一性呢?答案是UUID+threadId。入口在redissonClient.getLock("REDLOCK_KEY"),原始碼在Redisson.java和RedissonLock.java中:

 
  1. protected final UUID id = UUID.randomUUID();

  2. String getLockName(long threadId) {

  3. return id + ":" + threadId;

  4. }

獲取鎖

獲取鎖的程式碼為redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),兩者的最終核心原始碼都是下面這段程式碼,只不過前者獲取鎖的預設租約時間(leaseTime)是LOCKEXPIRATIONINTERVAL_SECONDS,即30s:

 
  1. <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {

  2. internalLockLeaseTime = unit.toMillis(leaseTime);

  3. // 獲取鎖時向5個redis例項傳送的命令

  4. return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,

  5. // 首先分散式鎖的KEY不能存在,如果確實不存在,那麼執行hset命令(hset REDLOCK_KEY uuid+threadId 1),並通過pexpire設定失效時間(也是鎖的租約時間)

  6. "if (redis.call('exists', KEYS[1]) == 0) then " +

  7. "redis.call('hset', KEYS[1], ARGV[2], 1); " +

  8. "redis.call('pexpire', KEYS[1], ARGV[1]); " +

  9. "return nil; " +

  10. "end; " +

  11. // 如果分散式鎖的KEY已經存在,並且value也匹配,表示是當前執行緒持有的鎖,那麼重入次數加1,並且設定失效時間

  12. "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +

  13. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +

  14. "redis.call('pexpire', KEYS[1], ARGV[1]); " +

  15. "return nil; " +

  16. "end; " +

  17. // 獲取分散式鎖的KEY的失效時間毫秒數

  18. "return redis.call('pttl', KEYS[1]);",

  19. // 這三個引數分別對應KEYS[1],ARGV[1]和ARGV[2]

  20. Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

  21. }

獲取鎖的命令中,

  • KEYS[1] 就是Collections.singletonList(getName()),表示分散式鎖的key,即REDLOCK_KEY;

  • ARGV[1] 就是internalLockLeaseTime,即鎖的租約時間,預設30s;

  • ARGV[2] 就是getLockName(threadId),是獲取鎖時set的唯一值,即UUID+threadId:


釋放鎖

釋放鎖的程式碼為redLock.unlock(),核心原始碼如下:

 
  1. protected RFuture<Boolean> unlockInnerAsync(long threadId) {

  2. // 向5個redis例項都執行如下命令

  3. return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

  4. // 如果分散式鎖KEY不存在,那麼向channel釋出一條訊息

  5. "if (redis.call('exists', KEYS[1]) == 0) then " +

  6. "redis.call('publish', KEYS[2], ARGV[1]); " +

  7. "return 1; " +

  8. "end;" +

  9. // 如果分散式鎖存在,但是value不匹配,表示鎖已經被佔用,那麼直接返回

  10. "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +

  11. "return nil;" +

  12. "end; " +

  13. // 如果就是當前執行緒佔有分散式鎖,那麼將重入次數減1

  14. "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +

  15. // 重入次數減1後的值如果大於0,表示分散式鎖有重入過,那麼只設置失效時間,還不能刪除

  16. "if (counter > 0) then " +

  17. "redis.call('pexpire', KEYS[1], ARGV[2]); " +

  18. "return 0; " +

  19. "else " +

  20. // 重入次數減1後的值如果為0,表示分散式鎖只獲取過1次,那麼刪除這個KEY,併發布解鎖訊息

  21. "redis.call('del', KEYS[1]); " +

  22. "redis.call('publish', KEYS[2], ARGV[1]); " +

  23. "return 1; "+

  24. "end; " +

  25. "return nil;",

  26. // 這5個引數分別對應KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]

  27. Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));


  28. }

參考:https://redis.io/topics/distlock


原文釋出時間為: 2018-12-02
本文作者:阿飛的部落格
本文來自雲棲社群合作伙伴“Java技術驛站”,瞭解相關資訊可以關注“Java技術驛站”。