【redis】-分布式锁-解决多个进程并发秒杀引起的超卖问题
一.单进程多线程的锁-线程锁
锁住线程的锁叫线程锁,像C#中的lock,Monitor,让线程排队,同一时刻只能有一个线程进来,让线程同步排队。
二.多进程的锁-分布式锁
锁住进程的锁就叫分布式锁,是锁住进程的一种机制,让进程排队。
三.电商秒杀场景
3.1.单体架构
并发量不够,秒杀服务只能并发1000,而客户端同时发送3000个请求。
3.2.集群架构
这时候就需要多两个角色,一个角色是网关,一个角色是秒杀集群,网关把收到的用户请求转发到3个不同的秒杀服务,这样每个秒杀服务并发1000个请求,而有3个秒杀服务,就能够满足客户端同时发送3000个请求。
四.秒杀服务集群带来新的问题
第1个请求进入到秒杀服务1里面,查询数据库商品库存是10,判断有库存,扣减库存,更新数据库,当前库存是9。
第2个请求进入到秒杀服务2里面,查询数据库商品库存是10,判断有库存,扣减库存,更新数据库,当前库存是9。
第3个请求进入到秒杀服务3里面,查询数据库商品库存是10,判断有库存,扣减库存,更新数据库,当前库存是9。
实际库存只减少了1个,但是同1个商品被3个人秒杀到了,这就是超卖问题。
五.分布式锁解决什么问题?
分布式系统中,涉及到多个进程共享资源的时候,就需要使用分布式锁。
谁抢到了锁,谁才能操作数据库扣减库存、增加订单。
六.运行效果
6.1.单进程发起20个线程模拟20个用户并发请求,秒杀商品,会发现20个线程,20个请求秒杀到同1个商品。
6.2.对于单进程可以通过加lock锁解决超卖问题
商品库存有10个,开启20个线程秒杀商品,有10个请求分别秒杀到不同的商品,另外10个线程没有秒杀到商品,因为库存只有10个。
6.3.我现在把相同的代码Copy一份,新建个工程MyRedis.SecKill.MultiProcess.Other,也同样使用了lock锁,快速的启动2个进程,每个进程中开启20个线程就发现lock锁不住了,lock锁失效了,同一个商品编号10被2个不同的进程中的线程秒杀到了。
我们看到单进程通过加lock锁可以保证不发生超卖问题,10个线程秒杀到商品,商品编号不同,另外10个线程没有秒杀到商品。
但是因为为了提高并发量,现在是秒杀服务集群提供秒杀服务了,我们在两个秒杀服务进程中都开启20个线程去秒杀商品,就会发现如图所示控制不住了,两个进程中的线程都秒杀到同一个商品了(这里用商品库存当做商品编号),那么如何解决跨进程并发引起的商品超卖问题?这就需要分布式锁了。
七.封装Redis分布式锁--解决跨进程并发秒杀超卖问题
7.1.秒杀服务端
namespace MyRedis.SecKill.MultiProcess.SecKill { /// <summary> /// 商品秒杀服务 /// </summary> public class ProductSecKill { /// <summary> /// 秒杀方法 /// </summary> public void SecKillProduct() { RedisLock redisLock = new RedisLock(); redisLock.Lock(); //lock (this)//只是适合单进程 //{ //1.获取商品库存 var productStock = GetPorductStocks(); //2.判断商品库存是否为空 if (productStock.Conut == 0) { //2.1 秒杀失败消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒杀已结束,商品编号:{productStock.Conut}"); redisLock.UnLock(); return; } //3.秒杀成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒杀成功,商品编号:{productStock.Conut}"); //4.扣减商品库存 SubtracPorductStocks(productStock); //} redisLock.UnLock(); } /// <summary> /// 获取商品库存 /// </summary> /// <returns></returns> private Product_Stock GetPorductStocks() { using (ShoppingEntities shoppingEntities = new ShoppingEntities()) { //1。查询数据库获取库存,获取第一个商品的库存数 Product_Stock productStock = shoppingEntities.Product_Stock.FirstOrDefault(s => s.Id == 1); //2.返回库存 return productStock; } } /// <summary> /// 扣减商品库存 /// </summary> private void SubtracPorductStocks(Product_Stock stocks) { using (ShoppingEntities shoppingEntities = new ShoppingEntities()) { //1.扣减商品库存 Product_Stock updateStocks = shoppingEntities.Product_Stock.FirstOrDefault(s => s.Id == stocks.Id); updateStocks.Conut = stocks.Conut - 1; //2.更新数据库 shoppingEntities.SaveChanges(); } } } }
7.2.秒杀客户端
namespace MyRedis.SecKill.MultiProcess { class Program { static void Main(string[] args) { //1.开始秒杀 ClientRequest.SendRequest(20); Console.ReadKey(); } } }
namespace MyRedis.SecKill.MultiProcess.SecKill { class ClientRequest { /// <summary> /// 客户端请求 /// </summary> /// <param name="threadCount">线程数</param> public static void SendRequest(int threadCount) { //1.商品秒杀服务 ProductSecKill productSecKill = new ProductSecKill(); //2.创建20个请求来秒杀 for (int i = 0; i < threadCount; i++) { Thread thread = new Thread(() => { productSecKill.SecKillProduct(); }); thread.Start(); } } } }
7.3.Redis分布式锁
封装分布式锁4要素
1.锁名
2.加锁操作
锁对象,也就是谁持有这把锁,持有锁的才能解锁
3.解锁操作
4.锁超时时间
namespace MyRedis.SecKill.MultiProcess.Locks { /// <summary> /// redis分布式锁 /// 分布式锁四要素 /// 1.锁名 /// 2.加锁操作 /// 3.解锁操作 /// 4.锁超时时间 /// </summary> class RedisLock { //1.redis连接管理类 private ConnectionMultiplexer connectionMultiplexer = null; //2.redis数据操作类 private IDatabase database = null; public RedisLock() { connectionMultiplexer = ConnectionMultiplexer.Connect("localhost:6379"); database = connectionMultiplexer.GetDatabase(0); } /// <summary> /// 1.加锁 /// </summary> public void Lock() { // 1.redis加锁api--LockTake // key--锁名--redis_lock // value--锁对象(谁持有这把锁)--进程Id+线程Id // expiry--锁超时时间,为什么?解锁死锁问题! // 2.如果加锁失败?循环加锁,对于未知的事情用循环 while (true) { bool flag = database.LockTake("redis_lock", "ProcessNo1" +Thread.CurrentThread.ManagedThreadId, TimeSpan.FromSeconds(10)); //3.如果加锁成功,则退出循环 if (flag) { break; } //3.1 加锁失败,线程休眠下,走循环,再尝试加锁 Thread.Sleep(200); } } /// <summary> /// 2.解锁 /// </summary> public void UnLock() { //1.redis解锁api--LockRelease // key--锁名--redis_lock // value--锁对象(谁持有这把锁)--进程Id+线程Id--使加锁和解锁是同一个对象 //2.如果解锁失败?循环解锁,对于未知的事情用循环 bool flag = database.LockRelease("redis_lock", "ProcessNo1" +Thread.CurrentThread.ManagedThreadId); while (true) { //3.如果解锁成功,则退出循环 if (flag) { break; } //3.1.解锁失败,线程休眠下,走循环,再尝试解锁 Thread.Sleep(200); } //4.关闭资源 connectionMultiplexer.Dispose(); } } }
八.再次运行效果
最后我们发现库存36个商品,2个进程,每个进程开启20个线程,都是不同的商品编号没有秒杀到同一件商品。
九.项目结构