歡迎光臨
每天分享高質量文章

ASP.NET Core中借助CSRedis實現安全高效的分佈式鎖

引言

最近回頭看了看開發的.NET Core 2.1專案的復盤總結,其中在多處用到Redis實現的分佈式鎖,雖然在OnResultExecuting方法中做了防止死鎖的處理,但在某些場景下還是會發生死鎖的問題,下麵我只展示部分代碼:

問題:

1、這裡setnx設置的值“1”,我想問,你最後del的這個值一定是你自己創建的嗎?

2、圖中標註的步驟1和步驟2不是原子操作,會有死鎖的概率嗎?

大家可以思考一下先,下麵讓我們帶著這兩個問題往下看,下麵介紹一下使用Redis實現分佈式鎖常用的幾個命令。

一、使用Redis實現分佈式鎖常見的幾個命令

► Setnx

命令:SETNX key value

說明:將 key 的值設為 value ,當且僅當 key 不存在。若給定的 key 已經存在,則 SETNX 不做任何動作。SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。

時間複雜度:O(1)

傳回值:設置成功,傳回1 ; 設置失敗,傳回 0

► Getset

命令:GETSET key value

說明:將給定 key 的值設為 value ,並傳回 key 的舊值(old value)。當 key 存在但不是字串型別時,傳回一個錯誤。

時間複雜度:O(1)

傳回值:傳回給定 key 的舊值; 當 key 沒有舊值時,也即是, key 不存在時,傳回 nil 。

► Expire

命令:EXPIRE key seconds

說明:為給定 key 設置生存時間,當 key 過期時(生存時間為 0 ),它會被自動刪除。

時間複雜度:O(1)

傳回值:設置成功傳回 1 ;當 key 不存在或者不能為 key 設置生存時間時(比如在低於 2.1.3 版本的 Redis 中你嘗試更新 key 的生存時間),傳回 0 。

► Del

命令:DEL key [key …]

說明:刪除給定的一個或多個 key 。不存在的 key 會被忽略。

時間複雜度:O(N); N 為被刪除的 key 的數量。

刪除單個字串型別的 key ,時間複雜度為O(1)。

刪除單個串列、集合、有序集合或哈希表型別的 key ,時間複雜度為O(M), M 為以上資料結構內的元素數量。

傳回值:被刪除 key 的數量。

好了,命令熟悉之後,下麵我們就開始一步一步實現分佈式鎖。

二、使用Redis實現分佈式鎖版本一:與時間戳的結合

對於上面的setnx設置的預設值1,我們採用時間戳來防止問題一,下麵先讓我們來看下想當然寫法流程圖。

C#代碼實現:

static void Main(string[] args)
{
   var lockTimeout = 5000;//單位是毫秒
   var currentTime = DateTime.Now.ToUnixTime(true);
   if (SetNx("lockkey", currentTime+ lockTimeout,lockTimeout))
   {
       //TODO:一些業務邏輯代碼
       //.....
       //.....
       //最後釋放鎖
       Remove("lockkey");
   }
   else
   {
       Console.WriteLine("沒有獲得分佈式鎖");
   }
   Console.ReadKey();
}

public static bool SetNx(string key,long time ,double expireMS)
{
   if (redisClient.SetNx(key, time))
   {
       if (expireMS > 0)
           redisClient.Expire(key, TimeSpan.FromMilliseconds(expireMS));
       return true;
   }
   return false;
}

public static bool Remove(string key)
{
   return redisClient.Del(key) > 0;
}

上面的代碼中value的值我們使用時間戳,不是一個固定的值了,至少能保證你刪除的key確實是你自己的,所以,建議大家在設value的值時,不要設置一個固定的值,最好是隨機的。

但是這樣寫雖然解決了問題一,但是這種寫法還是存在一定的風險,雖然Redis是單執行緒的並且setnx、expire是原子操作,但是先setnx再expire就不是原子操作了!!!我們要考慮多執行緒環境和容器部署時多實體環境等等,那這樣的寫法就會出現問題。

比如:現在有A、B兩台服務器在跑這個應用,當A台應用跑到:setnx成功但是還沒有設置過期時間的時候,突然重啟服務,這個時候在分佈式環境中就會發生死鎖的問題,因為你沒有設置過期時間。

下麵我們通過除錯來展示死鎖的場景:

A應用:在執行到setnx成功但是在執行expire之前宕機了,此時的Redis已經有資料了,但是沒有過期時間

B應用:運行正常

但是B應用就會一直獲取不到鎖,導致死鎖。

所以上面在獲取鎖的邏輯還是有問題的,為瞭解決這個問題,我們採用下麵的方式來處理。

三、使用Redis實現分佈式鎖版本二:雙重防死鎖

流程圖:

C#代碼實現:

public static void RedisLockV2()
{
   var lockTimeout = 5000;//單位是毫秒
   var currentTime = DateTime.Now.ToUnixTime(true);
   if (SetNxV2("lockkey",DateTime.Now.ToUnixTime(true)+lockTimeout))
   {
       //設置過期時間
       redisClient.Expire("lockkey", TimeSpan.FromMilliseconds(5000));
       //TODO:一些業務邏輯代碼
       Console.WriteLine("處理業務ing");
       Thread.Sleep(100000);
       Console.WriteLine("處理業務ed");
       //最後釋放鎖
       Remove("lockkey");
   }
   else
   {
       //未獲取到鎖,繼續判斷,判斷時間戳看看是否可以重置並獲取鎖
       var lockValue = redisClient.Get("lockkey");
       var time = DateTime.Now.ToUnixTime(true);
       if (!string.IsNullOrEmpty(lockValue) &&  time> lockValue.ToInt64())
       {
           //再次用當前時間戳getset
           //傳回固定key的舊值,舊值判斷是否可以獲取鎖
           var getsetResult = redisClient.GetSet("lockkey", time);
           if (getsetResult == null || (getsetResult != null && getsetResult == lockValue))
           {
               Console.WriteLine("獲取到Redis鎖了");
               //真正獲取到鎖
               redisClient.Expire("lockkey", TimeSpan.FromMilliseconds(5000));
               //TODO:一些業務邏輯代碼
               //.....
               //.....
               Console.WriteLine("處理業務");
               //最後釋放鎖
               Remove("lockkey");
           }
           else
           {
               Console.WriteLine("沒有獲取到鎖");
           }
       }
       else
       {
           Console.WriteLine("沒有獲取到鎖");
       }
   }
}

現在,Redis中的情況如下:

我們運行上面的代碼,結果如下:

副本.exe中添加一行代碼。來模擬這種場景:有A、B兩台服務器在跑這個應用,當A台應用跑到:setnx成功但是還沒有設置過期時間的時候,突然重啟服務,這個時候在分佈式環境中就會發生死鎖的問題,因為你沒有設置過期時間

我們先執行Lottery.ThriftRpc – 副本.exe,等Redis裡面有值了,並且這個key是沒有過期時間,再關閉掉該程式:

然後,再執行Lottery.ThriftRpc.exe

我們是不是解決了該問題,至於過期時間設置為多少要結合你的具體業務處理時間來計算出一個合理的值,好了,聊到這裡關於Redis的分佈式鎖就講完了。

四、總結

上面的示例中Redis的組件用的是CSRedisCore,這裡只是自己的一點體會,如果你有更好的辦法,可以在評論區討論,關於Redis的理論講解有太多的文章了,大家可以參考,關於Redis的文章我只總結工作中遇到的一些問題,關於文章中的原始碼,我就不提供了,太簡單了。

原文地址:http://cnblogs.com/runningsmallguo/p/10322315.html

赞(0)

分享創造快樂