Redisson分佈式鎖實現

概述

分佈式系統有一個著名的理論CAP,指在一個分佈式系統中,最多隻能同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance)這三項中的兩項。所以在設計系統時,往往需要權衡,在CAP中作選擇。當然,這個理論也並不一定完美,不同系統對CAP的要求級別不一樣,選擇需要考慮方方面面。

在微服務系統中,一個請求存在多級跨服務調用,往往需要犧牲強一致性老保證系統高可用,比如通過分佈式事務,異步消息等手段完成。但還是有的場景,需要阻塞所有節點的所有線程,對共享資源的訪問。比如併發時“超賣”和“餘額減爲負數”等情況。

本地鎖可以通過語言本身支持,要實現分佈式鎖,就必須依賴中間件,數據庫、redis、zookeeper等。

分佈式鎖特性

不管使用什麼中間件,有幾點是實現分佈式鎖必須要考慮到的。

  1. 互斥:互斥好像是必須的,否則怎麼叫鎖。
  2. 死鎖: 如果一個線程獲得鎖,然後掛了,並沒有釋放鎖,致使其他節點(線程)永遠無法獲取鎖,這就是死鎖。分佈式鎖必須做到避免死鎖。
  3. 性能: 高併發分佈式系統中,線程互斥等待會成爲性能瓶頸,需要好的中間件和實現來保證性能。
  4. 鎖特性:考慮到複雜的場景,分佈式鎖不能只是加鎖,然後一直等待。最好實現如Java Lock的一些功能如:鎖判斷,超時設置,可重入性等。

Redis實現之Redisson原理

redission實現了JDK中的Lock接口,所以使用方式一樣,只是Redssion的鎖是分佈式的。如下:

RLock lock = redisson.getLock("className"); 
lock.lock(); 
try {
    // do sth.
} finally {
    lock.unlock(); 
}

好,Lock主要實現是RedissionLock。

RedissonLock

先來看常用的Lock方法實現。

@Override
public void lock() {
    try {
        lockInterruptibly();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
@Override
public void lockInterruptibly() throws InterruptedException {
    lockInterruptibly(-1, null);
}

再看lockInterruptibly方法:

@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    // 獲取鎖
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    if (ttl == null) { // 獲取成功
        return;
    }

    // 異步訂閱redis chennel
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    commandExecutor.syncSubscription(future); // 阻塞獲取訂閱結果

    try {
        while (true) {// 循環判斷知道獲取鎖
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            if (ttl >= 0) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                getEntry(threadId).getLatch().acquire();
            }
        }
    } finally {
        unsubscribe(future, threadId);// 取消訂閱
    }
}

總結lockInterruptibly:獲取鎖,不成功則訂閱釋放鎖的消息,獲得消息前阻塞。得到釋放通知後再去循環獲取鎖。

下面重點看看如何獲取鎖:Long ttl = tryAcquire(leaseTime, unit, threadId)

 private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(leaseTime, unit, threadId));// 通過異步獲取鎖,但get(future)實現同步
}private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) { //1 如果設置了超時時間,直接調用 tryLockInnerAsync
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //2 如果leaseTime==-1,則默認超時時間爲30s
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG);
    //3 監聽Future,獲取Future返回值ttlRemaining(剩餘超時時間),獲取鎖成功,但是ttlRemaining,則刷新過期時間
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

已經在註釋中解釋了,需要注意的是,此處用到了Netty的Future-listen模型,可以看看我的另一篇對Future的簡單講解:給Future一個Promise

下面就是最重要的redis獲取鎖的方法tryLockInnerAsync:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    return commandExecutor.evalWriteAsync(
        getName(), 
        LongCodec.INSTANCE, 
        command,
          "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; " +
          "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; " +
          "return redis.call('pttl', KEYS[1]);",
        Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

這個方法主要就是調用redis執行eval lua,爲什麼使用eval,因爲redis對lua腳本執行具有原子性。把這個方法翻譯一下:

-- 1.  沒被鎖{key不存在}
eval "return redis.call('exists', KEYS[1])" 1 myLock
-- (1) 設置Lock爲key,uuid:threadId爲filed, filed值爲1
eval "return redis.call('hset', KEYS[1], ARGV[2], 1)" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (2) 設置key過期時間{防止獲取鎖後線程掛掉導致死鎖}
eval "return redis.call('pexpire', KEYS[1], ARGV[1])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 2. 已經被同線程獲得鎖{key存在並且field存在}
eval "return redis.call('hexists', KEYS[1], ARGV[2])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (1) 可重入,但filed字段+1
eval "return redis.call('hincrby', KEYS[1], ARGV[2],1)" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (2) 刷新過去時間
eval "return redis.call('pexpire', KEYS[1], ARGV[1])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 3. 已經被其他線程鎖住{key存在,但是field不存在}:以毫秒爲單位返回 key 的剩餘超時時間
eval "return redis.call('pttl', KEYS[1])" 1 myLock

這就是核心獲取鎖的方式,下面直接釋放鎖方法unlockInnerAsync

-- 1. key不存在
eval "return redis.call('exists', KEYS[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (1) 發送釋放鎖的消息,返回1,釋放成功
eval "return redis.call('publish', KEYS[2], ARGV[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 2. key存在,但field不存在,說明自己不是鎖持有者,無權釋放,直接return nil
eval "return redis.call('hexists', KEYS[1], ARGV[3])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
eval "return nil"

-- 3. filed存在,說明是本線程在鎖,但有可能其他地方重入鎖,不能直接釋放,應該-1
eval "return redis.call('hincrby', KEYS[1], ARGV[3],-1)" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 4. 如果減1後大於0,說明還有其他重入鎖,刷新過期時間,返回0。
eval "return redis.call('pexpire', KEYS[1], ARGV[2])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 5. 如果不大於0,說明最後一把鎖,需要釋放
-- 刪除key
eval "return redis.call('del', KEYS[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- 發釋放消息
eval "return redis.call('publish', KEYS[2], ARGV[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- 返回1,釋放成功

從釋放鎖代碼中看到,刪除key後會發送消息,所以上文提到獲取鎖失敗後,阻塞訂閱此消息。

另外,上文提到刷新過期時間方法scheduleExpirationRenewal,指線程獲取鎖後需要不斷刷新失效時間,避免未執行完鎖就失效。這個方法的實現原理也類似,只是使用了Netty的TimerTask,每到過期時間1/3就去重新刷一次,如果key不存在則停止刷新。Timer實現大概如下:

private static void nettyTimer() {
    final int expireTime = 6;
    EventExecutorGroup group = new DefaultEventExecutorGroup(1);
    final Timer timer = new HashedWheelTimer();
    timer.newTimeout(timerTask -> {
        Future<Boolean> future = group.submit(() -> {
            System.out.println("刷新key的失效時間爲"+expireTime +"秒");
            return false;// 但key不存在時,返回true
        });
        future.addListener(future1 -> {
            if (!future.getNow()) {
                nettyTimer();
            }
        });
    }, expireTime/3, TimeUnit.SECONDS);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章