天天看點

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

筆耕墨耘,深研術道。

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

01寫在前面 Redis是一個高性能的記憶體資料庫,常用于資料庫、緩存和消息中間件。它提供了豐富的資料結構,更适合各種業務場景;基于AP模型,Redis保證了其高可用和高性能。

本文主要内容:

  • Redis實作分布式鎖的依據
  • 基于setnx + expire的不斷探索
  • 分布式利器—Redisson

  • Redlock算法及其争議

02Redis實作分布式鎖的依據

Redis分布式鎖的實作的核心依據:redis的單線程模型,保證最終隻有一個setnx指令可以執行成功;

如何了解Redis的單線程模型(選讀)?

Redis基于Reactor模式開發了自己網絡事件處理器:這個處理器被稱為檔案事件處理器(file event handler):

  • 檔案事件處理器使用I/O多路複用(multiplexing)程式來同時監聽多個套接字,并根據套接字目前執行的任務來為套接字關聯不同的事件處理器;
  • 當被監聽的套接字準備好執行連接配接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案事件就會産生,這時檔案事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。
注:檔案事件處理器是統稱,是網絡事件處理的子產品(或者說方案),其包括四個組成部分:套接字、I/O多路複用程式、檔案事件分派器(dispatcher),以及事件處理器。

檔案事件處理器四個組成部分如下:

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

檔案事件處理器的四個組成部分

  • I/O多路複用程式負責監聽多個套接字,對于産生了事件的套接字,将它們放入一個隊列裡面,然後通過這個隊列,以有序、同步、每次一個套接字的方式向檔案事件分派器傳送套接字(單線程模型);
  • 當上一個套接字産生的事件被處理完畢之後,I/O多路複用程式才會繼續向檔案事件分派器傳送下一個套接字;
  • 檔案事件分派器接受I/O多路複用程式傳來的套接字,并根據套接字産生的事件類型,調用相應的事件處理器;
  • 這些事件處理器是一個個函數,它們會執行相應的操作。

總結:雖然檔案事件處理器以單線程方式運作,但通過使用I/O多路複用程式來監聽多個套接字,檔案事件處理器既實作了高性能的網絡通信模型,又可以很好地與Redis伺服器中其他同樣以單線程方式運作的子產品進行對接,這保持了Redis内部單線程設計的簡單性。03基于setnx + expire的不斷探索 為了代碼的複用和可維護性,這裡提供一個分布式鎖的接口,各個版本的方案均實作了此接口。

基本實作代碼:這裡給出了阻塞和非阻塞兩種方式。

public interface DistributedLock {    /**     * 阻塞     *     * @param lockKey     * @param timeout     */    void lock(String lockKey, long timeout);    /**     * 非阻塞     *     * @param lockKey     * @param timeout     * @return     */    boolean tryLock(String lockKey, long timeout);    /**     * 釋放鎖     *     * @param lockKey     */    void unlock(String lockKey);}
           

利用stringRedisTemplate實作,以下代碼并不嚴謹,僅僅是示範某些問題。

@Override    public void lock(String lockKey, long timeout) {        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey);        while (true) {            Boolean b = boundValueOps.setIfAbsent("1");            if (b) {                Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS);                if (expire) {                    return;                }            }            // 休眠1s後再重試            LockSupport.parkNanos(1*1000*1000);        }    }    @Override    public boolean tryLock(String lockKey, long timeout) {        BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey);        Boolean b = boundValueOps.setIfAbsent("1");        if (b) {            Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS);            if (expire) {                return true;            }        }        return false;    }    @Override    public void unlock(String lockKey) {        stringRedisTemplate.delete(lockKey);    }
           

上述代碼,是一種簡單的分布式鎖的實作,但是問題也較明顯:

  • 非原子性:setnx + expire兩條指令非原子操作,有可能導緻死鎖;
  • 鎖誤解除:B線程加的鎖,可能被A線程釋放掉,導緻鎖失效;
  • 逾時并發:業務執行時間超過鎖逾時時間,導緻鎖失效,産生并發安全問題;
  • 不可重入:不支援可重入;
  • 叢集問題:Redis哨兵模式和叢集模式帶來的問題,主從發生failover時候帶來的鎖失效。

針對以上問題,提供以下解決方案:

Q1 非原子性

  • 方案一:在高版本redis中(Redis 2.6.12以後),官方完善了setnx:SET key value [EX seconds] [PX milliseconds] [NX|XX];
  • 方案二:使用LUA腳本,如下:
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)then return 0;end;redis.call('expire', KEYS[1], tonumber(ARGV[2]));return 1;
           

Q2 & Q3  鎖誤解除和鎖逾時并發

如下圖:

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

當鎖逾時釋放後,會有其他線程争搶鎖。此時,會發生線程A釋放了線程B占用鎖的情況,并且會導緻鎖失效産生并發問題(圖中棕色所示)。 針對鎖誤解除的問題,可以:

  • 加鎖時候,設定value值,這個value值來标記目前線程,嚴格來說,該值在所有用戶端和所有鎖定請求中必須唯一。
This value must be unique across all clients and all lock requests.
  • 在解鎖的時候判斷解鎖線程是否是占有鎖的線程,出于安全性(原子性)考慮,這段解鎖邏輯使用LUA腳本編寫。如下:
Basically the random value is used in order to release the lock in a safe way, with a script that tells Redis: remove the key only if it exists and the value stored at the key is exactly the one I expect to be.
if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end
           

針對鎖逾時帶來的并發問題,可以:

為擷取鎖的線程(圖中線程A)設定一個守護線程,守護線程周期性地給目前鎖續期,當線程A執行完成任務,會顯示關閉守護線程;即使線程A挂掉,由于線程A和守護線程在同一個程序,守護線程也會停下。這把鎖到了逾時的時候,沒人給它續命,也就自動釋放了。如下圖:

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

補充:Redisson的鎖續期實作,基于netty的時間輪算法。

Q4 不可重入

參考Redisson利用hash資料結構實作對線程的重入計數。這個問題将在Redisson分布式鎖源碼分析裡面講述。

Q5 叢集問題

我們知道Redis的主從複制是異步的,主從發生failover時将帶來鎖失效問題。

What happens if the Redis master goes down? Well, let’s add a slave! And use it if the master is unavailable. This is unfortunately not viable. By doing so we can’t implement our safety property of mutual exclusion, because Redis replication is asynchronous.
redis cluster 分布式鎖_漫談分布式鎖之Redis實作

如上圖所示:

  • 左邊client在Master擷取到鎖;
  • 在将鎖資訊同步到slave之前,master挂掉,此時發生failover;
  • slave節點更新為新的master(New Master),此時,其他線程來擷取鎖,發現并沒有其他線程占用,也加鎖成功。這導緻了鎖失效。

解決方案:

可以使用Redlock算法。

We propose an algorithm, called Redlock, which implements a DLM which we believe to be safer than the vanilla single instance approach.

04分布式利器—Redisson

Redisson是一個在Redis的基礎上實作的Java駐記憶體資料網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。這裡我們關注其對分布式鎖的支援,它解決了我們上面論述的幾種問題。

概覽Redisson鎖的實作

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

如上圖Redisson的類圖,總結常用對象:

  • 可重入鎖(Reentrant Lock)

  • 公平鎖(Fair Lock)

  • 聯鎖(MultiLock)

  • 紅鎖(RedLock)

  • 讀寫鎖(ReadWriteLock)

  • 信号量(Semaphore)

這裡我們以RedissonLock(最常用的)為例,了解下其常用的一些api:

public void lock();// 不建議使用,鎖續期僅僅在leaseTime = -1時生效public void lock(long leaseTime, TimeUnit unit);public boolean tryLock();// 不建議使用,鎖續期僅僅在leaseTime = -1時生效public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit);public boolean tryLock(long waitTime, TimeUnit unit);
           

上述api的實際使用,這裡不再示範,相對比較簡單,可以通過其官網學習:

https://github.com/redisson/redisson/wiki

注意事項

  • 鎖續期生效的場景:鎖續期僅僅在leaseTime = -1時生效;
  • 鎖【lock.lock()】的使用必須緊跟try代碼塊,且unlock要放到finally塊第一行。這點是阿裡規範,說明如下:
redis cluster 分布式鎖_漫談分布式鎖之Redis實作

05Redlock算法及其争議 為了解決叢集環境下,分布式鎖的缺陷,Antirez發明了Redlock算法,它的實作流程比較複雜,不過業界已經有很多開源的類庫封裝實作了。比如Redisson提供的RedissonRedLock對象,其實作了Redlock描述的加鎖算法。

Redlock算法

為了使用Redlock算法,需要提供多個Redis執行個體,并且這些執行個體之間互相獨立,沒有主從關系(不會有資料的同步等)。同很多分布式算法一樣,Redlock也使用大多數機制。加鎖時候,它會向過半節點發送set(key,value,nx=True,ex=xxx)指令,隻要過半節點set成功,就認為加鎖成功。釋放鎖時候,需要向所有節點發送del指令。不過Redlock算法還需要考慮逾時問題、出錯重試、時鐘漂移等很多細節問題,同時因為Redlock需要向多個節點進行讀寫,意味着其相比單執行個體的Redis的性能會下降一些。

Redlock算法的争議

可以參閱:

[1] https://redis.io/topics/distlock

[2] http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

[3] http://antirez.com/news/101

06總結 至此,簡單介紹了分布式鎖基于Redis的實作。實際項目中建議使用Redisson提供的鎖來保證臨界資源的安全性。對于追求業務強一緻的業務場景,可以利用分布式協調器來實作,如基于zookeeper的實作,這将在後面提到。07引用 [1] https://redis.io/topics/distlock

[2] https://github.com/redisson/redisson/wiki

[3] https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA

[4] https://learnku.com/articles/47769

[5] 黃健宏,《Redis設計與實作》,機械工業出版社

[6] Josiah L. Carlson,黃健宏,《Redis實戰》,中國工信出版社

[7] 錢文品,《Redis深度曆險》,中國工信出版社

發現“在看”和“贊”了嗎,戳我試試吧

redis cluster 分布式鎖_漫談分布式鎖之Redis實作

繼續閱讀