Redis Redisson 分散式鎖的應用和原始碼

KerryWu發表於2021-10-20

1. 前言

之前寫過一篇《Redis分散式鎖的實現》的文章,主要介紹的Redis分散式鎖的原始性實現,核心是基於setnx來加鎖,以及使用lua保障事務的原子性等。但畢竟比較原始,需要根據不同的應用場景做不同的程式碼實現,也容易考慮不周。當時文章中就有提到 Redisson框架,剛好最近工作中又用的比較多,這次就著重介紹。

Redisson 是架設在 Redis基礎上的一個Java開發框架,底層基於 Netty框架,為使用者提供了一系列具有分散式特性的常用工具類。Redisson的功能非常豐富,具體可參考 github中文wiki,但本文只介紹 Redisson分散式鎖的功能。

2. 普通可重入鎖

2.1. 使用示例

在SpringBoot專案通過Redisson來加鎖非常容易,不需要像之前文章中一樣寫一大堆程式碼,框架遮蔽掉了很多細節。如下例:

Config config = new Config();
config.useSingleServer().setAddress("redis://ip:port").setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);

RLock lock = redissonClient.getLock("LOCK_KEY");
long waitTime=500L;
long leaseTime=15000L;
boolean isLock;
try {
    isLock = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
    if (isLock) {
        // do something ...
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    lock.unlock();
}

注意程式碼中 Config 並無限制,示例中是Redis單節點連線,但實際上可以是哨兵模式、叢集模式、主從模式等。

2.2. 原始碼講解

前面例子中加鎖用到了RLock介面,這裡貼一下原始碼:
org.redisson.api.RLock.java

public interface RLock extends Lock, RLockAsync {
    String getName();

    void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;

    boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;

    void lock(long var1, TimeUnit var3);

    boolean forceUnlock();

    boolean isLocked();

    boolean isHeldByThread(long var1);

    boolean isHeldByCurrentThread();

    int getHoldCount();

    long remainTimeToLive();
}

對於可重入鎖,介面對應的實現方法在org.redisson.RedissonLock類裡面,原始碼就不貼了,可以看到落到Redis時,實際的“加鎖”和“解鎖”過程也是一段lua指令碼。

1、加鎖

lua

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]);

引數解釋:

  • KEYS[1]:被鎖資源名。
  • ARGV[1]:過期時間。
  • ARGV[2]:當前程式標識(UUID + 當前threadId)。

加鎖的邏輯,是在redis中存入一個Set型別值。資源一旦被鎖,初次設定Value為1,也只有當前程式可重複加鎖,即Value往上加1。

2、解鎖

lua

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    redis.call('del', KEYS[1]);
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end
return nil;

引數解釋:

  • KEYS[1]:被鎖資源名。
  • KEYS[2]:解鎖時廣播通道名。
  • ARGV[1]:解鎖時廣播通道訊息(值為0L)。
  • ARGV[2]:過期時間。
  • ARGV[3]:當前程式標識(UUID + 當前threadId)。

解鎖的邏輯,是先判斷被鎖資源名是否存在,如果存在則給Value減1,當Value為0時,則刪除Key,並向指定通道廣播訊息。

廣播通道的設計很有亮點,當多個執行緒同時競爭鎖時,未搶到鎖的執行緒無需無效輪詢,只需訂閱一個通道。當鎖釋放時,在通道中廣播訊息,通知那些等待獲取鎖的執行緒現在可以獲得鎖了,那些執行緒再去競爭鎖,避免效能資源的浪費。

3、看門狗機制

如果拿到分散式鎖的節點當機,且這個鎖正好處於鎖住的狀態時,會出現鎖死的狀態,為了避免這種情況的發生,鎖都會設定一個過期時間。即前面在加鎖時傳入的leaseTime。某些應用場景中,如果在指定時間中,我們尚未完成業務,此時就需要給鎖“續期”。如果整個過程完全可控,可以在程式中手動給鎖續期。但如果希望能自動續期,就可以用到Redisson的Wath Dog(看門狗)機制。

Redisson提供了一個監控鎖的看門狗,它的作用是在Redisson例項被關閉前,不斷的延長鎖的有效期,也就是說,如果一個拿到鎖的執行緒一直沒有完成邏輯,那麼看門狗會幫助執行緒不斷的延長鎖超時時間,鎖不會因為超時而被釋放。預設情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。

下面就是加鎖的原始碼,注意,在呼叫加鎖方法時,如果想用看門狗,則傳leaseTime值為-1L。如果給leaseTime設定了有效值,那麼看門狗就不會生效,鎖不會自動續期,而是在你指定的時間後自動解鎖。

    private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }
                }
            });
            return ttlRemainingFuture;
        }
    }

3. RedLock 紅鎖

3.1. 概念說明

也是在之前的那一篇文章中,也提到了RedLock,中文直譯“紅鎖”。其實那篇文章已經介紹過了,這裡再介紹一下。前面用redis實現分散式鎖時存在漏洞,具體場景:

客戶端A在Redis master節點申請鎖。但master在將儲存的key同步到slave上之前崩潰了,然後slave晉升為master。而客戶端B申請一個客戶端A已經持有的資源的鎖。然後呢?然後呢?出問題啦,客戶端A和B都能申請到同一個鎖。

RedLock是Redis官方提出的演算法,具體流程包括:

  1. 獲取當前時間。
  2. 依次N個節點獲取鎖,並設定響應超時時間,防止單節點獲取鎖時間過長。
  3. 鎖有效時間=鎖過期時間-獲取鎖耗費時間,如果第2步驟中獲取成功的節點數大於
    N/2+1,且鎖有效時間大於0,則獲得鎖成功。
  4. 若獲得鎖失敗,則向所有節點釋放鎖。

簡單點說,就是在鎖過期時間內,如果半數以上的節點成功獲取到了鎖,則說明獲取鎖成功。這個有點像ZooKeeper的選舉機制。這裡講講Redisson中的實現方法。

3.2. 使用示例

Redisson關於RedLock的使用程式碼上及其簡單,只是將幾個鎖組合成一個“大鎖”,然後再正常使用“大鎖”的加鎖/解鎖。

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://ip1:port1")
        .setPassword("password1").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://ip2:port2")
        .setPassword("password2").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://ip3:port3")
        .setPassword("password3").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String lockKey = "REDLOCK_KEY";
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
long waitTime=500L;
long leaseTime=15000L;
try {
    isLock = redLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
    if (isLock) {
        // do something ...
    }
} catch (Exception e) {
    ... ...
} finally {
    redLock.unlock();
}

注意程式碼中 Config 並無限制,示例中是Redis單節點連線,但實際上可以是哨兵模式、叢集模式、主從模式等。

3.3. 原始碼講解

在講Redisson的 RedLock(紅鎖)之前,先講 MultiLock(聯鎖),原因先看 RedissonRedLock原始碼,完全是繼承 RedissonMultiLock的所有功能。

RedissonRedLock.java

public class RedissonRedLock extends RedissonMultiLock {
    public RedissonRedLock(RLock... locks) {
        super(locks);
    }

    protected int failedLocksLimit() {
        return this.locks.size() - this.minLocksAmount(this.locks);
    }

    protected int minLocksAmount(List<RLock> locks) {
        return locks.size() / 2 + 1;
    }

    protected long calcLockWaitTime(long remainTime) {
        return Math.max(remainTime / (long)this.locks.size(), 1L);
    }

    public void unlock() {
        this.unlockInner(this.locks);
    }
}

RedissonMultiLock.java核心程式碼

    // 加鎖
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1L;
        if (leaseTime != -1L) {
            if (waitTime == -1L) {
                newLeaseTime = unit.toMillis(leaseTime);
            } else {
                newLeaseTime = unit.toMillis(waitTime) * 2L;
            }
        }

        long time = System.currentTimeMillis();
        long remainTime = -1L;
        if (waitTime != -1L) {
            remainTime = unit.toMillis(waitTime);
        }

        long lockWaitTime = this.calcLockWaitTime(remainTime);
        int failedLocksLimit = this.failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList(this.locks.size());
        ListIterator iterator = this.locks.listIterator();

        while(iterator.hasNext()) {
            RLock lock = (RLock)iterator.next();

            boolean lockAcquired;
            try {
                if (waitTime == -1L && leaseTime == -1L) {
                    lockAcquired = lock.tryLock();
                } else {
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException var21) {
                this.unlockInner(Arrays.asList(lock));
                lockAcquired = false;
            } catch (Exception var22) {
                lockAcquired = false;
            }

            if (lockAcquired) {
                acquiredLocks.add(lock);
            } else {
                if (this.locks.size() - acquiredLocks.size() == this.failedLocksLimit()) {
                    break;
                }

                if (failedLocksLimit == 0) {
                    this.unlockInner(acquiredLocks);
                    if (waitTime == -1L) {
                        return false;
                    }

                    failedLocksLimit = this.failedLocksLimit();
                    acquiredLocks.clear();

                    while(iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    --failedLocksLimit;
                }
            }

            if (remainTime != -1L) {
                remainTime -= System.currentTimeMillis() - time;
                time = System.currentTimeMillis();
                if (remainTime <= 0L) {
                    this.unlockInner(acquiredLocks);
                    return false;
                }
            }
        }

        if (leaseTime != -1L) {
            List<RFuture<Boolean>> futures = new ArrayList(acquiredLocks.size());
            Iterator var24 = acquiredLocks.iterator();

            while(var24.hasNext()) {
                RLock rLock = (RLock)var24.next();
                RFuture<Boolean> future = ((RedissonLock)rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                futures.add(future);
            }

            var24 = futures.iterator();

            while(var24.hasNext()) {
                RFuture<Boolean> rFuture = (RFuture)var24.next();
                rFuture.syncUninterruptibly();
            }
        }

        return true;
    }
    
    // 解鎖
    public void unlock() {
        List<RFuture<Void>> futures = new ArrayList(this.locks.size());
        Iterator var2 = this.locks.iterator();

        while(var2.hasNext()) {
            RLock lock = (RLock)var2.next();
            futures.add(lock.unlockAsync());
        }

        var2 = futures.iterator();

        while(var2.hasNext()) {
            RFuture<Void> future = (RFuture)var2.next();
            future.syncUninterruptibly();
        }

    }

RedissonRedLock.java 中重寫了 RedissonMultiLock.java裡的幾個方法:

  • failedLocksLimit:MultiLock中返回0,RedLock中返回 locks.size() / 2 - 1
  • calcLockWaitTime:MultiLock中返回 remainTime,RedLock中返回 Math.max(remainTime / (long)this.locks.size(), 1L)

通過原始碼容易看到,Redisson中的 RedLock演算法完全是基於 MultiLock實現的。Redisson 支援這種“聯合鎖”的概念,將多個 RLock鎖放入一個 ArrayList中,然後開始遍歷加鎖。只不過 MultiLock的要求比較苛刻,List中的所有的 RLock加鎖時,不能存在任何加鎖失敗的,即 failedLocksLimit=0。而 RedLock要求放鬆一點,只要過半加鎖成功即可,即 failedLocksLimit = locks.size() / 2 - 1。但解鎖時,要求將整個 ArrayList 中的鎖都解一遍。

相關文章