秒杀系统设计总结

简介

秒杀从规模上可以分为以下两类:

  • 大秒:类似双十一,商品数量规模大,价格低,流量超大的活动。
  • 小秒:商家自己配置的一些时段类型的活动,由商家自己指定时间上架。

秒杀系统面对的问题

秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。

  • 高并发环境下的系统稳定性:如何保证系统在面对巨大的流量情况下,不被打崩?
    • 两个问题
      • 并发读
        • 核心优化理念:减少用户到服务端来读数据。或者让他们读取更少的数据。
      • 并发写
        • 核心优化理念:在数据库层面独立出来一个库,做特殊的处理。
    • 大流量会产生以下实际待解决问题
      • Redis缓存击穿/雪崩/穿透等问题
      • 关系型数据库性能问题,锁竞争对性能的消耗
  • 保证数据的最终一致性:库存不能超卖。
  • 大数据分析功能:分析本次秒杀活动的商业效益。
  • 需要有一个兜底方案,以防最坏的情况发生。

设计

架构原则: “4要1不要”

架构是一种平衡的艺术,最好的架构一旦脱离了它所适应的场景,一切都将是空谈。

  • 数据要尽量少

    • 用户请求的数据能少就少,包括上传给系统的数据和系统返回给用户的数据。
  • HTTP请求数尽量少

    • 合并CSS,JS文件
  • 路径要尽量短

    • 用户发出请求到返回数据的过程中,经过的节点要尽量短
      • 通常,每经过一个节点,都会产生一个新的Socket连接。
    • 会减少时延
    • 可以选择将多个相互强依赖的引用部署在一起,将RPC变成JVM内部的方法调用
  • 依赖要尽量少

    • 完成一次用户请求必须依赖的系统或服务要少(指的是强依赖)
      • 比如展示秒杀页面,它强依赖商品信息,用户信息,但是优惠券,成交列表等非必要模块是可以在紧急情况下去掉。
    • 对系统模块进行分级,0级,1级,2级等
  • 不要有单点

    • 系统中的单点是系统架构上的一个大忌,单点意味着没有备份,风险不可控。
      • 如何避免单点?
        • 避免将服务的状态和机器绑定(服务无状态化)
          • 把服务的配置动态化(使用配置中心Nacos等)
          • 存储服务不好实现,因为数据持久化存储在机器的磁盘里面。文件存储可以通过冗余多个备份的方式来解决单点问题。

流量过滤

本质:逐级过滤掉无效的流量。基本有以下一些解决方案:

  • 活动开始前前端页面的Button无法点击,防止活动尚未开始时,用户进行点击产生流量。
    • 同时后端需要做相关校验。避免用户直接请求秒杀接口。
    • 秒杀url实现动态化,可以选择进行md5加密随机字符串,然后通过另一个接口校验秒杀接口的合法性
  • 错峰:前端添加验证码或者答题,防止瞬间产生超高的流量,增加题目辨别难度,避免以图像识别等技术进行破解。
  • 校验:对参与活动的用户进行校验拦截。主要从以下几个方面进行判断
    • 用户白名单
    • 用户终端校验:对用户终端类型进行判断
    • IP、MAC、ID校验
    • 参与次数校验:避免多次参与活动
    • 用户黑名单:避免羊毛党等
  • 限流:通过接口限流策略判断请求是否放行
    • 令牌桶算法

性能优化

前面的流量过滤基本过滤掉大部分流量,但是系统性能还需进行优化,主要有以下的解决方案:

  • 动静分离
  • 活动预热:将参加活动的商品独立出来,不和普通的商品库存共享服务,提前将数据缓存到Redis,查询全部走缓存,扣减库存视情况而定。
  • 选择Nginx
  • 采用微服务架构部署,提高部署量,均摊请求。
  • 秒杀是典型的读多写少的场景,考虑到单体redis的性能问题,可以考虑:
    • Redis集群
    • 主从同步
    • 读写分离
    • 如果使用Redis集群,同时需要考虑保证多节点的数据一致性
  • 异步处理:采用消息队列
    • 异步,削峰,解耦

动静分离

“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据

简而言之,静态数据是能不经过后端请求,直接输出给用户的数据,

如何对静态数据做缓存?

  • 把静态数据缓存到离用户最近的地方。
    • 用户浏览器
    • CDN
      • 失效问题
      • 命中率问题
      • 发布更新问题
    • 服务器端Cache
  • 静态化改造就是直接缓存HTTP连接。
    • 静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据。Web代理服务器直接根据请求URL,取出对应的HTTP响应头和响应体然后直接返回。
  • 选择缓存性能优秀的工具进行缓存服务,可以在Web服务器层面进行缓存,例如Nginx,Apache。

如何做动静分离的改造?

  • URL唯一化,每个商品由ID来标识。item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。
    • URL唯一可以根据id作为key,查询之前缓存的HTTP连接。
  • 分离浏览者相关的因素。登录状态可以通过请求获取
    • JWT
  • 分离时间因素。服务端输出的时间也通过请求获取。
  • 异步化地域因素:详情页面上的地域信息做成异步方式获取。
  • 去掉Cookie,可以通过Web服务器删除服务器输出的页面中的Cookie。

动态数据的处理方式

  • ESI方案:在Web代理服务器上做动态内容请求,并将请求插入到静态页面中。当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
  • CSI方案:即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

解决超卖

一般情况下,减库存有以下方式:

  • 下单减库存:是最简单的方式,也控制的最精准。但是存在恶意锁单等问题。
  • 付款减库存:大并发的情况下,可能存在买家下单后无法付款的问题。
  • 预扣库存:买家下单后,库存为其保留一定时间,超时未付款自动释放库存

如果扣库存逻辑较为简单,比如没有复杂的SKU库存和总库存这种联动关系的话,可以选择在Redis完成减库存。Redis定时更新到MySQL

复杂秒杀场景选择的方式:

  1. 首先查询Redis缓存库存是否充足
  2. 先扣库存再落订单数据,以防订单生成没有了库存的超卖问题
  3. 扣库存先扣数据库,再扣Redis,并且两个操作需要在同一个事务中,一个执行失败全部回滚。
    • 数据库MySQL更新可以采取乐观锁方式,添加version字段进行处理,性能较悲观锁高。

上述方案能一定程度解决问题,但是如果大量请求线程落在同一条库存记录上去update时,会造成InnoDB行锁的竞争问题,而并发度越高等待的线程会越多,TPS(Transaction Per Second)会下降,相应时间上升,数据库的吞吐量会严重受影响。

这时,需要做数据库层面的优化。

质量保障

  • 熔断限流降级
  • 监控:QPS监控,容器监控,CPU监控,IO监控等
  • 提前压测

数据统计

  • 埋点,检测
  • 数据大盘,通过数据库数据配合监控系统(首选)
  • 离线数据分析

秒杀系统设计总结
https://l1n.wang/2020/系统设计/seckill-summary/
作者
Lin Wang
发布于
2020年11月9日
许可协议