第三节:抢单流程优化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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2020-10-14 20:51  Yaopengfei  阅读(2098)  评论(1编辑  收藏  举报