前言
我們之前聊過redis的,對基礎不瞭解的可以移步檢視一下:
幾分鐘搞定redis儲存session共享——設計實現:https://www.cnblogs.com/xiongze520/p/10333233.html
對同一個資源進行操作,單一的快取讀取沒問題了,但是存在併發的時候怎麼辦呢,為了避免資料不一致,我們需要在操作共享資源之前進行加鎖操作。
我們在開發很多業務場景會使用到鎖,例如庫存控制,抽獎,秒殺等。一般我們會使用記憶體鎖的方式來保證線性的執行。
但現在大多站點都會使用分散式部署,那多臺伺服器間的就必須使用同一個目標來判斷鎖。分散式與單機情況下最大的不同在於其不是多執行緒而是多程式。
圖1:分散式站點使用記憶體鎖
圖2:分散式站點使用分散式鎖
當然我們暫時用不了這麼複雜的場景,我們就簡單訪問redis就行。
設計(悲觀鎖/樂觀鎖)
悲觀鎖方式(認為操作的時候,會出現問題,所以都加鎖)
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,
所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。
傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖方式(認為什麼時候不會出問題,所以不上鎖,更新的時候去查詢判斷一下,再此期間是否有人修改過這個資料。)
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,
所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。
樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。
兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,
加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了效能,所以這種情況下用悲觀鎖就比較合適。
Redis三個命令
1、SETNX
SETNX key value:當且僅當key不存在時,set一個key為val的字串,返回1;若key存在,則什麼都不做,返回0。
2、expire
expire key timeout:為key設定一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。
3、delete
delete key:刪除key
在使用Redis實現分散式鎖的時候,主要就會使用到這三個命令。
命題:某商品進行庫存秒殺。
假設要給某個商品舉行秒殺活動,我們事先把庫存資料100已經存入到了redis中,我們現在需要來進行庫存扣減。
圖3:加鎖請求示意圖
程式碼實現
我們基於 ServiceStack.Redis 操作
我們建立一個控制檯應用(.NET Framework),命名為 RedisLock ,注意,如果建立的是net core的應用,引入的ServiceStack.Redis就要選擇core的。
然後在NuGet裡面安裝ServiceStack.Redis。
Redis連線池
//Redis連線池(配置連線地址,讀寫連線地址等) public static PooledRedisClientManager RedisClientPool = CreateManager(); private static PooledRedisClientManager CreateManager() { //寫節點(主節點) List<string> writes = new List<string>(); writes.Add("10.17.3.97:6379"); //讀節點 List<string> reads = new List<string>(); reads.Add("10.17.3.97:6379"); //配置連線池和讀寫分類 return new PooledRedisClientManager(writes, reads, new RedisClientManagerConfig() { MaxReadPoolSize = 50, //讀節點個數 MaxWritePoolSize = 50,//寫節點個數 AutoStart = true, DefaultDb = 0 }); }
使用Redis的SetNX命令實現加鎖
/// <summary> /// 加鎖(使用Redis的SetNX命令實現加鎖) /// </summary> /// <param name="key">鎖key</param> /// <param name="selfMark">自己標記</param> /// <param name="lockExpirySeconds">鎖自動過期時間[預設10](s)</param> /// <param name="waitLockMilliseconds">等待鎖時間(ms)</param> /// <returns></returns> public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue) { DateTime begin = DateTime.Now; selfMark = Guid.NewGuid().ToString("N");//自己標記,釋放鎖時會用到,自己加的鎖除非過期否則只能自己開啟 using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient()) { string lockKey = "Lock:" + key; while (true) { string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000); //迴圈獲取取鎖 if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1) { return true; } //不等待鎖則返回 if (waitLockMilliseconds == 0) { break; } //超過等待時間,則不再等待 if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds) { break; } Thread.Sleep(100); } return false; } }
因為ServiceStack.Redis提供的SetNX方法,並沒有提供設定過期時間的方法,對於加鎖業務又不能分開執行(如果加鎖成功設定過期時間失敗導致的永久死鎖問題),所以就使用指令碼實現,解決了異常情況死鎖問題.
如果設定為0,為樂觀鎖機制,獲取不到鎖,直接返回未獲取到鎖.
預設值為long最大值,為悲觀鎖機制,約等於很多很多天,可以理解為一直等待.
釋放鎖
/// <summary> /// 釋放鎖 /// </summary> /// <param name="key">鎖key</param> /// <param name="selfMark">自己標記</param> public static void UnLock(string key, string selfMark) { using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient()) { string lockKey = "Lock:" + key; var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark }); } }
業務呼叫(我們使用多執行緒模擬多使用者秒殺的場景)
//業務:悲觀鎖方式 public static void PessimisticLock() { int num = 10; //總數量 string lockkey = "xianseng"; Console.WriteLine("我是【悲觀鎖】"); //悲觀鎖開啟20個人同時拿寶貝 for (int i = 0; i < 20; i++) { Task.Run(() => { string selfmark = ""; try { if (Lock(lockkey, out selfmark)) { if (num > 0) { num--; Console.WriteLine($"我拿到了寶貝:寶貝剩餘{num}個\t\t{selfmark}"); } else { Console.WriteLine("寶貝已經沒有了"); } Thread.Sleep(100); } } finally { UnLock(lockkey, selfmark); } }); } Console.ReadLine(); } //業務:樂觀鎖方式 public static void OptimisticLock() { int num = 10; //總數量 string lockkey = "xianseng"; Console.WriteLine("我是【樂觀鎖】"); //樂觀鎖開啟10個執行緒,每個執行緒拿5次 for (int i = 0; i < 10; i++) { var lineOn = "執行緒" + (i + 1); Task.Run(() => { for (int j = 0; j < 5; j++) { string selfmark = ""; try { if (Lock(lockkey, out selfmark, 10, 0)) { if (num > 0) { num--; Console.WriteLine($"{lineOn} 第{(j+1)}次 我拿到了寶貝:寶貝剩餘{num}個\t\t{selfmark}"); } else { Console.WriteLine($"{lineOn} 第{(j + 1)}次 寶貝已經沒有了"); } Thread.Sleep(1000); } else { Console.WriteLine($"{lineOn} 第{(j+1)}次 沒有拿到,不想等了"); } } finally { UnLock(lockkey, selfmark); } } }); } Console.ReadLine(); }
然後在main函式裡面呼叫檢視展示效果
static void Main(string[] args) { ////呼叫:悲觀鎖方式(認為我操作的時候,會出現問題,所以都加鎖) PessimisticLock(); ///呼叫:樂觀鎖方式(認為什麼時候不會出問題,所以不上鎖,更新的時候去查詢判斷一下,再此期間是否有人修改過這個資料。) //OptimisticLock(); }
這就簡單實現了Redis分佈鎖的功能,快去試試吧。
參考文獻
- Redis如何實現分散式鎖:https://www.cnblogs.com/laohanshuibi/p/15164807.html
- 淺談Redis快取的常用5種方式(String,Hash,List,set,SetSorted ):https://www.cnblogs.com/xiongze520/p/10267804.html
- 幾分鐘搞定redis儲存session共享——設計實現:https://www.cnblogs.com/xiongze520/p/10333233.html
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂 公眾號:熊澤有話說 出處: https://www.cnblogs.com/xiongze520/p/15176559.html 創作不易,任何人或團體、機構全部轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文連結。
|