C# Redis分布式鎖(基於ServiceStack.Redis)


  相關的文章其實不少,我也從中受益不少,但是還是想自己梳理一下,畢竟自己寫的更走心!

  首先給出一個拓展類,通過拓展方法實現加鎖和解鎖。

  注:之所以增加拓展方法,是因為合理使用拓展類(方法),可以讓程序更簡潔,拓展性更好。如.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 }

  實際上實現分布式鎖定的關鍵就是以上這些代碼,不過就此結束肯定不是丁哥的風格!雖然水平有限,但絕對是盡力說的明白。模擬一下使用場景吧,希望不會露怯呢。

  請留意最后標注的變量值,從這幾個值可以看出:

  1. 商品沒有超賣,300個;
  2. 出現了秒殺應該出現的場景,有人擠進來了但是商品已經秒光了(沒有被通知,茫然臉),3個;
  3. 還有一些人實際上可以認為根本就沒排上隊,請求來時已經賣光了(被正式通知),97個;
  4. 請求沒有丟,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、緩存預熱等更多的問題。我這里就是拋轉引玉,希望對大家有些許幫助。

 

努力工作 認真生活 持續學習 以勤補拙


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM