分佈式鎖-redLock And Redisson

CAP

在引出RedLock之前,先介紹一下分佈式系統中CAP理論:

C(Consistency):一致性,在同一時間點,所有節點的數據都是完全一致的。

A(Availability):可用性,應該能夠在正常時間內對請求進行響應。

P(Partition-tolerance):分區容忍性,在分佈式環境中,多個節點組成的網絡應該是互相連通的,當由於網絡故障等原因造成網絡分區,要求仍然能夠對外提供服務。

CAP理論告訴我們,任何分佈式系統只能滿足三個中的兩個,不可能全部都滿足。

參考分佈式系統那點事

redis分佈式鎖存在的問題

網上對於採用redis實現分佈式鎖有很多種方案,比較完善的方案應該是用setNx + lua進行實現。簡單實現如下:

- java代碼-加鎖,相當於setnx lock_key_name unique_value
set lock_key_name unique_value NX PX 5000;
- lua腳本-解鎖,原子性操作
if redis.call("get", KEYS[1] == ARGV[1]) then
	return redis.call("del", KEYS[1])
else
    return 0
end

注意:

  1. value需要具有唯一性,可以採用時間戳、uuid或者自增id實現;
  2. 客戶端在解鎖時,需要比較本地內存中的value和redis中的value是否一致,防止誤解鎖;(case:clientA獲取鎖lock1,由於clientA執行的時間比較久,導致key=lock1已經過期,redis實例會移除該key;clientB獲取相同的鎖lock1,clientB正在佔有鎖並執行業務,此時clientA業務已經執行完畢,準備釋放鎖;如果沒有比較value的邏輯,那麼clientA會把clientB持有的鎖釋放掉,這個顯然不行的,由於value值不同,那麼clientA釋放鎖的時候只會釋放自己加的鎖,不會誤釋放別的客戶端加的鎖)

在分佈式系統中,爲了避免單點故障,提高可靠性,redis都會採用主從架構,當主節點掛了後,從節點會作爲主繼續提供服務。該種方案能夠滿足大多數的業務場景,但是對於要求強一致性的場景如交易,該種方案還是有漏洞的,原因如下:

redis主從架構採用的是異步複製,當master節點拿到了鎖,但是鎖還未同步到slave節點,此時master節點掛了,發生故障轉移,slave節點被選舉爲master節點,丟失了鎖。這樣其他線程就能夠獲取到該鎖,顯然是有問題的。

因此,上述基於redis實現的分佈式鎖只是滿足了AP,並沒有滿足C。

RedLock

正是因爲上述redis分佈式鎖存在的一致性問題,redis作者提出了一個更加高級的基於redis實現的分佈式鎖——RedLock。原文可參考 Distributed locks with Redis

RedLock是什麼?

RedLock是基於redis實現的分佈式鎖,它能夠保證以下特性:

  1. 互斥性:在任何時候,只能有一個客戶端能夠持有鎖;
  2. 避免死鎖:當客戶端拿到鎖後,即使發生了網絡分區或者客戶端宕機,也不會發生死鎖;(利用key的存活時間)
  3. 容錯性:只要多數節點的redis實例正常運行,就能夠對外提供服務,加鎖或者釋放鎖;

而非redLock是無法滿足互斥性的,上面已經闡述過了原因。

RedLock算法

假設有N個redis的master節點,這些節點是相互獨立的(不需要主從或者其他協調的系統)。N推薦爲奇數~

客戶端在獲取鎖時,需要做以下操作:

  1. 獲取當前時間戳,以微妙爲單爲。
  2. 使用相同的lockName和lockValue,嘗試從N個節點獲取鎖。(在獲取鎖時,要求等待獲取鎖的時間遠小於鎖的釋放時間,如鎖的lease_time爲10s,那麼wait_time應該爲5-50毫秒;避免因爲redis實例掛掉,客戶端需要等待更長的時間才能返回,即需要讓客戶端能夠fast_fail;如果一個redis實例不可用,那麼需要繼續從下個redis實例獲取鎖)
  3. 當從N個節點獲取鎖結束後,如果客戶端能夠從多數節點(N/2 + 1)中成功獲取鎖,且獲取鎖的時間小於失效時間,那麼可認爲,客戶端成功獲得了鎖。(獲取鎖的時間=當前時間戳 - 步驟1的時間戳)
  4. 客戶端成功獲得鎖後,那麼鎖的實際有效時間 = 設置鎖的有效時間 - 獲取鎖的時間。
  5. 客戶端獲取鎖失敗後,N個節點的redis實例都會釋放鎖,即使未能加鎖成功。

爲什麼N推薦爲奇數呢?
原因1:本着最大容錯的情況下,佔用服務資源最少的原則,2N+1和2N+2的容災能力是一樣的,所以採用2N+1;比如,5臺服務器允許2臺宕機,容錯性爲2,6臺服務器也只能允許2臺宕機,容錯性也是2,因爲要求超過半數節點存活才OK。

原因2:假設有6個redis節點,client1和client2同時向redis實例獲取同一個鎖資源,那麼可能發生的結果是——client1獲得了3把鎖,client2獲得了3把鎖,由於都沒有超過半數,那麼client1和client2獲取鎖都失敗,對於奇數節點是不會存在這個問題。

失敗時重試

當客戶端無法獲取到鎖時,應該隨機延時後進行重試,防止多個客戶端在同一時間搶奪同一資源的鎖(會導致腦裂,最終都不能獲取到鎖)。客戶端獲得超過半數節點的鎖花費的時間越短,那麼腦裂的概率就越低。所以,理想的情況下,客戶端最好能夠同時(併發)向所有redis發出set命令。

當客戶端從多數節點獲取鎖失敗時,應該儘快釋放已經成功獲取的鎖,這樣其他客戶端不需要等待鎖過期後再獲取。(如果存在網絡分區,客戶端已經無法和redis進行通信,那麼此時只能等待鎖過期後自動釋放)

不明白爲什麼會發生腦裂???

釋放鎖

向所有redis實例發送釋放鎖命令即可,不需要關心redis實例有沒有成功上鎖。

redisson在加鎖的時候,key=lockName, value=uuid + threadID, 採用set結構存儲,幷包含了上鎖的次數 (支持可重入);解鎖的時候通過hexists判斷key和value是否存在,存在則解鎖;這裏不會出現誤解鎖

性能、 崩潰恢復和redis同步

如何提升分佈式鎖的性能?以每分鐘執行多少次acquire/release操作作爲性能指標,一方面通過增加redis實例可用降低響應延遲,另一方面,使用非阻塞模型,一次發送所有的命令,然後異步讀取響應結果,這裏假設客戶端和redis之間的RTT差不多。

如果redis沒用使用備份,redis重啓後,那麼會丟失鎖,導致多個客戶端都能獲取到鎖。通過AOF持久化可以緩解這個問題。redis key過期是unix時間戳,即便是redis重啓,那麼時間依然是前進的。但是,如果是斷電呢?redis在啓動後,可能就會丟失這個key(在寫入或者還未寫入磁盤時斷電了,取決於fsync的配置),如果採用fsync=always,那麼會極大影響性能。如何解決這個問題呢?可以讓redis節點重啓後,在一個TTL時間段內,對客戶端不可用即可。

針對redlock的爭議

後續會對該部分內容進行更新。
[參考鏈接](http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Redisson

redisson是在redis基礎上實現的一套開源解決方案,不僅提供了一系列的分佈式的java常用對象,還提供了許多分佈式服務,宗旨是促進使用者對redis的關注分離,更多的關注業務邏輯的處理上。

redisson也對redlock做了一套實現,詳細如下:

使用案例

public static void main() {

        Config config1 = new Config();
        config1.useSingleServer().setAddress("redis://xxxx1:xxx1")
                .setPassword("xxxx1")
                .setDatabase(0);
        RedissonClient redissonClient1 = Redisson.create(config1);
        Config config2 = new Config();
        config2.useSingleServer()
                .setAddress("redis://xxxx2:xxx2")
                .setPassword("xxxx2")
                .setDatabase(0);

        RedissonClient redissonClient2 = Redisson.create(config2);

        Config config3 = new Config();
        config3.useSingleServer().
                setAddress("redis://xxxx3:xxx3")
                .setPassword("xxxx3")
                .setDatabase(0);

        RedissonClient redissonClient3 = Redisson.create(config3);

        String lockName = "redlock-test";
        RLock lock1 = redissonClient1.getLock(lockName);
        RLock lock2 = redissonClient2.getLock(lockName);
        RLock lock3 = redissonClient3.getLock(lockName);

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLock;
        try {
            isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
            System.out.println("isLock = " + isLock);
            if (isLock) {
                // lock success, do something;
                Thread.sleep(30000);
            }
        } catch (Exception e) {

        } finally {
            // 無論如何, 最後都要解鎖
            redLock.unlock();
            System.out.println("unlock success");
        }
    }

源碼

tryLock():redisson對redlock的實現方式基本和上述描述的類似,有一點區別在於,redisson在獲取鎖成功後,會對key的失效時間重新。

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1;
        if (leaseTime != -1) {
            newLeaseTime = unit.toMillis(waitTime)*2;
        }
        
        long time = System.currentTimeMillis();
        long remainTime = -1;
        if (waitTime != -1) {
            remainTime = unit.toMillis(waitTime);
        }
        long lockWaitTime = calcLockWaitTime(remainTime);
        
        int failedLocksLimit = failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
        for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
            RLock lock = iterator.next();
            boolean lockAcquired;
            try {
                if (waitTime == -1 && leaseTime == -1) {
                    lockAcquired = lock.tryLock();
                } else {
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException e) {
                unlockInner(Arrays.asList(lock));
                lockAcquired = false;
            } catch (Exception e) {
                lockAcquired = false;
            }
            
            if (lockAcquired) {
                acquiredLocks.add(lock);
            } else {
                if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                    break;
                }

                if (failedLocksLimit == 0) {
                    unlockInner(acquiredLocks);
                    if (waitTime == -1 && leaseTime == -1) {
                        return false;
                    }
                    failedLocksLimit = failedLocksLimit();
                    acquiredLocks.clear();
                    // reset iterator
                    while (iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    failedLocksLimit--;
                }
            }
            
            if (remainTime != -1) {
                remainTime -= (System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                if (remainTime <= 0) {
                    unlockInner(acquiredLocks);
                    return false;
                }
            }
        }

        if (leaseTime != -1) {
            List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
            for (RLock rLock : acquiredLocks) {
                RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                futures.add(future);
            }
            
            for (RFuture<Boolean> rFuture : futures) {
                rFuture.syncUninterruptibly();
            }
        }
        
        return true;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章