分布式環境下的並發編程


在JAVA多線程編程中,經常會用到synchronized、lock和原子變量等,分布式系統中,由於分布式系統的分布性,即多線程和多進程並且分布在不同機器中,synchronized和lock這兩種鎖將失去原有鎖的效果,需要我們自己實現分布式鎖來處理並發問題。分布式系統處理並發的辦法有三種

1.隊列

我們可以將所有要執行的任務放入隊列(kafka等)里,然后一個個消費,這樣就能避免並發問題。

2.悲觀鎖

我們經常會用到的,將數據記錄加版本號,如果版本號不一致,就不更新。這種方式同JAVA的CAS理念類似。

3.分布式鎖。

常見的分布式鎖有三種實現:

  基於數據庫實現分布式鎖
  基於緩存,實現分布式鎖,如redis
  基於Zookeeper實現分布式鎖

基於數據庫實現分布式鎖

利用數據庫表

首先創建一張鎖的表主要包含下列字段:方法名,時間戳等字段。方法名稱要游唯一性約束。

如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

可以對該方案優化,記錄當前獲得鎖的機器的主機信息和線程信息,那么下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了,實現可重入鎖。

基於數據庫排他鎖

在查詢語句后面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。其他沒有獲取到鎖的就會阻塞在上述select語句上,可能的結果有2種,在超時之前獲取到了鎖,在超時之前仍未獲取到鎖。獲得排它鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執行方法的業務邏輯,執行完方法之后,釋放鎖connection.commit()。存在的問題主要是性能不高和sql超時的異常。

基於Zookeeper實現分布式鎖
基於zookeeper臨時有序節點可以實現的分布式鎖。每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。
判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
提供的第三方庫有curator,具體使用大家可以自行去看一下。Curator提供的InterProcessMutex是分布式鎖的實現。acquire方法獲取鎖,release方法釋放鎖。

基於緩存(redis)來實現分布式鎖

網上很多用jedis.setnx()和jedis.expire()組合實現加鎖。setnx()方法作用就是SET IF NOT EXIST,expire()方法就是給鎖加一個過期時間。由於這是兩條Redis命令,不具有原子性,如果程序在執行完setnx()之后突然崩潰,導致鎖沒有設置過期時間。那么將會發生死鎖。網上之所以有人這樣實現,是因為低版本的jedis並不支持多參數的set()方法。

多參數的set來實現分布式鎖

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 嘗試獲取分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

可以看到,我們加鎖就一行代碼:jedis.set(String key, String value, String nxxx, String expx, int time),這個set()方法一共有五個形參:
第一個為key,我們使用key來當鎖,因為key是唯一的。
第二個為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們在上面講到可靠性時,分布式鎖要滿足第四個條件解鈴還須系鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用UUID.randomUUID().toString()方法生成。
第三個為nxxx,這個參數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;
第四個為expx,這個參數我們傳的是PX,意思是我們要給這個key加一個過期的設置,具體時間由第五個參數決定。
第五個為time,與第四個參數相呼應,代表key的過期時間。
總的來說,執行上面的set()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那么就進行加鎖操作,並對鎖設置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。
心細的童鞋就會發現了,我們的加鎖代碼滿足我們可靠性里描述的三個條件。首先,set()加入了NX參數,可以保證如果已有key存在,則函數不會調用成功,也就是只有一個客戶端能持有鎖,滿足互斥性。其次,由於我們對鎖設置了過期時間,即使鎖的持有者后續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發生死鎖。最后,因為我們將value賦值為requestId,代表加鎖的客戶端請求標識,那么在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端。

RedLock

redlock算法是redis作者推薦的一種分布式鎖實現方式,算法的內容如下:

(1) 獲取當前時間;

(2) 嘗試從5個相互獨立redis客戶端獲取鎖;

(3) 計算獲取所有鎖消耗的時間,當且僅當客戶端從多數節點獲取鎖,並且獲取鎖的時間小於鎖的有效時間,認為獲得鎖;

(4) 重新計算有效期時間,原有效時間減去獲取鎖消耗的時間;

(5) 刪除所有實例的鎖

redlock算法相對於單節點redis鎖可靠性要更高,但是實現起來條件也較為苛刻。

(1) 必須部署5個節點才能讓Redlock的可靠性更強。

(2) 需要請求5個節點才能獲取到鎖,通過Future的方式,先並發向5個節點請求,再一起獲得響應結果,能縮短響應時間,不過還是比單節點redis鎖要耗費更多時間。

然后由於必須獲取到5個節點中的3個以上,所以可能出現獲取鎖沖突,即大家都獲得了1-2把鎖,結果誰也不能獲取到鎖,這個問題,redis作者借鑒了raft算法的精髓,通過沖突后在隨機時間開始,可以大大降低沖突時間,但是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,所以獲取鎖的時間成本增加了。

 

數據庫鎖:

  • 優點:直接使用數據庫,使用簡單。

  • 缺點:分布式系統大多數瓶頸都在數據庫,使用數據庫鎖會增加數據庫負擔。

緩存鎖:

  • 優點:性能高,實現起來較為方便,在允許偶發的鎖失效情況,不影響系統正常使用,建議采用緩存鎖。

  • 缺點:通過鎖超時機制不是十分可靠,當線程獲得鎖后,處理時間過長導致鎖超時,就失效了鎖的作用。

zookeeper鎖:

  • 優點:不依靠超時時間釋放鎖;可靠性高;系統要求高可靠性時,建議采用zookeeper鎖。

  • 缺點:性能比不上緩存鎖,因為要頻繁的創建節點刪除節點。


免責聲明!

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



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