基於緩存的分佈式鎖(公司大牛內部文章分享)
目前有不少成熟的緩存產品,包括Redis,memcached等。這裏以Redis爲例來分析下使用緩存實現分佈式鎖的方案。主要的實現方式是使用Jedis.setNX方法來實現。java
public boolean trylock(String key) { ResultCode code = jedis.setNX(key, "This is a Lock."); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key){ ldbTairManager.invalid(NAMESPACE, key); }
以上實現方式一樣存在幾個問題:
一、單點問題。
二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在redis中,其餘線程沒法再得到到鎖。
三、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。
四、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在redis中已經存在。沒法再執行setNX操做。
五、這把鎖是非公平的,全部等待的線程同時去發起setNX操做,運氣好的線程能獲取鎖。redis
固然,一樣有方式能夠解決。
如今主流的緩存服務都支持集羣部署,經過集羣來解決單點問題。
沒有失效時間?redis的setExpire方法支持傳入失效時間,到達時間以後數據會自動刪除。
非阻塞?while重複執行。
非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。
非公平?在線程獲取鎖以前先把全部等待的線程放入一個隊列中,而後按先進先出原則獲取鎖。算法
redis集羣的同步策略是須要時間的,有可能A線程setNX成功後拿到鎖,可是這個值尚未更新到B線程執行setNX的這臺服務器,那就會產生併發問題。
redis的做者Salvatore Sanfilippo,提出了Redlock算法,該算法實現了比單一節點更安全、可靠的分佈式鎖管理(DLM)。
Redlock算法假設有N個redis節點,這些節點互相獨立,通常設置爲N=5,這N個節點運行在不一樣的機器上以保持物理層面的獨立。數據庫
算法的步驟以下:
一、客戶端獲取當前時間,以毫秒爲單位。
二、客戶端嘗試獲取N個節點的鎖,(每一個節點獲取鎖的方式和前面說的緩存鎖同樣),N個節點以相同的key和value獲取鎖。客戶端須要設置接口訪問超時,接口超時時間須要遠遠小於鎖超時時間,好比鎖自動釋放的時間是10s,那麼接口超時大概設置5-50ms。這樣能夠在有redis節點宕機後,訪問該節點時能儘快超時,而減少鎖的正常使用。
三、客戶端計算在得到鎖的時候花費了多少時間,方法是用當前時間減去在步驟一獲取的時間,只有客戶端得到了超過3個節點的鎖,並且獲取鎖的時間小於鎖的超時時間,客戶端纔得到了分佈式鎖。
四、客戶端獲取的鎖的時間爲設置的鎖超時時間減去步驟三計算出的獲取鎖花費時間。
五、若是客戶端獲取鎖失敗了,客戶端會依次刪除全部的鎖。
使用Redlock算法,能夠保證在掛掉最多2個節點的時候,分佈式鎖服務仍然能工做,這相比以前的數據庫鎖和緩存鎖大大提後端
問題一:一、GC等場景可能隨時發生,並致使在客戶端獲取了鎖,在處理中超時,致使另外的客戶端獲取了鎖?緩存
在一個客戶端獲取了分佈式鎖後,在客戶端的處理過程當中,可能出現鎖超時釋放的狀況,這裏說的處理中除了GC等非抗力外,程序流程未處理完也是可能發生的。以前在說到數據庫鎖設置的超時時間2分鐘,若是出現某個任務佔用某個訂單鎖超過2分鐘,那麼另外一個交易中心就能夠得到這把訂單鎖,從而兩個交易中心同時處理同一個訂單。正常狀況,任務固然秒級處理完成,但是有時候,加入某個rpc請求設置的超時時間過長,一個任務中有多個這樣的超時請求,那麼,極可能就出現超過自動解鎖時間了。當初咱們的交易模塊是用C++寫的,不存在GC,若是用java寫,中間還可能出現Full GC,那麼鎖超時解鎖後,本身客戶端沒法感知,是件很是嚴重的事情。我以爲這不是鎖自己的問題,上面說到的任何一個分佈式鎖,只要自帶了超時釋放的特性,都會出現這樣的問題。若是使用鎖的超時功能,那麼客戶端必定得設置獲取鎖超時後,採起相應的處理,而不是繼續處理共享資源。Redlock的算法,在客戶端獲取鎖後,會返回客戶端能佔用的鎖時間,客戶端必須處理該時間,讓任務在超過該時間後中止下來。安全
問題二:算法依賴本地時間,會出現時鐘不許,致使2個客戶端同時得到鎖的狀況?
Redlock有個關鍵的特性是,獲取鎖的時間是鎖默認超時的總時間減去獲取鎖所花費的時間,這樣客戶端處理的時間就是一個相對時間,就跟本地時間無關了。服務器
總結:
由此看來,Redlock的正確性是能獲得很好的保證的。仔細分析Redlock,相比於一個節點的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場景下是很重要的特性。可是我以爲Redlock爲了實現可靠性,卻花費了過大的代價。併發
首先必須部署5個節點才能讓Redlock的可靠性更強。而後須要請求5個節點才能獲取到鎖,經過java Future的方式,先併發向5個節點請求,再一塊兒得到響應結果,能縮短響應時間,不過仍是比單節點redis鎖要耗費更多時間。而後因爲必須獲取到5個節點中的3個以上,因此可能出現獲取鎖衝突,即你們都得到了1-2把鎖,結果誰也不能獲取到鎖,這個問題,redis做者借鑑了raft算法的精髓,經過沖突後在隨機時間開始,能夠大大下降衝突時間,可是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,因此獲取鎖的時間成本增長了。若是5個節點有2個宕機,此時鎖的可用性會極大下降,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這所有3個節點的鎖才能擁有鎖,難度也加大了。分析了這麼多緣由,我以爲Redlock的問題,最關鍵的一點在於Redlock須要客戶端去保證寫入的一致性,後端5個節點徹底獨立,全部的客戶端都得操做這5個節點。
若是5個節點有一個leader,客戶端只要從leader獲取鎖,其餘節點能同步leader的數據,這樣,分區、超時、衝突等問題都不會存在。因此爲了保證分佈式鎖的正確性,我以爲使用強一致性的分佈式協調服務能更好的解決問題。分佈式
問題又來了,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。
對於這個問題目前主流的作法是每得到一個鎖時,只設置一個很短的超時時間,同時起一個線程在每次快要到超時時間時去刷新鎖的超時時間。在釋放鎖的同時結束這個線程。如redis官方的分佈式鎖組件redisson,就是用的這種方案