一. 小白寫法
1.設計思路
純DB操作
DB查庫存→判斷庫存→(DB扣減庫存+DB創建訂單)
2.分析
A.響應非常慢,導致大量請求拿不到結果而報錯
B.存在超賣現象
C.扣減庫存錯誤
3.壓測結果
前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。
代碼分享:

/// <summary> /// 原始版本-純DB操作 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <returns></returns> public string POrder1(string userId, string arcId, string totalPrice) { try { //1. 查詢庫存 var sArctile = _baseService.Entities<T_SeckillArticle>().Where(u => u.articleId == arcId).FirstOrDefault(); if (sArctile.articleStockNum - 1 > 0) { //2. 扣減庫存 sArctile.articleStockNum--; //3. 進行下單 T_Order tOrder = new T_Order(); tOrder.id = Guid.NewGuid().ToString("N"); tOrder.userId = userId; tOrder.orderNum = Guid.NewGuid().ToString("N"); tOrder.articleId = arcId; tOrder.orderTotalPrice = Convert.ToDecimal(totalPrice); tOrder.addTime = DateTime.Now; tOrder.orderStatus = 0; _baseService.Add<T_Order>(tOrder); _baseService.SaveChange(); return "下單成功"; } else { //賣完了 return "賣完了"; } } catch (Exception ex) { throw new Exception(ex.Message); } }
測試結果:
(1). 100並發,需要1788ms,訂單數量插入正確,但庫存扣減錯誤。
(2). 200並發,需要4453ms,訂單數量插入正確,但庫存扣減錯誤。
二. lock寫法
1.設計思路
純DB操作的基礎上Lock鎖
Lock { DB查庫存→判斷庫存→(DB扣減庫存+DB創建訂單) }
2.分析
A. 解決超賣現象
B. 響應依舊非常慢,導致大量請求拿到結果而報錯
3.壓測結果
前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。
代碼分享:

/// <summary> /// 02-純DB操作+Lock鎖 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <returns></returns> public string POrder2(string userId, string arcId, string totalPrice) { try { lock (_lock) { //1. 查詢庫存 var sArctile = _baseService.Entities<T_SeckillArticle>().Where(u => u.articleId == arcId).FirstOrDefault(); if (sArctile.articleStockNum - 1 > 0) { //2. 扣減庫存 sArctile.articleStockNum--; //3. 進行下單 T_Order tOrder = new T_Order(); tOrder.id = Guid.NewGuid().ToString("N"); tOrder.userId = userId; tOrder.orderNum = Guid.NewGuid().ToString("N"); tOrder.articleId = arcId; tOrder.orderTotalPrice = Convert.ToDecimal(totalPrice); tOrder.addTime = DateTime.Now; tOrder.orderStatus = 0; _baseService.Add<T_Order>(tOrder); _baseService.SaveChange(); return "下單成功"; } else { //賣完了 return "賣完了"; } } } catch (Exception ex) { throw new Exception(ex.Message); } }
(1). 30並發,需要2132ms,訂單數量插入正確,庫存扣減正確。
(2). 100並發,需要9186ms,訂單數量插入正確,庫存扣減正確。
三. 服務器緩存+隊列
1.設計思路
生產者和消費者模式→流量削峰(異步的模式平滑處理請求)
A. Lock{ 事先同步DB庫存到緩存→緩存查庫存→判斷庫存→訂單相關信息服務端隊列中 }
B. 消費者從隊列中取數據批量提交信息,依次進行(DB扣減庫存+DB創建訂單)
2.分析
A. 接口中徹底干掉了DB操作, 並發數提升非常大
B. 服務宕機,原隊列中的下單信息全部丟失
C. 但是生產者和消費者必須在一個項目及一個進程內
3.壓測結果
前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。
代碼分享:
初始化庫存到內存緩存中

/// <summary> /// 后台任務-初始化庫存到緩存中 /// </summary> public class CacheBackService : BackgroundService { private IMemoryCache _cache; private StackExchange.Redis.IDatabase _redisDb; private IConfiguration _Configuration; public CacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration) { _cache = cache; _redisDb = redisHelp.GetDatabase(); _Configuration = Configuration; } protected async override Task ExecuteAsync(CancellationToken stoppingToken) { // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的 // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文 var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>(); optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr")); ESHOPContext context = new ESHOPContext(optionsBuilder.Options); IBaseService _baseService = new BaseService(context); //初始化庫存信息,連臨時寫在這個位置,充當服務器啟動的時候初始化 var data = await _baseService.Entities<T_SeckillArticle>().Where(u => u.id == "300001").FirstOrDefaultAsync(); //服務器緩存 _cache.Set<int>($"{data.articleId}-sCount", data.articleStockNum); } }
隊列定義和下單接口

/// <summary> /// 基於內存的隊列 /// </summary> public static class MyQueue { private static ConcurrentQueue<string> _queue = new ConcurrentQueue<string>(); public static ConcurrentQueue<string> GetQueue() { return _queue; } } /// <summary> /// 03-服務端緩存+隊列版本+Lock /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <returns></returns> public string POrder3(string userId, string arcId, string totalPrice) { try { lock (_lock) { //1. 查詢庫存 int count = _cache.Get<int>($"{arcId}-sCount"); if (count - 1 >= 0) { //2. 扣減庫存 count = count - 1; _cache.Set<int>($"{arcId}-sCount", count); //3. 將下單信息存到消息隊列中 var orderNum = Guid.NewGuid().ToString("N"); MyQueue.GetQueue().Enqueue($"{userId}-{arcId}-{totalPrice}-{orderNum}"); //4. 把部分訂單信息返回給前端 return $"下單成功,訂單信息為:userId={userId},arcId={arcId},orderNum={orderNum}"; } else { //賣完了 return "賣完了"; } } } catch (Exception ex) { throw new Exception(ex.Message); } }
基於內存的消費者

/// <summary> /// 后台任務--基於內存隊列的消費者(已經測試) /// </summary> public class CustomerService : BackgroundService { private IConfiguration _Configuration; public CustomerService(IConfiguration Configuration) { _Configuration = Configuration; } protected async override Task ExecuteAsync(CancellationToken stoppingToken) { // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的 // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文 var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>(); optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr")); ESHOPContext context = new ESHOPContext(optionsBuilder.Options); IBaseService _baseService = new BaseService(context); Console.WriteLine("下面開始執行消費業務"); while (true) { try { string data = ""; MyQueue.GetQueue().TryDequeue(out data); if (!string.IsNullOrEmpty(data)) { List<string> tempData = data.Split('-').ToList(); //1.扣減庫存---禁止狀態追蹤 var sArctile = context.Set<T_SeckillArticle>().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault(); sArctile.articleStockNum = sArctile.articleStockNum - 1; context.Update(sArctile); //2. 插入訂單信息 T_Order tOrder = new T_Order(); tOrder.id = Guid.NewGuid().ToString("N"); tOrder.userId = tempData[0]; tOrder.orderNum = tempData[3]; tOrder.articleId = tempData[1]; tOrder.orderTotalPrice = Convert.ToDecimal(tempData[2]); tOrder.addTime = DateTime.Now; tOrder.orderStatus = 0; context.Add<T_Order>(tOrder); int count = await context.SaveChangesAsync(); //釋放一下 context.Entry<T_SeckillArticle>(sArctile).State = EntityState.Detached; Console.WriteLine($"執行成功,條數為:{count},當前庫存為:{ sArctile.articleStockNum}"); } else { Console.WriteLine("暫時沒有訂單信息,休息一下"); await Task.Delay(TimeSpan.FromSeconds(1)); } } catch (Exception ex) { Console.WriteLine($"執行失敗:{ex.Message}"); } } } }
(1). 1000並發,需要600ms,訂單數量插入正確,庫存扣減正確。
(2). 2000並發,需要1500ms,訂單數量插入正確,庫存扣減正確。
四. Redis緩存+原子性+隊列【干掉lock】
1.設計思路
生產者和消費者模式→流量削峰(異步的模式平滑處理請求)
思路同上,緩存和隊列改成基於Redis的。
2. 分析
A. 引入Redis緩存和消息隊列代替基於內存的緩存和隊列,數據可以持久化解決了丟失問題。
B. Redis是單線程的,利用api自身的原子性,從而可以干掉lock鎖。
C. 引入進程外的緩存Redis,從而可以把生產者和消費者解耦分離,可以作為兩個單獨的服務運行。
3. 壓測結果
前提:原庫存為10萬,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。
代碼分享:
初始化庫存到redis緩存中

/// <summary> /// 后台任務-初始化庫存到緩存中 /// </summary> public class CacheBackService : BackgroundService { private IMemoryCache _cache; private StackExchange.Redis.IDatabase _redisDb; private IConfiguration _Configuration; public CacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration) { _cache = cache; _redisDb = redisHelp.GetDatabase(); _Configuration = Configuration; } protected async override Task ExecuteAsync(CancellationToken stoppingToken) { // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的 // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文 var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>(); optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr")); ESHOPContext context = new ESHOPContext(optionsBuilder.Options); IBaseService _baseService = new BaseService(context); //初始化庫存信息,連臨時寫在這個位置,充當服務器啟動的時候初始化 var data = await _baseService.Entities<T_SeckillArticle>().Where(u => u.id == "300001").FirstOrDefaultAsync(); //Redis緩存 _redisDb.StringSet($"{data.articleId}-sCount", data.articleStockNum); } }
下單接口

/// <summary> /// 04-Redis緩存+隊列 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <returns></returns> public string POrder4(string userId, string arcId, string totalPrice) { try { //1. 直接自減1 int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1); if (iCount >= 0) { //2. 將下單信息存到消息隊列中 var orderNum = Guid.NewGuid().ToString("N"); _redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}"); //3. 把部分訂單信息返回給前端 return $"下單成功,訂單信息為:userId={userId},arcId={arcId},orderNum={orderNum}"; } else { //賣完了 return "賣完了"; } } catch (Exception ex) { throw new Exception(ex.Message); } }
基於redis隊列的消費者

{ Console.WriteLine("下面開始執行消費業務"); using (ESHOPContext db = new ESHOPContext()) { RedisHelp redisHelp = new RedisHelp("localhost:6379"); var redisDB = redisHelp.GetDatabase(); while (true) { try { var data = (string)redisDB.ListRightPop("200001"); if (!string.IsNullOrEmpty(data)) { List<string> tempData = data.Split('-').ToList(); { //1.扣減庫存 --去掉狀態追蹤 var sArctile = db.Set<T_SeckillArticle>().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault(); sArctile.articleStockNum = sArctile.articleStockNum - 1; db.Update(sArctile); //2. 插入訂單信息 T_Order tOrder = new T_Order(); tOrder.id = Guid.NewGuid().ToString("N"); tOrder.userId = tempData[0]; tOrder.orderNum = tempData[3]; tOrder.articleId = tempData[1]; tOrder.orderTotalPrice = Convert.ToDecimal(tempData[2]); tOrder.addTime = DateTime.Now; tOrder.orderStatus = 0; db.Add<T_Order>(tOrder); int count = db.SaveChanges(); //釋放一下--否則報錯 db.Entry<T_SeckillArticle>(sArctile).State = EntityState.Detached; Console.WriteLine($"執行成功,條數為:{count},當前庫存為:{ sArctile.articleStockNum}"); } } else { Console.WriteLine("暫時沒有訂單信息,休息一下"); Thread.Sleep(1000); } } catch (Exception ex) { Console.WriteLine($"執行失敗-{ex.Message}"); } } } }
(1). 1000並發,需要600ms,訂單數量插入正確,庫存扣減正確。
(2). 2000並發,需要1560ms,訂單數量插入正確,庫存扣減正確。
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。