在JAVA多線程編程中,常常會用到synchronized、lock和原子變量等,分佈式系統中,因爲分佈式系統的分佈性,即多線程和多進程而且分佈在不一樣機器中,synchronized和lock這兩種鎖將失去原有鎖的效果,須要咱們本身實現分佈式鎖來處理併發問題。分佈式系統處理併發的辦法有三種redis
1.隊列算法
咱們能夠將全部要執行的任務放入隊列(kafka等)裏,而後一個個消費,這樣就能避免併發問題。sql
2.悲觀鎖數據庫
咱們常常會用到的,將數據記錄加版本號,若是版本號不一致,就不更新。這種方式同JAVA的CAS理念相似。編程
3.分佈式鎖。緩存
常見的分佈式鎖有三種實現:多線程
基於數據庫實現分佈式鎖
基於緩存,實現分佈式鎖,如redis
基於Zookeeper實現分佈式鎖併發
基於數據庫實現分佈式鎖dom
利用數據庫表分佈式
首先建立一張鎖的表主要包含下列字段:方法名,時間戳等字段。方法名稱要遊惟一性約束。
若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。
能夠對該方案優化,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了,實現可重入鎖。
基於數據庫排他鎖
在查詢語句後面增長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鎖。
-
缺點:性能比不上緩存鎖,由於要頻繁的建立節點刪除節點。