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

普通實現

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

- 獲取鎖(unique_value能夠是UUID等)
SET resource_name unique_value NX PX 30000

- 釋放鎖(lua腳本中,必定要比較value,防止誤解鎖)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

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

  1. set命令要用set key value px milliseconds nx
  2. value要具備惟一性;
  3. 釋放鎖時要驗證value值,不能誤解鎖;

事實上這類瑣最大的缺點就是它加鎖時只做用在一個Redis節點上,即便Redis經過sentinel保證高可用,若是這個master節點因爲某些緣由發生了主從切換,那麼就會出現鎖丟失的狀況:redis

  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實例)。dom

  • POM依賴
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.3.2</version>
</dependency>

用法

首先,咱們來看一下redission封裝的redlock算法實現的分佈式鎖用法,很是簡單,跟重入鎖(ReentrantLock)有點相似:機器學習

Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
        .setMasterName("masterName")
        .setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 還能夠getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
    isLock = redLock.tryLock();
    // 500ms拿不到鎖, 就認爲獲取鎖失敗。10000ms即10s是鎖失效時間。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    // 不管如何, 最後都要解鎖
    redLock.unlock();
}

惟一ID

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

protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
    return id + ":" + threadId;
}

獲取鎖

獲取鎖的代碼爲redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),二者的最終核心源碼都是下面這段代碼,只不過前者獲取鎖的默認租約時間(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 獲取鎖時向5個redis實例發送的命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              // 首先分佈式鎖的KEY不能存在,若是確實不存在,那麼執行hset命令(hset REDLOCK_KEY uuid+threadId 1),並經過pexpire設置失效時間(也是鎖的租約時間)
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 若是分佈式鎖的KEY已經存在,而且value也匹配,表示是當前線程持有的鎖,那麼重入次數加1,而且設置失效時間
              "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]); " +
                  "return nil; " +
              "end; " +
              // 獲取分佈式鎖的KEY的失效時間毫秒數
              "return redis.call('pttl', KEYS[1]);",
              // 這三個參數分別對應KEYS[1],ARGV[1]和ARGV[2]
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

獲取鎖的命令中,

  • KEYS[1]就是Collections.singletonList(getName()),表示分佈式鎖的key,即REDLOCK_KEY;
  • ARGV[1]就是internalLockLeaseTime,即鎖的租約時間,默認30s;
  • ARGV[2]就是getLockName(threadId),是獲取鎖時set的惟一值,即UUID+threadId:

釋放鎖

釋放鎖的代碼爲redLock.unlock(),核心源碼以下:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    // 向5個redis實例都執行以下命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 若是分佈式鎖KEY不存在,那麼向channel發佈一條消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // 若是分佈式鎖存在,可是value不匹配,表示鎖已經被佔用,那麼直接返回
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 若是就是當前線程佔有分佈式鎖,那麼將重入次數減1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            // 重入次數減1後的值若是大於0,表示分佈式鎖有重入過,那麼只設置失效時間,還不能刪除
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                // 重入次數減1後的值若是爲0,表示分佈式鎖只獲取過1次,那麼刪除這個KEY,併發布解鎖消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            // 這5個參數分別對應KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

 

做者: 阿飛的博客

免費Java資料領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo/Kafka、Hadoop、Hbase、Flink等高併發分佈式、大數據、機器學習等技術。
傳送門: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
相關文章
相關標籤/搜索