引言:最近回頭看了看開發的.Net Core 2.1項目的覆盤總結,其中在多處用到Redis實現的分佈式鎖,雖然在OnResultExecuting方法中作了防止死鎖的處理,但在某些場景下仍是會發生死鎖的問題,下面我只展現部分代碼:redis
問題:服務器
(1)這裏setnx設置的值「1」,我想問,你最後del的這個值必定是你本身建立的嗎?數據結構
(2)圖中標註的步驟1和步驟2不是原子操做,會有死鎖的機率嗎?多線程
你們能夠思考一下先,下面讓咱們帶着這兩個問題往下看,下面介紹一下使用Redis實現分佈式鎖經常使用的幾個命令。分佈式
► Setnxspa
命令: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 。code
► Expireblog
命令: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 的數量。
好了,命令熟悉以後,下面咱們就開始一步一步實現分佈式鎖。
對於上面的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應用就會一直獲取不到鎖,致使死鎖。
因此上面在獲取鎖的邏輯仍是有問題的,爲了解決這個問題,咱們採用下面的方式來處理。
流程圖:
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的文章我只總結工做中遇到的一些問題,關於文章中的源碼,我就不提供了,太簡單了。後面我會不按期分享一些Redis的問題,但願你們多多支持。
做者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。