相關的文章其實不少,我也從中受益不少,但是還是想自己梳理一下,畢竟自己寫的更走心!
首先給出一個拓展類,通過拓展方法實現加鎖和解鎖。
注:之所以增加拓展方法,是因為合理使用拓展類(方法),可以讓程序更簡潔,拓展性更好。如.Net Core中新增拓展就是通過拓展類實現的,如services.AddMemoryCache();services.AddSignalR()。哎呀說多了!
1 using ServiceStack.Redis; 2 using System; 3 4 namespace Redis.Core.Extension 5 { 6 /// <summary> 7 /// RedisNativeClient拓展類 8 /// </summary> 9 public static class RedisNativeClientExtension 10 { 11 /// <summary> 12 /// 鎖定指定的Key 13 /// </summary> 14 /// <param name="redisClient">RedisClient 對象</param> 15 /// <param name="key">要鎖定的Key</param> 16 /// <param name="expirySeconds">鎖定時長 秒</param> 17 /// <param name="waitSeconds">鎖定等待時長 秒 默認不等待</param> 18 /// <returns>是否鎖定成功</returns> 19 public static bool Lock(this RedisNativeClient redisClient,string key, int expirySeconds = 5, double waitSeconds = 0) 20 { 21 int waitIntervalMs = 50;//間隔等待時長 毫秒 合理配置一下 應該也會影響性能 間隔時間太長肯定是不行的 22 string lockKey = "lock_key:" + key; 23 24 DateTime begin = DateTime.Now; 25 while (true) 26 { 27 if (redisClient.SetNX(lockKey, new byte[] { 1 }) == 1) 28 { 29 redisClient.Expire(lockKey, expirySeconds); 30 return true; 31 } 32 33 //不等待鎖則返回 34 if (waitSeconds <= 0) 35 break; 36 37 if ((DateTime.Now - begin).TotalSeconds >= waitSeconds)//等待超時 38 break; 39 40 System.Threading.Thread.Sleep(waitIntervalMs); 41 } 42 return false; 43 } 44 45 /// <summary> 46 /// 接觸鎖定 47 /// </summary> 48 /// <param name="redisClient">RedisClient 對象</param> 49 /// <param name="key">要解鎖的Key</param> 50 /// <returns></returns> 51 public static long UnLock(this RedisNativeClient redisClient, string key) 52 { 53 string lockKey = "lock_key:" + key; 54 return redisClient.Del(lockKey); 55 } 56 } 57 }
實際上實現分布式鎖定的關鍵就是以上這些代碼,不過就此結束肯定不是丁哥的風格!雖然水平有限,但絕對是盡力說的明白。模擬一下使用場景吧,希望不會露怯呢。
請留意最后標注的變量值,從這幾個值可以看出:
- 商品沒有超賣,300個;
- 出現了秒殺應該出現的場景,有人擠進來了但是商品已經秒光了(沒有被通知,茫然臉),3個;
- 還有一些人實際上可以認為根本就沒排上隊,請求來時已經賣光了(被正式通知),97個;
- 請求沒有丟,300+3+97,共400個。

1 List<string> products = new List<string>(); 2 public Startup(IConfiguration configuration) 3 { 4 Configuration = configuration; 5 6 for (int i = 0; i < 300; i++) 7 { 8 //模擬了300個商品 實際這些商品保存在Redis里更合適 這里是為了少寫一些代碼了 9 products.Add("product_" + i.ToString()); 10 } 11 //這里只是我自己封裝的類庫,根據配置文件創建了一個PooledRedisClientManager 12 //不要想復雜了 替換成你的創建方式就可以了 13 var clientManager = new Redis.Core.RedisClientManager(configuration); 14 string lockKey = "秒殺_0706"; //隨意寫的一個鎖定用的key 這個根據業務確定用什么值就可以了 例如說商品的編號 15 var clientTemp = clientManager.GetClient(); 16 clientTemp.Set("已售罄", false); //存一些數值幫你看明白運行效果 17 clientTemp.Set("售出數量", 0); 18 clientTemp.Set("售罄后請求次數", 0); 19 clientTemp.Set("緩存顯示已售罄", 0); 20 clientTemp.Set("鎖定失敗", 0); 21 clientTemp.Dispose(); 22 DateTime startTime = DateTime.Now; 23 bool isSellOut = false; 24 List<Task> tasks = new List<Task>(); 25 for (int i = 0; i < 400; i++) //多線程方式 模擬很多人搶有限的商品 26 { 27 tasks.Add(Task.Factory.StartNew(() => 28 { 29 using (var client = (ServiceStack.Redis.RedisClient)clientManager.GetClient()) 30 { 31 isSellOut = client.Get<bool>("已售罄"); 32 if (isSellOut) 33 { 34 client.Incr("緩存顯示已售罄");//真實場景中 此時會在前端頁面給用戶提示:已經賣光啦 35 } 36 //這個Lock及下面的UnLock是個拓展方法 為了讓大家看清楚邏輯 所以當靜態方法用了 37 //3 是鎖定時間3秒;1 是進行鎖定等待時間1秒,這個值越小 鎖定失敗的幾率就會越大。 38 else if (Redis.Core.Extension.RedisNativeClientExtension.Lock(client, lockKey, 3, 1)) 39 { 40 if (products.Count == 0) 41 { 42 //這個很重要 這里標記已經賣完 就不會再做后面的邏輯了 43 //能避免后面幾十萬的不必要訪問(如果是電商的話) 44 client.Set("已售罄", true); 45 //秒殺場景中 肯定會有大量用戶請求執行到這個環節 所以采用isSellOut 46 client.Incr("售罄后請求次數"); 47 } 48 else 49 { 50 client.Incr("售出數量"); 51 products.RemoveAt(0);//這里只是最簡單的模擬了生成訂單、減少庫存量 52 } 53 //As you know,這里要解鎖得啦 54 Redis.Core.Extension.RedisNativeClientExtension.UnLock(client, lockKey); 55 } 56 else 57 { 58 client.Incr("鎖定失敗"); 59 } 60 } 61 })); 62 } 63 Task.WaitAll(tasks.ToArray());//等待所有人搶完(多線程的知識點這里就不講了喲) 然后才獲取下面的結果 64 var 售出數量 = clientManager.GetClient().Get<string>("售出數量"); //300 65 var 售罄后請求次數 = clientManager.GetClient().Get<string>("售罄后請求次數"); //3 66 var 緩存顯示已售罄 = clientManager.GetClient().Get<string>("緩存顯示已售罄"); //97 67 var 鎖定失敗 = clientManager.GetClient().Get<string>("鎖定失敗"); //0 68 }
真實秒殺場景必然要比以上示例要復雜的多,涉及到商品信息的來源(不要走DB喲),訂單的創建等等,這時又會涉及到MQ、緩存預熱等更多的問題。我這里就是拋轉引玉,希望對大家有些許幫助。
努力工作 認真生活 持續學習 以勤補拙