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 }
实际上实现分布式锁定的关键就是以上这些代码,不过就此结束肯定不是丁哥的风格!虽然水平有限,但绝对是尽力说的明白。模拟一下使用场景吧,希望不会露怯呢。
请留意最后标注的变量值,从这几个值可以看出:
- 商品没有超卖,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、缓存预热等更多的问题。我这里就是抛转引玉,希望对大家有些许帮助。
努力工作 认真生活 持续学习 以勤补拙