秒杀在电商中是一个很常见的场景。所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式 从技术角度出发,秒杀是指在短时间内,大量用户并发请求去争夺有限数量的产品。
一.考虑的问题
对于秒杀,设计时要考虑哪些问题呢?
1. 超卖
何谓超卖?即指卖出的商品超出了设定的库存。秒杀场景下,商品的价格一般是特别低的,本身就是亏本,吸引客户。一旦出现超卖,会造成大量经济损失。所以超卖可谓第一个要注意点。
2.高并发
秒杀都是数秒内出现大量请求,如果这些请求都到数据库,很容易造成奔溃。故而如何设计避免出现这种状态很重要。
3.接口防刷
接口防刷主要指有大量恶意请求秒杀接口,导致服务器反应不过来。这里包括脚本去抢秒杀商品或者进行本身恶意的攻击。
4. 数据库设计
数据库设计指发生秒杀业务时,影响到了其他正常的数据库业务。
二.秒杀设计
对于秒杀请求,类似于漏斗处理,层层过滤,最终实现少量数据入库的过程。
1. 首先是分流
在现实中,硬件设备是有限的,单个服务器所能处理的请求也是有限的,故而分流是首先要做的。这里的分流采用冷热分离的思想,主要包括如下几点:
- 页面数据缓存:
- 参与秒杀商品信息是固定的,可以预加载数据到缓存中
- 对于静态的页面资源例如html,图片等可通过CDN进行加速
这部分操作主要是为了是加快’冷’数据的响应,减少客户等待时间
- 动态化秒杀地址
这部分操作是为了避免提前暴露秒杀地址。如果是一个固定的地址,只要稍微懂些技术的人员就可以找到地址,提前进行发起请求。当然在点击秒杀按钮之后,要对按钮进行置灰操作,避免客户频繁点击,这个很简单但很实用。
示例代码如下:
public String getSkillUrl(Long activityId) {
ValueOperations ops = redisTemplate.opsForValue();
String raw = (String) ops.get(String.format(ACTIVITY_KEY, activityId));
if (!StringUtils.hasText(raw)) {
return "";
}
ActivityDO activityDO = JSON.parseObject(raw, ActivityDO.class);
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activityDO.getStartTime()) || now.isAfter(activityDO.getEndTime())) {
log.info("活动还未开始");
return "";
}
String url = SecretUtil.md5Encode(String.valueOf(activityDO.getGoodsId()), SKILL_SALT);
ops.set(String.format(STOCK_URL_KEY,url),"1",60, TimeUnit.SECONDS);
return url;
}
虽然动态秒杀地址避免了直接泄露,但是这个还是无法避免脚本的攻击 3. nginx进行分流 这层不仅仅进行请求的分流,也可以对一些ip进行限制,一定程度减少攻击。 4. 服务单一化 单独分离处秒杀服务进行部署,避免因为秒杀导致其他服务访问出现问题。
2. 接着是限流
限流操作时为了保证流量始终在服务器能接受的合理范围,超出可以执行一些熔断,风控策略。当然这一层是处理接口防刷最关键的地方。
- 网关
网关这层既可以做到流量的分发,限制,还可以进行服务降级保护的操作。 - 限流中间件
限流中间件分布式场景下,Sentinel还是很好用的,基本能满足常见场景。对于单机场景可以使用Guava工具包RateLimiter工具类。
3. 然后是秒杀校验
一般到这一步,并发数已处于一个合理的范围了主要做一些基础校验,然后把秒杀到的信息发给MQ。这里处理缓存库存时,要获取全局锁,这一步主要是防止超卖。 大致需要的校验如下:
- 校验动态地址获取的凭证
- 活动时间是否结束
- 库存数是否足够
- 其他额外的业务校验
示例代码如下:
public Boolean skill(String url,Long goodsId) {
if (!redisTemplate.hasKey(String.format(STOCK_URL_KEY, url))) {
return Boolean.FALSE;
}
RLock lock = redissonClient.getLock(String.format("skill:lock:%d", goodsId));
ValueOperations ops = redisTemplate.opsForValue();
String key = String.format(STOCK_KEY, goodsId);
if (!lock.tryLock()) {
return Boolean.FALSE;
}
try {
Integer num = (Integer) ops.get(key);
if (num <= 0) {
return false;
}
ops.decrement(key);
}finally {
lock.unlock();
}
Long userId = (long) new Random().nextInt(1000);
messageService.sendSkillInfo(new MsgDTO(goodsId,userId));
return Boolean.TRUE;
}
4. 最后是MQ以及数据落库
- MQ方面
要注意消息的持久化避免消息丢失,另外要考虑消费者的处理能力,避免消息积压过多。通常MQ都采用的推模式,有消息就会推给消费者,如果消息过多,消费者的压力会很大,那时可以考虑采用拉模式,定量获取MQ的数据。采用拉模式就得靠考虑时效问题了,每次拉多少,隔多久拉一次。 - 消费者
到这里具体处理消息,生成预订单,扣减库存。这里要考虑超时之后库存的恢复。
三.总结
本文主要对秒杀进行回顾,设计核心还是超卖和高并发,动静分离处理是关键思想。 后续有新思路继续补充。示例demo地址
|