再說分佈式鎖

分佈式鎖

1. 什麼是分佈式鎖?

1.1 使用場景

鎖,就是爲了防止多個線程併發操做共享變量而致使不可預期的結果鎖採起的一種串行化的方式,那分佈式鎖 ,顧名思義,也就是在控制分佈式條件下,線程可以串行化的操做共享變量,從而達到不一樣機器的進程的線程之間的同步或者互斥。java

1.2 常見的實現方式

分佈式鎖常見的實現方式有redis的實現,zk的實現,tair的實現,本篇咱們主要討論下經過redis的實現。redis

2. 分佈式鎖redis的實現

2.1 鎖的實現

話很少說,咱們先上代碼數據庫

class RedisLock{
    public boolean lock(String key, V v, int expireTime){
            int retry = 0;
            //獲取鎖失敗最多嘗試10次
            while (retry < failRetryTimes){
                //獲取鎖
                Boolean result = redis.setNx(key, v, expireTime);
                if (result){
                    return true;
                }
    
                try {
                    //獲取鎖失敗間隔一段時間重試
                    TimeUnit.MILLISECONDS.sleep(sleepInterval);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
    
            }
    
            return false;
        }
        public boolean unlock(String key){
            return redis.delete(key);
        }
}
複製代碼

這是分佈式鎖簡單的實現,先嚐試往redis裏面設值,若是成功則返回true,不然睡眠指定的時間重試,直到獲取成功。 若是超太重試的次數獲取鎖仍是失敗的話,就返回false。api

2.2 鎖的不足

咱們看下上面的實現,會發現如下存在的幾個問題:服務器

  1. 在咱們這一步Boolean result = redis.setNx(key, v, expireTime);去設置k,v的時候,若是此時返回的false是因爲超時致使,而實際redis是執行成功了, 那咱們從新再設置就會一直失敗,咱們就會在這裏空等待一個expireTime的時間週期。
  2. 鎖的釋放沒有檢測當前的鎖是不是當前線程所加,因此是有可能誤釋放掉別的線程加的鎖
  3. 可重入性,這個其實能夠和第2條放在一塊兒

2.3 鎖的改進

根據上面的兩點的不足,咱們改進下鎖的實現代碼:網絡

public class RedisLock {
    public boolean lock(String key, V v, int expireTime){
        int retry = 0;
        //獲取鎖失敗最多嘗試10次
        while (retry < failRetryTimes){
            //1.先獲取鎖,若是是當前線程已經持有,則直接返回
            //2.防止後面設置鎖超時,實際上是設置成功,而網絡超時致使客戶端返回失敗,因此獲取鎖以前須要查詢一下
            V value = redis.get(key);
            //若是當前鎖存在,而且屬於當前線程持有,直接返回
            if (null != value && value.equals(v)){
                return true;
            }

            //獲取鎖
            Boolean result = redis.setNx(key, v, expireTime);
            if (result){
                return true;
            }

            try {
                //獲取鎖失敗間隔一段時間重試
                TimeUnit.MILLISECONDS.sleep(sleepInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }

        return false;
    }
    public boolean unlock(String key, String requestId){
        String value = redis.get(key);
        //鎖應該是已經超時了,其實這裏能夠加一些監控去看下
        if (Strings.isNullOrEmpty(value)){
            return true;
        }
        //判斷當前鎖的持有者是不是當前線程,若是是的話釋放鎖,不是的話返回false
        if (value.equals(requestId)){
            redis.delete(key);
            return true;
        }

        return false;
    }
}
複製代碼

能夠看到,咱們針對上面不足的兩點已經作了些改進,這部分基本上已經知足了咱們業務的需求。 其實,咱們仍是能夠作一些優化的,並不是必須,不一樣的業務能夠有不一樣的實現方式:併發

  1. 在最後釋放鎖的時候,咱們先去判斷該鎖是否存在而且屬於當前線程全部,若是是的話再去釋放鎖,而這兩步操做其實並不是是原子性的,因此也是會存在 競汏條件,出現問題,因此咱們能夠把這兩部經過lua腳本實現作成原子性的,具體能夠不一樣的業務去考量
  2. 鎖的睡眠時間和重試次數能夠暴露出來,不一樣的業務能夠根據業務特色進行控制

2.4 分佈式鎖實踐遇到的問題

在咱們分佈式鎖的實際使用中,有遇到過一些考慮不足的點,這裏簡單列一下:分佈式

  1. 大多數業務的分佈式鎖,k和v是一致的,這種其實在併發量比較小的時候,問題不大,可是在併發量比較大的時候,是會出現當前線程釋放掉別的線程的鎖的,這點見到的比較多, 因此也單獨列出來講明下。
  2. 過時時間的設置,這個比較重要。咱們若是分佈式鎖以後,進行本地事務的操做,若是咱們設置的過時時間比事務的超時時間短,那麼可能就存在的問題是分佈式鎖已通過期了,可是事務還在等待提交,等到下一個 線程獲取到鎖以後,那就會存在併發提交,這就和咱們想經過分佈式鎖達到的預期結果相違背了,因此在設置過時時間的時候,若是有事務的操做須要格外注意下,包括失敗以後的重試,若是有的話 也須要多考量下。
  3. 鎖的監控,對於鎖獲取時間比較長,以及釋放的時候鎖已通過期了,對於這部分請求能夠監控下來,業務上去排查下,是否存在一些問題,在系統上儘可能咱們仍是作到 由咱們主動的去找系統的問題,而不是經過系統被動的曝出問題咱們去排查。

2.5 不合理的實現

網上常常還能夠看到這種實現方式,就是獲取到鎖以後要檢查下鎖的過時時間,若是鎖過時了要從新設置下時間,大體代碼以下:優化

public boolean tryLock2(String key, int expireTime){
        long expires = System.currentTimeMillis() + expireTime;

        //獲取鎖
        Boolean result = redis.setNx(key, expires, expireTime);
        if (result){
            return true;
        }

        V value = redis.get(key);
        if (value != null && (Long)value < System.currentTimeMillis()){
            //鎖已通過期
            String oldValue = redis.getSet(key, expireTime);
            if (oldValue != null && oldValue.equals(value)){
                return true;
            }
        }

        return false;

    }
複製代碼

這種實現存在的問題,過分依賴當前服務器的時間了,若是在大量的併發請求下,都判斷出了鎖過時,而這個時候再去設置鎖的時候,最終是會只有一個線程,可是可能會致使不一樣服務器根據自身不一樣的時間覆蓋掉最終獲取鎖的那個線程設置的時間。lua

3. 其餘實現的方式

3.1 zk的實現

網上有關zk實現的代碼比較多,這裏就不展現代碼了,能夠大體說下思路:

3.1.1獲取鎖
  1. 先有一個鎖跟節點,lockRootNode,這能夠是一個永久的節點
  2. 客戶端獲取鎖,先在lockRootNode下建立一個順序的瞬時節點,節點裏面能夠存儲當前線程的一些信息,好比requestId等能夠惟一識別當前線程的信息。瞬時節點能夠保證客戶端斷開鏈接,節點也自動刪除
  3. 調用lockRootNode父節點的getChildren()方法,獲取全部的節點,並從小到大排序,獲取最小節點,而且判斷最小節點的節點信息是不是當前線程,如果,則返回true,獲取鎖成功,不然,關注比本身序號小的節點的釋放動做(exist watch),這樣能夠保證每個客戶端只須要關注一個節點,不須要關注全部的節點,避免羊羣效應。
  4. 若是有節點釋放操做,重複步驟3
3.1.2釋放鎖

只須要刪除步驟2中建立的節點便可

3.2 tair的實現

經過tair來實現分佈式鎖和redis的實現核心差很少,不過tair有個很方便的api,感受是實現分佈式鎖的最佳配置,就是put api調用的時候須要傳入一個version,就和數據庫的樂觀鎖同樣,修改數據以後,版本會自動累加,若是傳入的版本和當前數據版本不一致,就不容許修改,具體能夠看下這篇文章的實現:Tair分佈式鎖這裏就再也不多說了

小結

分佈式鎖的常見實現方式,更多的經過redis和tair比較多一些。固然其使用的過程當中存在的問題還有好多咱們沒有說起到,好比redis集羣模式下,master down以後,鎖若是還沒來得及同步到從,那這個時候也會致使業務出現問題。 這裏也是想說明下,具體使用方式仍是須要根據不一樣的業務的需求進行考量,畢竟咱們使用這個是基於業務,須要保證業務的穩定運行。

相關文章
相關標籤/搜索