【分布式緩存系列】集群環境下Redis分布式鎖的正確姿勢


一、前言

  在上一篇文章中,已經介紹了基於Redis實現分布式鎖的正確姿勢,但是上篇文章存在一定的缺陷——它加鎖只作用在一個Redis節點上,如果通過sentinel保證高可用,如果master節點由於某些原因發生了主從切換,那么就會出現鎖丟失的情況:

  1.  客戶端1在Redis的master節點上拿到了鎖
  2. Master宕機了,存儲鎖的key還沒有來得及同步到Slave上
  3. master故障,發生故障轉移,slave節點升級為master節點
  4. 客戶端2從新的Master獲取到了對應同一個資源的鎖

  於是,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破了。針對這個問題。Redis作者antirez提出了RedLock算法來解決這個問題

二、RedLock算法的實現思路

  antirez提出的redlock算法實現思路大概是這樣的。

  客戶端按照下面的步驟來獲取鎖:

  1. 獲取當前時間的毫秒數T1。
  2. 按順序依次向N個Redis節點執行獲取鎖的操作。這個獲取鎖的操作和上一篇中基於單Redis節點獲取鎖的過程相同。包括唯一UUID作為Value以及鎖的過期時間(expireTime)。為了保證在某個在某個Redis節點不可用的時候算法能夠繼續運行,這個獲取鎖的操作還需要一個超時時間。它應該遠小於鎖的過期時間。客戶端向某個Redis節點獲取鎖失敗后,應立即嘗試下一個Redis節點。這里失敗包括Redis節點不可用或者該Redis節點上的鎖已經被其他客戶端持有。
  3. 計算整個獲取鎖過程的總耗時。即當前時間減去第一步記錄的時間。計算公司為T2=now()- T1。如果客戶端從大多數Redis節點(>N/2 +1)成功獲取到鎖。並且獲取鎖總共消耗的時間小於鎖的過期時間(即T2<expireTime)。則認為客戶端獲取鎖成功,否則,認為獲取鎖失敗
  4. 如果獲取鎖成功,需要重新計算鎖的過期時間。它等於最初鎖的有效時間減去第三步計算出來獲取鎖消耗的時間,即expireTime - T2
  5. 如果最終獲取鎖失敗,那么客戶端立即向所有Redis系欸但發起釋放鎖的操作。(和上一篇釋放鎖的邏輯一樣)

  雖然說RedLock算法可以解決單點Redis分布式鎖的安全性問題,但如果集群中有節點發生崩潰重啟,還是會鎖的安全性有影響的。具體出現問題的場景如下:

  假設一共有5個Redis節點:A, B, C, D, E。設想發生了如下的事件序列:

  1. 客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)
  2. 節點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了
  3. 節點C重啟后,客戶端2鎖住了C, D, E,獲取鎖成功

  這樣,客戶端1和客戶端2同時獲得了鎖(針對同一資源)。針對這樣場景,解決方式也很簡單,也就是讓Redis崩潰后延遲重啟,並且這個延遲時間大於鎖的過期時間就好。這樣等節點重啟后,所有節點上的鎖都已經失效了。也不存在以上出現2個客戶端獲取同一個資源的情況了。 

  相比之下,RedLock安全性和穩定性都比前一篇文章中介紹的實現要好很多,但要說完全沒有問題不是。例如,如果客戶端獲取鎖成功后,如果訪問共享資源操作執行時間過長,導致鎖過期了,后續客戶端獲取鎖成功了,這樣在同一個時刻又出現了2個客戶端獲得了鎖的情況。所以針對分布式鎖的應用的時候需要多測試。服務器台數越多,出現不可預期的情況也越多。如果客戶端獲取鎖之后,在上面第三步發生了GC得情況導致GC完成后,鎖失效了,這樣同時也使得同一時間有2個客戶端獲得了鎖。如果系統對共享資源有非常嚴格要求得情況下,還是建議需要做數據庫鎖得得方案來補充。如飛機票或火車票座位得情況。對於一些搶購獲取,針對偶爾出現超賣,后續可以人為溝通置換得方式采用分布式鎖得方式沒什么問題。因為可以絕大部分保證分布式鎖的安全性。

三、分布式場景下基於Redis實現分布式鎖的正確姿勢

  目前redisson包已經有對redlock算法封裝,接下來就具體看看使用redisson包來實現分布式鎖的正確姿勢。

  具體實現代碼如下代碼所示:

  

public interface DistributedLock {
    /**
     * 獲取鎖
     * @author zhi.li
     * @return 鎖標識
     */
    String acquire();

    /**
     * 釋放鎖
     * @author zhi.li
     * @param indentifier
     * @return
     */
    boolean release(String indentifier);
}

public class RedisDistributedRedLock implements DistributedLock {

    /**
     * redis 客戶端
     */
    private RedissonClient redissonClient;

    /**
     * 分布式鎖的鍵值
     */
    private String lockKey;

    private RLock redLock;

    /**
     * 鎖的有效時間 10s
     */
    int expireTime = 10 * 1000;

    /**
     * 獲取鎖的超時時間
     */
    int acquireTimeout  = 500;

    public RedisDistributedRedLock(RedissonClient redissonClient, String lockKey) {
        this.redissonClient = redissonClient;
        this.lockKey = lockKey;
    }

    @Override
    public String acquire() {
        redLock = redissonClient.getLock(lockKey);
        boolean isLock;
        try{
            isLock = redLock.tryLock(acquireTimeout, expireTime, TimeUnit.MILLISECONDS);
            if(isLock){
                System.out.println(Thread.currentThread().getName() + " " + lockKey + "獲得了鎖");
                return null;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean release(String indentifier) {
        if(null != redLock){
            redLock.unlock();
            return true;
        }

        return false;
    }
}

  由於RedLock是針對主從和集群場景准備。上面代碼采用哨兵模式。所以要讓上面代碼運行起來,需要先本地搭建Redis哨兵模式。本人的環境是Windows,具體Windows 哨兵環境搭建參考文章:redis sentinel部署(Windows下實現)

  具體測試代碼如下所示:

  

public class RedisDistributedRedLockTest {
    static int n = 5;
    public static void secskill() {
        if(n <= 0) {
            System.out.println("搶購完成");
            return;
        }

        System.out.println(--n);
    }
    public static void main(String[] args) {

        Config config = new Config();
        //支持單機,主從,哨兵,集群等模式
        //此為哨兵模式
        config.useSentinelServers()
                .setMasterName("mymaster")
                .addSentinelAddress("127.0.0.1:26369","127.0.0.1:26379","127.0.0.1:26389")
                .setDatabase(0);
        Runnable runnable = () -> {
            RedisDistributedRedLock redisDistributedRedLock = null;
            RedissonClient redissonClient = null;
            try {
                redissonClient = Redisson.create(config);
                redisDistributedRedLock = new RedisDistributedRedLock(redissonClient, "stock_lock");
                redisDistributedRedLock.acquire();
                secskill();
                System.out.println(Thread.currentThread().getName() + "正在運行");
            } finally {
                if (redisDistributedRedLock != null) {
                    redisDistributedRedLock.release(null);
                }

                redissonClient.shutdown();
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }

  具體的運行結果,如下圖所示:

四、總結

  到此,基於Redis實現分布式鎖的就告一段落了,由於分布式鎖的實現方式主要有:數據庫鎖的方式、基於Redis實現和基於Zookeeper實現。接下來的一篇文章將介紹基於Zookeeper分布式鎖的正確姿勢。

  本文所有代碼地址:https://github.com/learninghard-lizhi/common-util 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM