京东面试官问哭的秒杀系统考点

tech2025-02-08  35

什么是秒杀系统?

  秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,就是秒杀开始的时候才能对这个商品进行购买,在没有开始之前是不能购买的(购买按钮隐藏或置灰),会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。

  综上所述,可以总结出秒杀系统场景的几个特点:

      秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。

      秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。

    而且大部分用户越临近秒杀时间刷新的频率越快,这么多的请求服务器怎么承受呢?

01

秒杀架构设计

限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。

削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。

可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。

  PS:关于限流之前的文章有过介绍哦,小伙伴可以去参阅。传送门

02

架构原则

争取做到4要1不要

1.数据要尽量少:所谓“数据要尽量少”,就是指用户请求的数据能少则少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。因为这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器来处理,而服务器在写网络时都要做压缩和编码,这些都非常消耗CPU,所以减少传输的数据量可以显著的减少CPU的使用。例如,我们可以简化秒杀页面的大小,去掉不必要的页面装修效果等。其次,“数据要尽量少”还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务和数据库打交道的。调用其他服务会涉及到数据的序列化和反序列化,这也是CPU的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,越简单越好。

2.请求数要尽量少:用户请求的页面返回后,浏览器渲染页面还包含其他的额外请求,比如说这个页面依赖的CSS/Javascript,图片以及Ajax请求等,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,另外不同请求访问的域名不一样,还需要做DNS域名解析,可能会耗时更久。

3.路径要尽量短:所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。

4.依赖要尽量少:所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。

5.不要有单点:系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。

03

流量削峰

排队:要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。在这里,消息队列就像“水库”一样, 拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

答题:防止用户作弊,延缓用户请求。

分层过滤:分层校验的目的是:在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。

04

异步处理

  我们大家想一想,即使我们做了上面的限流机制,卡住了绝大部分请求到业务系统中,但还是有很多请求进入了业务系统。如果我们商品库存数为5000个库存,在大流量的时候,会有很多用户都有资格去下单,那也就是同时会有5000个并发去操作mysql数据库。那数据库也是抗不住的,一般mysql的并发量500左右。

  这个时候我们需要做一些异步处理,让5000个下单请求进入消息队列,让订单消费服务去慢慢处理5000个请求,这样就有效的把并发同步请求,改为了串行异步。

  当然改成了异步处理,前端在下单的时候,就没法立刻得到下单的结果,所以前端要做一个【抢购中。。。】的状态页面,在此页面中定时轮询下单结果。这样就大大提升了系统的吞吐量,降低了系统压力。

消息队列中间件有很多选择,一般选择RabbitMq,RocketMq。

05

多级缓存

使用场景:所有商品库存全部依赖数据,在高并发情况下,数据库死翘翘了...

秒杀商品一个用户只能抢购一次,一次只能抢购一个!

一级缓存:在程序启动完成后,将商品信息存入redis中

二级缓存:ConcurrentHashMap内记录商品是否售完标识;

大概思路:程序启动加载init代码块,将商品库存信息放入redis中,下单时直接使用redis decr;如果stock小于0,记录jvm缓存中商品售完标识;启动zk监听,同步多个jvm内存标识;

坑坑坑点:redis原子减会减成负数,如果有下单失败退单等操作,负数 +1还是负数,然鹅是有库存的!所以redis库存一定要还原或者改成0

商品信息加入redis中:

@PostConstruct

public voidinit()throws Exception{

Product productParam = new Product();

productParam.setSpecial(ProductType.PRODUCT_MIAOSHA);

List<Product> miaoshaProducts = productService.selectList(productParam);

for (Product product : miaoshaProducts) {

RedisUtil.set(RedisKeyPrefix.PRODUCT_STOCK + "_" + product.getId(), String.valueOf(product.getStock()));

}

}

减少库存 zk监听:

Long stock = RedisUtil.decr(RedisKeyPrefix.PRODUCT_STOCK + "_" + productId);

if (stock == null) {

return ReturnMessage.error("商品数据还未准备好");

}

if (stock < 0) {

RedisUtil.incr(RedisKeyPrefix.PRODUCT_STOCK + "_" + productId);

productSoldOutMap.put(productId, true);

//写zk的商品售完标记true

//判断父节点是否存在

if (zooKeeper.exists(ZookeeperPathPrefix.PRODUCT_SOLD_OUT, false) == null) {

//创建父节点

zooKeeper.create(ZookeeperPathPrefix.PRODUCT_SOLD_OUT, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

}

//判断节点内商品信息是否存在

if (zooKeeper.exists(ZookeeperPathPrefix.getZKSoldOutProductPath(productId), true) == null) {

//创建商品子节点

zooKeeper.create(ZookeeperPathPrefix.getZKSoldOutProductPath(productId), "true".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

}

if ("false".equals(new String(zooKeeper.getData(ZookeeperPathPrefix.getZKSoldOutProductPath(productId), true, new Stat())))) {

zooKeeper.setData(ZookeeperPathPrefix.getZKSoldOutProductPath(productId), "true".getBytes(), -1);

//监听zk售完标记节点

zooKeeper.exists(ZookeeperPathPrefix.getZKSoldOutProductPath(productId), true);

}

return ReturnMessage.error("商品已抢完");

<bean id="zooKeeperWatcher" class="com.jiagouedu.core.zk.ZooKeeperWatcher">

<constructor-arg name="zookeeperAddr"value="${zk.ip}" />

</bean>

    <bean id="zookeeper" class="org.apache.zookeeper.ZooKeeper" lazy-init="true">

<constructor-arg name="connectString" value="${zk.ip}" />

<constructor-arg name="sessionTimeout"value="5000" />

<constructor-arg name="watcher"ref="zooKeeperWatcher" />

</bean>

/**

 * zk更新缓存watcher

 */

public class ZooKeeperWatcher implements Watcher {

 

private static final Logger logger = LoggerFactory.getLogger(ZooKeeperWatcher.class);

private ZooKeeper zooKeeper;

public ZooKeeperWatcher(String zookeeperAddr) throws IOException {

super();

this.zooKeeper = new ZooKeeper(zookeeperAddr, 500, null);

}

 

@Override

public void process(WatchedEvent event) {

if (event.getType() == EventType.NodeDataChanged) { // zk目录节点数据变化通知事件

try {

String path = event.getPath();  //  /product_sold_out/10270

String soldOutFlag = new String(zooKeeper.getData(path, true, new Stat()));

logger.info("zookeeper数据节点修改变动,path={},value={}", path, soldOutFlag);

if ("false".equals(soldOutFlag)) {

String productId = path.substring(path.lastIndexOf("/")+1, path.length());

MiaoshaAction.getProductSoldOutMap().remove(productId); 

}

} catch (Exception e) {

logger.error("zookeeper数据节点修改回调事件异常", e);

          }

     }

  }

}

06

其他问题

服务单一职责:设计个能抗住高并发的系统,我觉得还是得单一职责。

什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式

也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。

单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。

至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节我会说的)

单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强行高可用)

Redis集群:秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!

资源静态化:秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

ps:上一篇文章也有提到过redis集群和Lua+redis哦,点我参阅

    秒杀系统的特点就是千万不要卖超,限量卖!如果买超了老板那里可能就不好交代了,这是前提。

    例如,我们平常购物都是这样,看到喜欢的商品然后下单,但并不是每个下单请求你都最后付款了。你说系统是用户下单了就算这个商品卖出去了,还是等到用户真正付款了才算卖出了呢?这的确是个问题!

最新回复(0)