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