• 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、缓存预热等更多的问题。我这里就是抛转引玉,希望对大家有些许帮助。

    努力工作 认真生活 持续学习 以勤补拙

  • 相关阅读:
    使用NSTask调用shell
    《UML和模式应用》读书笔记(一)
    iOS网络编程
    多线程
    Quartz2D
    沙盒中的数据存取
    UIButton设置为圆形按钮并增加边框
    Mac开发快速入门
    JavaWeb学习总结(三)response与request
    JavaWeb学习总结(二) Servlet
  • 原文地址:https://www.cnblogs.com/dinggeonly/p/11143637.html
Copyright © 2020-2023  润新知