Redis分佈式鎖服務

概述

在多線程環境下,一般會使用鎖來保證有且只有一個線程來操做共享資源。好比:git

object obj = new object();
lock (obj) 
{ 
//操做共享資源 
}

利用操做系統提供的鎖機制,能夠確保多線程或多進程下的併發惟一操做。但若是在多機環境下就不能知足了,當A,B兩臺機器同時操做C機器的共享資源時,就須要第三方的鎖機制來保證在分佈式環境下的資源協調,也稱分佈式鎖。github

Redis有三個最基本屬性來保證分佈式鎖的有效實現:redis

  • 安全性: 互斥,在任什麼時候候,只有一個客戶端能持有鎖。
  • 活躍性A:沒有死鎖,即便客戶端在持有鎖的時候崩潰,最後也會有其餘客戶端能得到鎖,超時機制。
  • 活躍性B:故障容忍,只有大多數Redis節點時存活的,客戶端仍能夠得到鎖和釋放鎖。

分佈式鎖

因爲Redis是單線程模型,命令操做原子性,因此利用這個特性能夠很容易的實現分佈式鎖。 得到一個鎖算法

SET key uuid NX PX timeout
SET resource_name uniqueVal NX PX 30000

命令中的NX表示若是key不存在就添加,存在則直接返回。PX表示以毫秒爲單位設置key的過時時間,這裏是30000ms。 設置過時時間是防止得到鎖的客戶端忽然崩潰掉或其餘異常狀況,致使redis中的對象鎖一直沒法釋放,形成死鎖。 Key的值須要在全部請求鎖服務的客戶端中,確保是個惟一值。 這是爲了保證拿到鎖的客戶端能安全釋放鎖,防止這個鎖對象被其餘客戶端刪除。 舉個例子:安全

  1. A客戶端拿到對象鎖,但在由於一些緣由被阻塞致使沒法及時釋放鎖。
  2. 由於過時時間已到,Redis中的鎖對象被刪除。
  3. B客戶端請求獲取鎖成功。
  4. A客戶端此時阻塞操做完成,刪除key釋放鎖。
  5. C客戶端請求獲取鎖成功。
  6. 這時B、C都拿到了鎖,所以分佈式鎖失效。

要避免例子中的狀況發生,就要保證key的值是惟一的,只有拿到鎖的客戶端才能進行刪除。 基於這個緣由,普通的del命令是不能知足要求的,咱們須要一個能判斷客戶端傳過來的value和鎖對象的value是否同樣的命令。遺憾的是Redis並無這樣的命令,但能夠經過Lua腳原本完成:多線程

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

邏輯很簡單,獲取key中的值和參數中的值相比較,相等刪除,不相等返回0。 併發

多實例分佈式鎖

上面是在單個Redis實例實現分佈式鎖的,這存在一個問題就是,若是這臺實例因某些緣由崩潰掉,那麼全部客戶端的鎖服務所有失效。 Redis自己支持Master-Slave結構,能夠一主多從,採用高可用方法,能夠保證在master掛的時候自動切換到slave。 可是因爲主從之間是異步同步數據的,因此redis並不能徹底的實現鎖的安全性。 舉個例子來講:異步

  1. A客戶端在master實例上得到一個鎖。
  2. 在對象鎖key傳送到slave以前,master崩潰掉。
  3. 一個slave被選舉成master。
  4. B客戶端能夠獲取到同個key的鎖,但A也已經拿到鎖,致使鎖失效。

在多臺master狀況下實現這個算法,並保證鎖的安全性。 步驟以下:分佈式

  1. 客戶端以毫秒爲單位獲取當前時間。
  2. 使用一樣key和值,循環在多個實例中得到鎖。 爲了得到鎖,客戶端應該設置個偏移時間,它小於鎖自動釋放時間(即key的過時時間)。 舉個例子來講,若是一個鎖自動釋放時間是10秒,那偏移時間應該設置在5~50毫秒的範圍。 防止由於某個實例崩潰掉或其餘緣由,致使client在獲取鎖時耗時過長。
  3. 計算獲取全部鎖的耗時,即當前時間減去開始時間,獲得a值。 用鎖自動釋放時間減去a值,在減去偏移時間,獲得c值,若是獲取鎖成功的實例數量大於實際的數量一半,而且c大於0,那麼鎖就被獲取成功。
  4. 鎖獲取成功,鎖對象的有效時間是上面的c值。
  5. 如果客戶端由於一些緣由獲取失敗,緣由多是上面的c值爲負數或者鎖成功的數量小於實例數,以用N/2+1當標準(N爲實例數)。 那麼會釋放全部實例上的鎖。

上面描述可能不方便理解,用代碼表示以下:ui

//鎖自動釋放時間
TimeSpan ttl=new TimeSpan(0,0,0,30000)
//獲取鎖成功的數量
 int n = 0; 
//記錄開始時間
 var startTime = DateTime.Now;

  //在每一個實例上獲取鎖
                for_each_redis(
                    redis =>
                    {
                        if (LockInstance(redis, resource, val, ttl)) n += 1;
                    }
                );

//偏移時間是鎖自動釋放時間的1%,根據上面10s是5-50毫秒推出。
 var drift = Convert.ToInt32(ttl.TotalMilliseconds * 0.01); 

//鎖對象的有效時間=鎖自動釋放時間-(當前時間-開始時間)-偏移時間
 var validity_time = ttl - (DateTime.Now - startTime) - new TimeSpan(0, 0, 0, 0, drift);

//判斷成功的數量和有效時間c值是否大於0 if (n >= (N/2+1) && validity_time.TotalMilliseconds > 0) { }

總結

用Redis作分佈式鎖相比其餘分佈式鎖(zookeeper)實現更簡單,速度更快。 在ServiceStack.Redis客戶端組件上是直接支持鎖實現的。 或者用stackexchange客戶端組件,鎖實現及示例代碼:https://github.com/kidfashion/redlock-cs。

官方介紹文檔:http://redis.io/topics/distlock。

相關文章
相關標籤/搜索