一. 单品限流
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个请求,同时库存扣减和订单创建也正确。
二. 购买商品限制
每位用户在秒杀期间对某商品只能购买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件
三. 方法幂等
用户在下单页面,假设网络延迟多次点击按钮,服务端仅处理第一次请求(第一次成功则成功,失败则失败),退出该页面重新进入,又可以重新点击下单了
A.前端生成一个requestId,规则:时间戳+arcId,存放到SessionStorage中。
B.后端存到redis中string中,也是利用自增api,判断值是否大于1,但要设置一个过期时间,否则就一直在redis中了。
C.前端页面:点击变灰,拿到返回结果后 或者 5s后才可以继续点击。
PS:前端的页面业务和效果在后续业务中完善,这里单纯优化接口!!!
方法幂等是防错的一种措施,防止网络延迟或用户误操作多次下单出错的问题
要求: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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。