詳解ASP.Net Core 中如何借助CSRedis實現(xiàn)一個安全高效的分布式鎖
引言:最近回頭看了看開發(fā)的.Net Core 2.1項目的復(fù)盤總結(jié),其中在多處用到Redis實現(xiàn)的分布式鎖,雖然在OnResultExecuting方法中做了防止死鎖的處理,但在某些場景下還是會發(fā)生死鎖的問題,下面我只展示部分代碼:

問題:
(1)這里setnx設(shè)置的值“1”,我想問,你最后del的這個值一定是你自己創(chuàng)建的嗎?
(2)圖中標(biāo)注的步驟1和步驟2不是原子操作,會有死鎖的概率嗎?
大家可以思考一下先,下面讓我們帶著這兩個問題往下看,下面介紹一下使用Redis實現(xiàn)分布式鎖常用的幾個命令。
一、使用Redis實現(xiàn)分布式鎖常見的幾個命令
► Setnx
命令:SETNX key value
說明:將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在。若給定的 key 已經(jīng)存在,則 SETNX 不做任何動作。SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。
時間復(fù)雜度:O(1)
返回值:設(shè)置成功,返回1 ; 設(shè)置失敗,返回 0
► Getset
命令:GETSET key value
說明:將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。當(dāng) key 存在但不是字符串類型時,返回一個錯誤。
時間復(fù)雜度:O(1)
返回值:返回給定 key 的舊值; 當(dāng) key 沒有舊值時,也即是, key 不存在時,返回 nil 。
► Expire
命令:EXPIRE key seconds
說明:為給定 key 設(shè)置生存時間,當(dāng) key 過期時(生存時間為 0 ),它會被自動刪除。
時間復(fù)雜度:O(1)
返回值:設(shè)置成功返回 1 ;當(dāng) key 不存在或者不能為 key 設(shè)置生存時間時(比如在低于 2.1.3 版本的 Redis 中你嘗試更新 key 的生存時間),返回 0 。
► Del
命令:DEL key [key ...]
說明:刪除給定的一個或多個 key 。不存在的 key 會被忽略。
時間復(fù)雜度:O(N); N 為被刪除的 key 的數(shù)量。
刪除單個字符串類型的 key ,時間復(fù)雜度為O(1)。
刪除單個列表、集合、有序集合或哈希表類型的 key ,時間復(fù)雜度為O(M), M 為以上數(shù)據(jù)結(jié)構(gòu)內(nèi)的元素數(shù)量。
返回值:被刪除 key 的數(shù)量。
好了,命令熟悉之后,下面我們就開始一步一步實現(xiàn)分布式鎖。
二、使用Redis實現(xiàn)分布式鎖版本一:與時間戳的結(jié)合
對于上面的setnx設(shè)置的默認(rèn)值1,我們采用時間戳來防止問題一,下面先讓我們來看下想當(dāng)然寫法流程圖。
流程圖:

C#代碼實現(xiàn):
static void Main(string[] args)
{
var lockTimeout = 5000;//單位是毫秒
var currentTime = DateTime.Now.ToUnixTime(true);
if (SetNx("lockkey", currentTime+ lockTimeout,lockTimeout))
{
//TODO:一些業(yè)務(wù)邏輯代碼
//.....
//.....
//最后釋放鎖
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確實是你自己的,所以,建議大家在設(shè)value的值時,不要設(shè)置一個固定的值,最好是隨機(jī)的。但是這樣寫雖然解決了問題一,但是這種寫法還是存在一定的風(fēng)險,雖然Redis是單線程的并且setnx、expire是原子操作,但是先setnx再expire就不是原子操作了?。。∥覀円紤]多線程環(huán)境和容器部署時多實例環(huán)境等等,那這樣的寫法就會出現(xiàn)問題。
比如:現(xiàn)在有A、B兩臺服務(wù)器在跑這個應(yīng)用,當(dāng)A臺應(yīng)用跑到:setnx成功但是還沒有設(shè)置過期時間的時候,突然重啟服務(wù),這個時候在分布式環(huán)境中就會發(fā)生死鎖的問題,因為你沒有設(shè)置過期時間。
下面我們通過調(diào)試來展示死鎖的場景:
A應(yīng)用:在執(zhí)行到setnx成功但是在執(zhí)行expire之前宕機(jī)了,此時的Redis已經(jīng)有數(shù)據(jù)了,但是沒有過期時間

B應(yīng)用:運行正常
但是B應(yīng)用就會一直獲取不到鎖,導(dǎo)致死鎖。

所以上面在獲取鎖的邏輯還是有問題的,為了解決這個問題,我們采用下面的方式來處理。
三、使用Redis實現(xiàn)分布式鎖版本二:雙重防死鎖
流程圖:

C#代碼實現(xiàn):
public static void RedisLockV2()
{
var lockTimeout = 5000;//單位是毫秒
var currentTime = DateTime.Now.ToUnixTime(true);
if (SetNxV2("lockkey",DateTime.Now.ToUnixTime(true)+lockTimeout))
{
//設(shè)置過期時間
redisClient.Expire("lockkey", TimeSpan.FromMilliseconds(5000));
//TODO:一些業(yè)務(wù)邏輯代碼
Console.WriteLine("處理業(yè)務(wù)ing");
Thread.Sleep(100000);
Console.WriteLine("處理業(yè)務(wù)ed");
//最后釋放鎖
Remove("lockkey");
}
else
{
//未獲取到鎖,繼續(xù)判斷,判斷時間戳看看是否可以重置并獲取鎖
var lockValue = redisClient.Get("lockkey");
var time = DateTime.Now.ToUnixTime(true);
if (!string.IsNullOrEmpty(lockValue) && time> lockValue.ToInt64())
{
//再次用當(dāng)前時間戳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:一些業(yè)務(wù)邏輯代碼
//.....
//.....
Console.WriteLine("處理業(yè)務(wù)");
//最后釋放鎖
Remove("lockkey");
}
else
{
Console.WriteLine("沒有獲取到鎖");
}
}
else
{
Console.WriteLine("沒有獲取到鎖");
}
}
}
現(xiàn)在,Redis中的情況如下:

我們運行上面的代碼,結(jié)果如下:

副本.exe中添加一行代碼。來模擬這種場景:有A、B兩臺服務(wù)器在跑這個應(yīng)用,當(dāng)A臺應(yīng)用跑到:setnx成功但是還沒有設(shè)置過期時間的時候,突然重啟服務(wù),這個時候在分布式環(huán)境中就會發(fā)生死鎖的問題,因為你沒有設(shè)置過期時間

我們先執(zhí)行Lottery.ThriftRpc - 副本.exe,等Redis里面有值了,并且這個key是沒有過期時間,再關(guān)閉掉該程序:

然后,再執(zhí)行Lottery.ThriftRpc.exe


看,我們是不是解決了該問題,至于過期時間設(shè)置為多少要結(jié)合你的具體業(yè)務(wù)處理時間來計算出一個合理的值,好了,聊到這里關(guān)于Redis的分布式鎖就講完了,希望對你有幫助,謝謝。
四、總結(jié):
上面的示例中Redis的組件用的是CSRedisCore,這里只是自己的一點體會,如果你有更好的辦法,可以在評論區(qū)討論,關(guān)于Redis的理論講解有太多的文章了,大家可以參考,關(guān)于Redis的文章我只總結(jié)工作中遇到的一些問題,關(guān)于文章中的源碼,我就不提供了,太簡單了。后面我會不定期分享一些Redis的問題,希望大家多多支持。
以上所述是小編給大家介紹的ASP.Net Core 中如何借助CSRedis實現(xiàn)一個安全高效的分布式鎖詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
- Redis數(shù)據(jù)庫基礎(chǔ)與ASP.NET?Core緩存實現(xiàn)
- ASP.NET?Core擴(kuò)展庫ServiceStack.Redis用法介紹
- Asp.net core中RedisMQ的簡單應(yīng)用實現(xiàn)
- .NET Core中使用Redis與Memcached的序列化問題詳析
- .net core如何使用Redis發(fā)布訂閱
- .net core使用redis基于StackExchange.Redis
- 詳解Asp.net Core 使用Redis存儲Session
- 詳解如何在ASP.NET Core中使用Redis
- .NET?Core實現(xiàn)簡單的Redis?Client框架
相關(guān)文章
Community Server專題二:體系結(jié)構(gòu)
Community Server專題二:體系結(jié)構(gòu)...2007-03-03
Request.QueryString與一般NameValueCollection的區(qū)別
最近在做一個搜索程序的優(yōu)化改進(jìn),將搜索結(jié)果按照查詢的參數(shù)不同進(jìn)行緩存。緩存的Key很自然的就想到了用查詢字符串,而獲取查詢字符串的最簡單方式是通過Request.QueryString.ToString()方法2011-12-12
.NET中獲取Access新增記錄Id怪現(xiàn)象解決方法
寫了一個函數(shù)獲取Access表中指定用戶Id,要求當(dāng)傳入的用戶名不存在時,則在表中新增一條記錄并返回Id2012-03-03
jQuery 插件autocomplete自動完成應(yīng)用(自動補(bǔ)全)(asp.net后臺)
項目中有時會用到自動補(bǔ)全查詢,就像Google搜索框、淘寶商品搜索功能,輸入漢字或字母,則以該漢字或字母開頭的相關(guān)條目會顯示出來供用戶選擇, autocomplete插件就是完成這樣的功能2011-10-10
創(chuàng)建第一個ASP.NET應(yīng)用程序(第1節(jié))
本文通過創(chuàng)建第一個ASP.NET應(yīng)用程序,了解.net代碼后置技術(shù)以及事件驅(qū)動機(jī)制和web頁面設(shè)計中的基本控件使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2015-08-08

