一. 單品限流
1. 含義
某件商品n秒內只接受m個請求, 比如:限制商品A在2s內只接受500個下單請求。
2.設計思路
利用Redis自增的Api,該商品的第一個請求進來的時候設置緩存過期時間,限制內正常走業務,限制外返回限流提示;時間到了,原緩存內容消失,下一次第一個請求進來重新設置過期時間
3.分析
單品限流屬於商品層次的限流,后面會有Nginx全局限流
4.壓測結果
要求:1秒內該商品只能接收100個下單請求。
代碼分享:

/// <summary> /// 05-單品限流 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <param name="goodNum">用戶購買的商品數量</param> /// <returns></returns> public string POrder5(string userId, string arcId, string totalPrice, int goodNum = 1) { try { //一. 業務完善優化 //1. 單品限流 { int tLimits = 100; //限制請求數量 int tSeconds = 1; //限制秒數 string limitKey = $"LimitRequest{arcId}";//受限商品ID long myLimitCount = _redisDb.StringIncrement(limitKey, 1); //key不存在則會自動創建,第一次創建返回值為1 if (myLimitCount > tLimits) { throw new Exception($"不能購買了,{tSeconds}秒內只能請求{tLimits}次"); //return $"不能購買了,{tSeconds}秒內只能請求{tLimits}次"; } else if (myLimitCount == 1) { //設置過期時間 _redisDb.KeyExpire(limitKey, TimeSpan.FromSeconds(tSeconds)); } } #endregion //二. 邏輯優化 //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); } }
測試:1s內對商品發送500個請求,異常率80%,說明指接收了100個請求,同時庫存扣減和訂單創建也正確。
二. 購買商品限制
1. 含義
每位用戶在秒殺期間對某商品只能購買m件.
PS:哪件商品限制購買多少件依靠DB設計,事先錄好,不同商品的限制數量不同。
2. 設計思路
A. 同樣是利用Redis自增API, 1個用戶對應1件商品 存一條記錄
B. 也要設置一下過期時間,設計一個合理的數值,秒殺結束后,數據失效消失即可
C. 配合前端購買框內的設計限制
3. 分析
購買商品限制可以防止黃牛大量囤貨
4. 壓測結果
要求:1件商品一個用戶只能購買3件。
代碼分享:

/// <summary> /// 06-限制購買數量 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <param name="goodNum">用戶購買的商品數量</param> /// <returns></returns> public string POrder6(string userId, string arcId, string totalPrice, int goodNum = 1) { try { //一. 業務完善優化 //1. 單品限流 #region 2. 限制用戶購買數量 { //表示用戶商品可以購買的數量 //(秒殺商品表中有個limitNum字段,同步到redis中,這里從redis中讀取這個限制),這里臨時先寫死 int tGoodBuyLimits = 3; //這里先臨時寫死 string userBuyGoodLimitKey = $"userBuyGoodLimitKey-{userId}-{arcId}"; long myGoodLimitCount = _redisDb.StringIncrement(userBuyGoodLimitKey, goodNum); if (myGoodLimitCount > tGoodBuyLimits) { throw new Exception($"不能購買了,一個用戶只能買{tGoodBuyLimits}件"); } else { //這里設置10min,表示10min后秒殺結束,用戶可以繼續購買了,這個緩存消失 (這里緩存是否覆蓋影響不大) _redisDb.KeyExpire(userBuyGoodLimitKey, TimeSpan.FromMinutes(10)); } } #endregion //二. 邏輯優化 //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); } }
測試:模擬同一個用戶發送100個請求,異常率為97%,說明該用戶只能搶3件
三. 方法冪等
1. 含義
用戶在下單頁面,假設網絡延遲多次點擊按鈕,服務端僅處理第一次請求(第一次成功則成功,失敗則失敗),退出該頁面重新進入,又可以重新點擊下單了
2. 設計思路
A.前端生成一個requestId,規則:時間戳+arcId,存放到SessionStorage中。
B.后端存到redis中string中,也是利用自增api,判斷值是否大於1,但要設置一個過期時間,否則就一直在redis中了。
C.前端頁面:點擊變灰,拿到返回結果后 或者 5s后才可以繼續點擊。
PS:前端的頁面業務和效果在后續業務中完善,這里單純優化接口!!!
3.分析
方法冪等是防錯的一種措施,防止網絡延遲或用戶誤操作多次下單出錯的問題
4.壓測結果
要求:1個requestId只能生成一條訂單記錄
代碼分享:

/// <summary> ///07-方法冪等 /// </summary> /// <param name="userId">用戶編號</param> /// <param name="arcId">商品編號</param> /// <param name="totalPrice">訂單總額</param> /// <param name="requestId">請求ID</param> /// <param name="goodNum">用戶購買的商品數量</param> /// <returns></returns> public string POrder7(string userId, string arcId, string totalPrice, string requestId = "125643", int goodNum = 1) { try { //一. 業務完善優化 //1. 單品限流-同上 //2. 限制用戶購買數量-同上 //3. 方法冪等-防止網絡延遲多次提交問題 //(也可以考慮存hash,把訂單號也存進去,回頭改造, 但是HashIncrement沒法把value也存進去) var orderNum = Guid.NewGuid().ToString("N"); int requestIdNum = (int)_redisDb.StringIncrement(requestId, 1); if (requestIdNum == 1) { //僅第一次進來的時候設置過期時間,用於定期刪除 _redisDb.KeyExpire(requestId, TimeSpan.FromMinutes(10)); } else if (requestIdNum > 1) { throw new Exception($"您已經下過單了,不能重復下單"); } else { throw new Exception($"其它異常。。。。"); } //二. 邏輯優化 //1. 直接自減1 int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1); if (iCount >= 0) { //2. 將下單信息存到消息隊列中 _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); } }
測試:模擬同一個用戶發送100個請求,異常率為99%,說明該用戶只生成了一條訂單記錄
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。