詳解Redis分散式鎖

大雄45發表於2021-06-26
導讀 最近首度應用"分散式鎖",現在想想,分散式鎖不是孤立的技能點,這其實就是跨主機的執行緒同步。

詳解Redis分散式鎖詳解Redis分散式鎖

分散式鎖是"執行緒同步"的延續

最近首度應用"分散式鎖",現在想想,分散式鎖不是孤立的技能點,這其實就是跨主機的執行緒同步。

詳解Redis分散式鎖詳解Redis分散式鎖

單機伺服器可以透過共享某堆記憶體來標記上鎖/解鎖,執行緒同步說到底是建立在單機作業系統的使用者態/核心態對共享記憶體的訪問控制。

而分散式伺服器不是在同一臺機器上:跨主機,因此需要將鎖標記儲存在所有機器程式都能看到的地方。

在開發很多業務場景會使用到鎖,例如庫存控制,抽獎等。

例如庫存只剩1個商品,有三個使用者同時打算購買,誰先購買庫存立即清零,不能讓其他二人也購買成功。

解讀分散式鎖

我們常說的執行緒安全、執行緒同步方案,包括此次的分散式鎖都是基於

“多執行緒/多程式對特定資源同時有更新操作”。

詳解Redis分散式鎖詳解Redis分散式鎖

基本考量
  1. 分散式系統,一個鎖在同一時間只能被一個伺服器獲取 (這是分散式鎖的基礎)
  2. 具備鎖失效機制,防止死鎖 (防止某些意外,鎖沒有得到釋放,別人也無法得到鎖)

Redis SET resource-name anystring NX EX max-lock-time

是一種最簡單的分散式鎖實現方案。

SET  支援多個引數:

  • EX seconds-- 設定過期時間(s)
  • NX -- 如果key不存在,則設定 ......

因為SET 引數可以替代SETNX,SETEX,GETSET,這些命令在未來可能被廢棄。

上面的命令返回OK(或經過重試),客戶端就獲取到這個鎖;

使用DEL命令解鎖;到達超時時間會自動釋放鎖。

在解鎖時,增加一些設計,讓系統更加健壯:

3.不要使用固定的String值作為鎖標記值,而是使用一個不易被猜中的隨機值, 業內稱為token

4.不使用DEL命令釋放鎖,而是傳送script去移除key

第3、4點是為了解決 :“鎖提前過期,客戶端A還沒有執行完,然後客戶端B獲取了鎖,這時客戶端A執行完了,會不會在刪鎖的時候把B的鎖給刪掉” -- 4是3技術上的推薦實現。

如下:

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

下面使用StackExchange.Redis 寫了基於以上考量的程式碼示例:

////// Acquires the lock. 
//////  
/// 隨機值 
///  
 /// 非阻塞鎖 
static bool Lock(string key, string token,int expireSecond=10, double waitLockSeconds = 0) 
{ 
    var waitIntervalMs = 50; 
    bool isLock; 
             
    DateTime begin = DateTime.Now; 
    do 
    { 
         isLock = Connection.GetDatabase().StringSet(key, token, TimeSpan.FromSeconds(expireSecond), When.NotExists); 
         if (isLock) 
             return true; 
             //不等待鎖則返回 
             if (waitLockSeconds == 0) break; 
             //超過等待時間,則不再等待 
             if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break; 
             Thread.Sleep(waitIntervalMs); 
     } while (!isLock); 
     return false; 
 } 
        
////// Releases the lock.   
//////true, if lock was released,falseotherwise./// Key.   
/// value   
static bool UnLock(string key, string value) 
{ 
    string lua_script = @"   
    if (redis.call('GET', KEYS[1]) == ARGV[1]) then   
         redis.call('DEL', KEYS[1])   
          return true   
          else   
          return false   
        end   
      "; 
     try 
     { 
          var res = Connection.GetDatabase().ScriptEvaluate(lua_script, 
                                                           new RedisKey[] { key }, 
                                                           new RedisValue[] { value }); 
            return (bool)res; 
      } 
     catch (Exception ex) 
     { 
          Console.WriteLine($"ReleaseLock lock fail...{ex.Message}"); 
          return false; 
     } 
} 
         
        private static LazylazyConnection = new Lazy(() => 
        { 
            ConfigurationOptions configuration = new ConfigurationOptions 
            { 
                AbortOnConnectFail = false, 
                ConnectTimeout = 5000, 
            }; 
            configuration.EndPoints.Add("10.100.219.9", 6379); 
            return ConnectionMultiplexer.Connect(configuration.ToString()); 
        });  
        public static ConnectionMultiplexer Connection => lazyConnection.Value;

以上程式碼新增了第五點考量:

5. 為避免無限制搶鎖,增加了非阻塞鎖:輪詢_s等待鎖,未等到則不再搶鎖

使用方式:

下面並行開啟三個任務,同時減少庫存:

static void Main(string[] args) 
{ 
     // 嘗試並行執行3個任務 
     Parallel.For(0, 3, x => 
     { 
           string token = $"loki:{x}"; 
           bool isLocked = Lock("loki", token, 5, 10); 
             
           if (isLocked) 
           { 
               Console.WriteLine($"{token} begin reduce stocks (with lock) at {DateTime.Now}."); 
               Thread.Sleep(1000); 
               Console.WriteLine($"{token} release lock {UnLock("loki", token)} at {DateTime.Now}. "); 
           } 
           else 
           { 
             Console.WriteLine($"{token} begin reduce stocks at {DateTime.Now}."); 
           } 
       }); 
}

詳解Redis分散式鎖詳解Redis分散式鎖

可以看到三個並行任務依次獲取/釋放鎖

輸出總結

本文從基礎的執行緒安全、執行緒同步,認識到分散式鎖是跨主機的資源執行緒/程式同步方案, 以步步為營的風格 演示了RedisSET命令做分散式鎖的設計考量,好記性不如爛筆頭。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2778485/,如需轉載,請註明出處,否則將追究法律責任。

相關文章