第三節:搶單流程優化2(單品限流→購買數量限制→方法冪等)


一. 單品限流

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);
            }
        }
View Code

測試: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);
            }
        }
View Code

 測試:模擬同一個用戶發送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);
            }
        }
View Code

 測試:模擬同一個用戶發送100個請求,異常率為99%,說明該用戶只生成了一條訂單記錄

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM