天天看點

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技術驿站

”,了解相關資訊可以關注“

”。

繼續閱讀