基於Redis分佈式鎖(獲取鎖及解鎖)


  目前幾乎不少大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。redis

  在不少場景中,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案。數據庫

  針對分佈式鎖的實現,目前比較經常使用的有如下幾種方案:編程

  基於數據庫實現分佈式鎖、基於緩存(redis,memcached)、實現分佈式鎖 基於Zookeeper實現分佈式鎖。緩存

  在分析這幾種實現方案以前咱們先來想一下,咱們須要的分佈式鎖應該是怎麼樣的?(這裏以方法鎖爲例,資源鎖同理)併發

  能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。dom

  這把鎖要是一把可重入鎖(避免死鎖)。異步

  這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)。編程語言

  有高可用的獲取鎖和釋放鎖功能。分佈式

  獲取鎖和釋放鎖的性能要好。memcached

  基於Redis鎖實現

  加鎖

  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,表明加鎖的客戶端請求標識,那麼在客戶端在解鎖的時候就能夠進行校驗是不是同一個客戶端。因爲咱們只考慮Redis單機部署的場景,因此容錯性咱們暫不考慮。

  解鎖

  private static final Long RELEASE_SUCCESS = 1L;

  /**

  * 釋放分佈式鎖

  * @param jedis Redis客戶端

  * @param lockKey 鎖

  * @param requestId 請求標識

  * @return 是否釋放成功

  */

  public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

  String script = if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end;

  Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

  if (RELEASE_SUCCESS.equals(result)) {

  return true;

  }

  return false;

  }

  能夠看到,咱們解鎖只須要兩行代碼就搞定了!第一行代碼,咱們寫了一個簡單的Lua腳本代碼,上一次見到這個編程語言仍是在《黑客與畫家》裏,沒想到此次竟然用上了。第二行代碼,咱們將Lua代碼傳到jedis.eval()方法裏,並使參數KEYS[1]賦值爲lockKey,ARGV[1]賦值爲requestId。eval()方法是將Lua代碼交給Redis服務端執行。

  不管加鎖仍是解鎖,必須是原子操做。

  分佈式鎖的異常問題

  若是一個獲取到鎖的client由於某種緣由致使沒能及時釋放鎖,而且Redis由於超時釋放了鎖,另一個client獲取到了鎖,此時狀況以下圖所示:

  

img

 

  那麼如何解決這個問題呢?

  一種方案是引入鎖續約機制,也就是獲取鎖以後,釋放鎖以前,會定時進行鎖續約,好比以鎖超時時間的1/3爲間隔週期進行鎖續約。

  關於開源的Redis的分佈式鎖實現有不少,比較出名的有redisson、百度的dlock。

  對於高可用性,通常能夠經過集羣或者master-slave來解決,Redis鎖優點是性能出色,劣勢就是因爲數據在內存中,一旦緩存服務宕機,鎖數據就丟失了。

  像Redis自帶複製功能,能夠對數據可靠性有必定的保證,可是因爲複製也是異步完成的,所以依然可能出現master節點寫入鎖數據而未同步到slave節點的時候宕機,鎖數據丟失問題。這個暫時沒有好的解決辦法。

相關文章
相關標籤/搜索