如何设计一个秒杀系统?

87

如何设计一个秒杀系统?

  • 最近不知道咋了,碰到了一些执着于Java后端开发的人,它们执着与Java开发庞大的生态,不会以为三高的枯燥而失去耐心,甚至愿意静下心来去阅读一个个框架的源码。不免让我想到大一时沉迷造轮子的自己。。。
  • 😮‍💨,感觉好多东西都忘了,这里以一个老掉牙的秒杀系统稍微回顾一下吧。我可以不写代码,但我不能不知道怎么写。
  • 下面就从三高及其他一些角度出发吧,当然只能涉及大概,具体的细节还有很多,都是可以深究的。正文:

高并发&高性能

  • 概念:系统能够同时并行处理很多请求。
  • 指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

热点数据处理

What & Why 热点数据

  • 是什么?:指一段时间内被大量访问的数据。比如爆款商品,优惠卷或者是热点新闻等。
  • 为什么要关注热点数据?:热点数据的访问量占比可能比其他数据还多,如果我将热点数据与其他数据做相同的处理,可能导致系统大量资源去处理热点数据,而其他数据没有照顾到,导致资源浪费。
  • 分类?:
    • 静态热点数据:可以提前预测到的一些热点数据。
    • 动态热点数据:不能提前预测到的一些热点数据,需要通过一些手段动态检测系统运行产生。

How To do 热点数据

  • 检测热点数据:
  • 如何处理热点数据:
    • 热点数据一定要放在缓存里,最好在JVM里就做处理了(多级缓存),并设置一个过期时间。
    • 当然写入到JVM中的热点数据不应过多,避免OOM,同时以防万一,设置淘汰策略。
    • 为什么写到JVM中最好?没有网络开销,快!

流量削峰

消息队列(学了不用?)

  • 截屏2022-11-15 11.54.39

验证

  • 可以在用户访问前加点验证方式,至少防御一下脚本小子,视情况而定吧

高可用

  • 概念:系统无中断地执行其功能的能力,代表系统的可用性程度。

集群化(拿Redis举例)

  • 这个我必点,但是说不完,如果只是为了设计秒杀的话,最优先考虑的就应该是redis集群。
  • Redis replication(异步复制):一主多从,slave的多少决定你的吞吐量。
  • 关于master宕机:通过Sentinel(哨兵解决),Redis Sentinel是官方推荐的。
    • Sentinel是一个运行模式,主要作用是对Redis运行节点进行监控。当master出现故障时,会自动帮我们处理故障转移,不需要人工介入。
    • Sentinel也是一个Redis进程,不对外提供读写服务,通常哨兵要配置成单数(不然票数相同我怎么选)。

限流

  • 概念:限流就是从用户访问压力角度来考虑如何应对系统故障。对服务端的接口接受请求的频率进行限制,防止服务挂掉。

  • 🌰:我的接口一秒之内处理10w个请求,结果你来了15w,我只能把其他5w拦下来了,不然我服务要G。

  • 实现方式:

    • 直接用Redis来做(基于Lua脚本)
    • Hystrix:老一代springcloud了,Netflix开源
    • Sentinel(推荐):阿里开源,以流量为切入点,而且其他功能也有,发展很好,社区也活跃,🐮的截屏2022-11-15 12.16.40

排队

  • 可以理解为变种限流,当流量达到阈值时,不直接拒绝用户访问,等用户排队等待之后进行处理。

降级

  • 概念:从系统功能优先级角度考虑如何面对系统故障。
  • 🌰:当请求量达到阈值时,我直接关闭系统中的一些非核心的功能,或让它们功能降级。这样就有更多资源腾给秒杀系统。
  • 核心思想:丢车保帅

熔断

  • 概念:熔断与降级是比较容易混淆的二个概念,降级目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障
  • 🌰:比如我的秒杀业务在服务A上,由于秒杀流量过大原因,会导致我服务A上的其他功能无法正常处理,此时如果我有一个服务B需要调用我的服务A上的其他功能,我应该熔断它,从而避免服务B一直请求服务A的这个接口导致服务拖慢卡死。

一致性

关于库存

  • 主要关注减库存方案,可分为二种:

    • 下单即减库存
    • 付款再减库存:不过可能会存在下单成功,付款失败的情况。

    一般是下单即减库存为主,当然也可以对业务逻辑优化,对超时未付款的订单进行取消,释放库存。

  • 防止超卖---代码层面(以redis为例)

    • 伪代码

      // 1.检查库存是否充足
      Integer stockNum = (Integer) redisTemplate.get(key);
      if (stockNum < 1) {
        ......
      }
      // 2.如果库存充足,减少库存
      Long count = redisTemplate.increment(key,-1);
      if (count >= 0) {
        ......
      } else {
      	......
      }
      
    • Lua脚本方式:可以减少多个命令的网络开销保证多个命令整体的原子性

      -- 1.检查库存是否充足
      local stockNum = tonumber(redis.call("get",key));
      if stockNum < 1 then
        return 0;
      -- 2.如果库存充足,减少库存
      else 
        redis.call("DECRBY",1);
        return 1;
      end
      

接口幂

  • 概念:在分布式系统中,幂等(idempotency)是对请求操作结果的一个描述,这个描述就是不论执行多少次相同的请求,产生的效果和返回的结果和只发一次是一样的。
  • 🌰:有些东西只能买一份,你肯定不能让它买二次吧,及时它请求你的接口。
  • 前端处理:灰掉按钮等。
  • 后端处理:
    • 同步锁
    • 分布式锁:推荐redisson,老生长谈,推荐阅读
    • 业务字段的唯一索性约束,防止重复数据产生

性能测试

  • Jmeter、LoadRunner、Galting、Apache Bench