秒杀系统

1、瞬时高并发

   在秒杀时间点(比如:12点)前几分钟,用户并发量才真正突增,达到秒杀时间点时,并发量会达到顶峰。正常情况下,大部分用户会收到商品已经抢完的提醒,收到该提醒后,大概率不会在那个活动页面停留,如此一来,用户并发量又会急剧下降。所以这个峰值持续的时间其实是非常短的。

  从以下几个方面入手:

  • 页面静态化
  • CDN加速
  • 缓存
  • mq异步处理
  • 限流
  • 分布式锁

2、页面静态化

  活动页面是用户流量的第一入口,所以是并发量最大的地方。活动页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,通常情况下,会对活动页面做静态化处理。用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。

  只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。如何才能让用户最快访问到活动页面呢?

       需要使用CDN,全称是Content Delivery Network,即内容分发网络。 使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

3、读多写少

  在秒杀的过程中,系统一般会先查一下库存是否足够,如果足够才允许下单,写数据库。如果不够,则直接返回该商品已经抢完。

  由于大量用户抢少量商品,只有极少部分用户能够抢成功,所以绝大部分用户在秒杀时,库存其实是不足的,系统会直接返回该商品已经抢完。应该改用缓存,比如:redis

  

4、缓存问题

  需要在redis中保存商品信息,里面包含:商品id、商品名称、规格属性、库存等信息,同时数据库中也要有相关信息,毕竟缓存并不完全可靠。用户在点击秒杀按钮,请求秒杀接口的过程中,需要传入的商品id参数,然后服务端需要校验该商品是否合法。

 

缓存击穿

  商品A第一次秒杀时,缓存中是没有数据的,但数据库中有。如果从数据库中查到数据,则放入缓存的逻辑。在高并发下,同一时刻会有大量的请求,都在秒杀同一件商品,这些请求同时去查缓存中没有数据,然后又同时访问数据库,数据库可能扛不住压力,直接挂掉。需要加锁,最好使用分布式锁

   

 

 

        最好在项目启动之前,先把缓存进行预热。即事先把所有的商品,同步到缓存中,这样商品基本都能直接从缓存中获取到,就不会出现缓存击穿的问题。是不是上面加锁这一步可以不需要了?如果缓存中设置的过期时间不对,缓存提前过期了,或者缓存被不小心删除,如果不加锁同样可能出现缓存击穿。

缓存穿透

   如果有大量的请求传入的商品id,在缓存中和数据库中都不存在,这些请求不就每次都会穿透过缓存,而直接访问数据库。布隆过滤器绝大部分使用在缓存数据更新很少的场景中。就需要把不存在的商品id也缓存起来

库存问题

  预扣库存:真正的秒杀商品的场景,不是说扣完库存,就完事了,如果用户在一段时间内,还没完成支付,扣减的库存是要加回去的。特别注意的是库存不足和库存超卖问题

  

 

 

 数据库扣减库存

update product set stock=stock-1 where id=123;

  如何控制库存不足的情况下,不让用户操作呢?

int stock = mapper.getStockById(123);
if(stock > 0) {
  int count = mapper.updateStock(123);
  if(count > 0) {
    addOrder(123);
  }
}

  查询操作和更新操作不是原子性的,会导致在并发的场景下,出现库存超卖的情况。使用synchronized关键字,性能不够好。基于数据库的乐观锁,会少一次数据库查询,而且能够天然的保证数据操作的原子性。

update product set stock=stock-1 where id=product and stock > 0;

  stock > 0,就能保证不会出现超卖的情况

  在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题。

redis扣减库存

boolean exist = redisClient.query(productId,userId);
  if(exist) {
    return -1;
  }
  int stock = redisClient.queryStock(productId);
  if(stock <=0) {
    return 0;
  }
  redisClient.incrby(productId, -1);
  redisClient.add(productId,userId);
return 1;

  如果在高并发下,有多个请求同时查询库存,当时都大于0。由于查询库存和更新库存非原则操作,则会出现库存为负数的情况,即库存超卖。代码优化如下:

boolean exist = redisClient.query(productId,userId);
if(exist) {
  return -1;
}
if(redisClient.incrby(productId, -1)<0) {
  return 0;
}
redisClient.add(productId,userId);
return 1;

  如果在高并发场景中,有多个请求同时扣减库存,大多数请求的incrby操作之后,结果都会小于0。

  库存出现负数,不会出现超卖的问题。但由于这里是预减库存,如果负数值负的太多的话,后面万一要回退库存时,就会导致库存不准。

lua脚本扣减库存

  该代码的主要流程如下:

1)先判断商品id是否存在,如果不存在则直接返回。
2)获取该商品id的库存,判断库存如果是-1,则直接返回,表示不限制库存。
3)如果库存大于0,则扣减库存。
4)如果库存等于0,是直接返回,表示库存不足。

mq异步处理

  在真实的秒杀场景中,有三个核心流程:

  

 

 

   真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。

1、消息丢失问题

  秒杀成功了,往mq发送下单消息的时候,有可能会失败。原因有很多,比如:网络问题、broker挂了、mq服务端磁盘问题等。这些情况,都可能会造成消息丢失。那么,如何防止消息丢失呢?

  答:加一张消息发送表。

  

 

 

        在生产者发送mq消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。如果生产者把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了,造成了消息丢失。使用job,增加重试机制。

  

 

 

   用job每隔一段时间去查询消息发送表中状态为待处理的数据,然后重新发送mq消息。

重复消费问题

  本来消费者消费消息时,在ack应答的时候,如果网络超时,本身就可能会消费重复的消息。但由于消息发送者增加了重试机制,会导致消费者重复消息的概率增大。那么,如何解决重复消息问题呢?

  答:加一张消息处理表。

  

 

 

   消费者读到消息之后,先判断一下消息处理表,是否存在该消息,如果存在,表示是重复消费,则直接返回。如果不存在,则进行下单操作,接着将该消息写入消息处理表中,再返回。

  有个比较关键的点是:下单和写消息处理表,要放在同一个事务中,保证原子操作。

垃圾消息问题

   出现消息消费失败的情况。比如:由于某些原因,消息消费者下单一直失败,一直不能回调状态变更接口,这样job会不停的重试发消息。最后,会产生大量的垃圾消息。
   

 延迟消费问题

  

 

   下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。

如何限流

  1、对同一用户限流

  为了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。

  

   2、对同一ip限流

  只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了,需要加同一ip限流功能。这种限流方式可能会有误杀的情况,比如同一个公司或网吧的出口ip是相同的,如果里面有多个正常用户同时发起请求,有些用户可能会被限制住。

  3、对接口限流

  限制请求的接口总次数,可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口。看起来有点得不偿失。

  4、加验证码

  加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况。

  用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。

  此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。

秒杀系统数据库设计

  秒杀有把服务器击垮的风险,如果让它与我们的其他业务使用在同一个数据库中,耦合在一起,就很有可能牵连和影响其他的业务。如何防止这类问题发生,就算秒杀发生了宕机、服务器卡死问题,也应该让他尽量不影响线上正常进行的业务

  应该单独设计一个秒杀数据库,防止因为秒杀活动的高并发访问拖垮整个网站。这里只需要两张表,一张是秒杀订单表,一张是秒杀货品表
  

 

    

 

 

 秒杀url的设计

  为了避免有程序访问经验的人通过下单页面url直接访问后台接口来秒杀货品,我们需要将秒杀的url实现动态化,即使是开发整个系统的人都无法在秒杀开始前知道秒杀的url。具体的做法就是通过md5加密一串随机字符作为秒杀的url,然后前端访问后台获取具体的url,后台校验通过之后才可以继续秒杀。

服务降级

  假如在秒杀过程中出现了某个服务器宕机,或者服务不可用

 

 

        

  

posted on 2022-06-05 20:33  溪水静幽  阅读(241)  评论(0)    收藏  举报