本文适读人群:
对电子商务中货架、下单模块流程有所了解,具备基本的订单数据模型和接口设计相关概念。本文重点以秒杀中下单流程异步优化分析。货架优化方案、限流方案、库存校验优化、库存分割优化方案在之后的文章中给大家分享。
秒杀的背景:
“秒杀”原是电脑游戏中的名词,现已延伸到网络购物,指网络卖家发布一些超低价格的商品,让所有买家在同一时间通过网络进行抢购的一种促销方式。由于商品性价高,往往活动一开始就被抢购一空,所需时间甚至以秒计算。

商家举办秒杀的目的:
网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销、引流等目的组织的网上限时抢购活动。所以直接目的就是吸睛!吸睛!吸睛!赔本也要卖吆喝。
应用场景的特点:
业务特点
- 秒杀的形式
主要分为低价、限时、限量
- 低价:以超低的价格进行销售。比如一元抢购活动
- 限时:规定时间范围内进行抢购。比如小米手机抢购。
- 限量:固定的数量进行销售。
三个活动形式可以独立存在,也可以多维度组合存在。
- 持续时间短: 瞬间售空,售罄时间以秒计算。
- 定时上架:活动开始前期就展示对应活动,单前期活动未开始,大肆广告宣传吸引用户。

- 流程短:普通购物车可以多品购买,也会有购物车查询页面展示商品信息,秒杀通常为单品购买。

技术特点
- 顺势并发量高:大量用户会在同一时间进行抢购。突刺现象。
- 读多写少:抢购前大量用户访问详情页。抢购时,访问量远远大于库存数,绝大多数请求是对库存数进行校验。
- 流程更短:没有购物车选择流程。API设计为加入购入车、结算操作需要合并处理。

下单流程分析
业界秒杀产品设计参考
1、货架详情页部分:

秒杀的详情页在发起抢购前需要用户先进行登录。
早期绝大多数电商平台秒杀入口都增加了验证码。因为最求用户体验的问题,现在验证码越来越少了。但是并不是说就没有验证码了。在一些电商平台中,是否需要验证码是会根据风控系统对当前用户进行分析,如果是风险用户则限时验证码。
2、发起抢购
发起抢购显示出排队界面,排队时间15秒到30秒不等。
像12306、小米等网站都有对应设计。

秒杀失败

服务端下单接口设计分析:
对于一个秒杀加购物车的业务操作我设计包含两部分
- 数据有效性校验:活动状态是否有效、活动库存是否有效、用户是否黑名单、真实商品库存数校验。
- 扣减秒杀库存
结算页、提交订单和普通下单流程一致。
如果按照同步设计思路下单流程基本如下:

思路分析:
加购的环节是秒杀活动的峰值触发点。购物车服务需要承载超高并发。即使后端把活动信息、活动库存等输入放入缓存,针对于流量的突刺现象,应用层需要大量的资源去接收处理客户端链接的请求,在网卡、CPU这块也会遇到性能瓶颈。但作为企业来讲,如果因为应对突刺现象,通过扩容的方式去解决,显然需要巨大的成本。
这个时候我们就需要思考,当前这种业务场景,我们应该以什么样的方式去应对。首页我们明确一个观点,秒杀应用场景我们最求什么,用户体验?响应速度?系统稳定性?
那答案肯定是系统稳定性,如果失去了系统稳定性,用户体验、响应速度基本上属于唇亡齿寒。
在产品参考环节,排队的一个实现,如果前端是在请求加购物车接口后同步等待服务端响应,那按排队时间15s 、30s不等思考,长时间的占用链接势必会拖垮服务端。
结合现实中的饭店在饭店应对流量高峰例子:排队。
也就是通过MQ队列异步削峰的方式将用户是否进入队列和后台加购物车一系列有效性校验和扣减活动库存数业务流程解耦,这样就可以对系统的资源进行控制。
我的设计思路:

1、用户点击抢购(排队):
主要的流量入口,对于削峰的处理,一定要越靠前越好。
验证验证码;
校验队列长度(队列长度不大于当前活动数,这里我使用Redis队列),
队列长度校验通过,发送mq消息,
记录当前用户已在待处理列表。
核心代码演示:

2、异步消息监听:
此处的监听可以减少任务处理线程数,区别于同步的设计方案,这块就是重点的对服务器资源进行控制。保证更多的资源分配到排队接口。
/**
* 监听到消息后处理方法
*/
public void handle(SecKillRequestMessage message) {
// 查看请求用户是否在黑名单中(黑名单用户为通过拦截器计算超过访问频次的用户) rpc
if (userBlackListCache.isIn(message.getUserId())) {
logger.error("黑名单用户");
return;
}
// todo ant 实际业务中可对接分控系统检测当前用户是否存在风险
// 先看抢购是否已经结束了
if (secKillFinishCache.isFinish(message.getPromId())) {
logger.error("抱歉,您来晚了,抢购已经结束了");
return;
}
//todo 活动 时间判断
RushBuyItem rushBuyItem = rushBuySearchService.getRushBuyItem(Long.parseLong(message.getPromId()));
if (rushBuyItem == null) {
logger.error("查询秒杀信息异常");
return;
}
//检查实际库存 假预留
ItemDto itemById = itemService.getItemById(rushBuyItem.getProductId());
if (itemById == null) {
logger.error("商品信息异常");
return;
} else if (itemById.getNum() <= 0) {
logger.error("商品库存不足");
return;
}
// 先减redis库存
if (!secKillStockCache.decrStore(message.getPromId())) {
// 减库存失败
throw new YmshopException("占redis名额失败,等待重试");
}
// 减库存成功:生成下单token,并存入redis供前端获取
String token = secKillSuccessTokenCache.genToken(message.getUserId(), message.getPromId());
logger.info(MessageFormat.format("SecKillRequestHandler handle " +
"userId:{0} genToken ()", new String[]{message.getUserId(), token}));
}
通过redis 原子递减操作校验活动库存数,校验通过,生成排队成功token。意味着当前用户获得了秒杀资格。
3、查询秒杀令牌接口
根据当前用户和当前活动查询是否存在对应的token。
前端在请求排队接口后以短轮询的方式请求该接口。
4、添加秒杀购物车商品
前端需要把第三步查询到的token传入。
校验token通过,执行商品添加逻辑和去结算逻辑。前端跳转结算页。
商品价格取秒杀活动价格。
5、提交订单
这一步流量已经第一步进行控制,提交到底以同步方式和异步方式取决于自己之前的系统设计。
删除当前用户的下单token
6、token过期监控
token过期回滚秒杀活动数
压测分析:
对于该场景的压测,我选择短时间高并发的压测,观察TPS和cpu占用情况。
为什么选择短时间压测。对于秒杀来说库存有限,如果采样器执行次数远远大于库存数,那超出库存数的大量请求都在有效性校验这块第一时间拦截,没有太大意义了。
压测时间:60秒
并发数:50
库存3000
用户数:2w
同步接口测试
TPS 323/sec

异步接口测试
TPS 548/sec

cpu占用率对比

从下单逻辑接口方案来比对异步方案明显高于同步方案。cpu使用率略高于同步方案。
库存分割存储方案优化:
在我们上述的内容中,虽然购物车流程极大的提高了吞吐量,但是我们的秒杀库存是根据活动ID构建的redis存储key,那么当前这个秒杀活动的数据就只会存在一个Redis实例中。如果在实际秒杀应用中,那么我们发起秒杀的接口在校验当前秒杀活动库存是否有效,或者业务逻辑都会将请求打到单台redis实例。

按照redis的最大承载并发量我们算每秒平均6w。那么在大型秒杀平台,每秒并发量可能在几十万以上。那么一台Redis难以承载。

针对于这个问题,我使用了分割的思想,在库存存的时候将库存分成多份,以及建立虚拟秒杀key。

在发起秒杀接口,首先查询redis当前秒杀key是否存在映射的虚拟key。如果存在,则将对应数据记录到本地缓存,以及记录一个轮询计数。
之后每次接收到秒杀请求先去缓存中轮询计算出一个虚拟key。
这样就有效的将请求分散到不同的redis节点,实现了分而治之,也方便水平扩容。
核心代码如下:
后台配置

发起秒杀-查询映射key

轮询虚拟key

本文分享到此结束,如果有疑问,可以留言加关注。谢谢 。