如何设计一个秒杀系统

高并发系统的几大方向

1.请求数据尽量少,从而减少cpu消耗

2.访问路径尽量短,减少节点消耗

3.强依赖尽量少,减少加载时间

4.不要有单点,要有备份

5.减少额外请求,减少加载时间

设计技巧
10w级别可能瓶颈就在数据读取上,通过增加缓存一般就能解决

100w那么,可能服务端的网络可能都是瓶颈,所以要把大部分的静态数据放到cdn上甚至缓存在浏览器里

数据的动静分离

静态数据做缓存

第一,你应该把静态数据缓存到离用户最近的地方

第二,静态化改造就是要直接缓存 HTTP 连接

5 个方面来分离出动态内容

URL 唯一化

分离浏览者相关的因素

分离时间因素

异步化地域因素

去掉 Cookie

动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。

  • ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好

  • CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

动静分离的几种架构方案

实体机单机部署;

统一 Cache 层;

上 CDN。

有针对性地处理好系统的“热点数据”

为什么要关注热点

28定律 

什么是“热点”

  • 热点操作
读请求

写请求 
优化方法:存储层根据CAP理论做平衡
  • 热点数据
静态热点数据

能够提前预测的热点数据

动态热点数据

不能够提前预测的热点数据

发现热点数据

发现静态热点数据

报名

技术手段提前预测

发现动态热点数据

构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。

建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL

将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。

重点
(构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。, 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL, 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。)

处理热点数据

一是优化

缓存热点数据

LRU 淘汰算法

二是限制

例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。

三是隔离

业务隔离

把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。

系统隔离

系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。

数据隔离

秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01% 的数据有机会影响 99.99% 数据

隔离的其他办法

比如,你可以按照用户来区分,给不同的用户分配不同的 Cookie,在接入层,路由到不同的服务接口中;再比如,你还可以在接入层针对 URL 中的不同 Path 来设置限流策略。服务层调用不同的服务接口,以及数据层通过给数据打标来区分

流量削峰这事应该怎么做?

为什么要削峰

  • 平稳

  • 节省服务器的资源成本

方式方法?

排队

消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。

利用线程池加锁等待也是一种常用的排队方式;

先进先出、先进后出等常用的内存排队算法的实现方式;

把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)来恢复请求等方式。

答题

防止部分买家使用秒杀器在参加秒杀时作弊

延缓请求

题库生成模块

题库的推送模块

题目的图片生成模块

设计思路
(题库生成模块, 题库的推送模块, 题目的图片生成模块)

分层过滤

在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请

分层校验的基本原则是

将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;

对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;

对写数据进行基于时间的合理分片,过滤掉过期的失效请求;

对写请求做限流保护,将超出系统承载能力的请求过滤掉;

对写数据进行强一致性校验,只保留最后有效的数据。

业务手段

零点开启

优惠券

抽奖活动

总结

一个是通过队列来缓冲请求,即控制请求的发出;一个是通过答题来延长请求发出的时间,在请求发出后承接请求时进行控制,最后再对不符合条件的请求进行过滤;最后一种是对请求进行分层过滤。

队列缓冲能起到很好的削峰和缓冲作用。

答题更适用于秒杀或者营销活动等应用场景

分层过滤非常适合交易性的写请求

影响性能的因素有哪些?又该如何提高系统的性能?

影响性能的因素

QPS(Query Per Second,每秒请求数)

响应时间(Response Time,RT)

“总 QPS =(1000ms / 响应时间)× 线程数量”,一个是一次响应的服务端耗时,一个是处理请求的线程数。

响应时间和 QPS 有啥关系

真正对性能有影响的是 CPU 的执行时间

应该致力于减少 CPU 的执行时间

线程数对 QPS 的影响

设置什么样的线程数最合理呢

“线程数 = 2 * CPU 核数 + 1”

线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量

当然,最好的办法是通过性能测试来发现最佳的线程数。

如何发现瓶颈

缓存系统而言,制约它的是内存

对存储型系统来说 I/O 更容易是瓶颈。

秒杀,它的瓶颈更多地发生在 CPU 上

海量请求涌过来,你的页面又比较大,那么网络就有可能出现瓶颈。

如何发现CPU瓶颈?使用工具

JProfiler

Yourkit

jstack 定时地打印调用栈

怎样简单地判断 CPU 是不是瓶颈呢

一个办法就是看当 QPS 达到极限时,你的服务器的 CPU 使用率是不是超过了 95%,

如果没有超过,那么表示 CPU 还有提升的空间,要么是有锁限制,要么是有过多的本地 I/O 等待发生。

如何优化系统

1. 减少编码

网页输出是可以直接进行流输出的,即用 resp.getOutputStream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就可以减少静态数据的编码转换。


网页输出是可以直接进行流输出的,即用 resp.getOutputStream() 函数写数据

性能提升30% 左右

2. 减少序列化

避免或者减少 RPC 就可以减少序列化

合并部署

部署到一台机器上

同一个 Tomcat 容器中

且不能走本机的 Socket

3. Java 极致优化

Web 系统做静态化改造,让大部分请求和数据直接在 Nginx 服务器或者 Web 代理服务器(如 Varnish、Squid 等)上直接返回(这样可以减少数据的序列化与反序列化)

直接使用 Servlet 处理请求。避免使用传统的 MVC 框架,这样可以绕过一大堆复杂且用处不大的处理逻辑,节省 1ms 时间(具体取决于你对 MVC 框架的依赖程度)。

直接输出流数据。使用 resp.getOutputStream() 而不是 resp.getWriter() 函数,可以省掉一些不变字符数据的编码,从而提升性能;数据输出时推荐使用 JSON 而不是模板引擎(一般都是解释执行)来输出页面。

4. 并发读优化

放到 Tair 缓存里面

集中式缓存为了保证命中率一般都会采用一致性 Hash,所以同一个 key 会落到同一台机器上。虽然单台缓存机器也能支撑 30w/s 的请求,但还是远不足以应对像“大秒”这种级别的热点商品

答案是采用应用层的 LocalCache,即在秒杀系统的单机上缓存商品相关的数据。

像商品中的“标题”和“描述”这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;

像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。

秒杀系统“减库存”设计的核心逻辑

减库存有哪几种方式

  • 下单减库存

控制最精确

不会出现超卖

有些人下完单可能并不会付款

  • 付款减库存

并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。

  • 预扣库存

买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存

减库存可能存在的问题

  • 下单减库存

恶意下单商品就不能正常售卖

  • 付款减库存”又会导致另外一个问题:库存超卖。

下单成功但是付不了款,买家的购物体验自然比较差。

  • 预扣库存

一定程度上缓解上面的问题,但无法彻底解决

解决办法还是要结合安全和反作弊的措施来制止。

给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存

给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件)

以及对重复下单不付款的操作进行次数限制等。

对普通的商品下单数量超过库存数量的情况,可以通过补货来解决

有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。

大型秒杀中如何减库存?

下单减库存

由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。

主要就是保证大并发请求时库存数据不能为负数

一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;

另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错

再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:

UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END

秒杀减库存的极致优化

缓存中减库存

秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关系的话可以在缓存中做

数据库中减库存

比较复杂的减库存逻辑,或者需要使用事务,你还是必须在数据库中完成减库存。

单个热点商品会影响整个数据库的性能

进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。

由于 MySQL 存储数据的特点,同一数据在数据库里肯定是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁,而并发度越高时等待线程会越多,

要解决并发锁的问题,有两种办法:

应用层做排队。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。

数据库层做排队。应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL 的 InnoDB 层上的补丁程序(patch),可以在数据库层上对单行记录做到并发排队。

准备Plan B:如何设计兜底方案?

高可用建设应该从哪里着手

  • 架构阶段
可扩展性

容错性

避免系统出现单点问题

多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转
  • 编码阶段
健壮性

涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮

也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果
  • 测试阶段
保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
  • 发布阶段
要有紧急的回滚机制。
  • 运行阶段
运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题
  • 故障发生
最重要的就是及时止损

例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。

降级

所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务

可以通过预案系统和开关系统来实现降级

当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。

降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。

限流

基于 QPS 和线程数的限流应用最多,最大 QPS 很容易通过压测提前获取,例如我们的系统最高支持 1w QPS 时,可以设置 8000 来进行限流保护。线程数限流在客户端比较有效,例如在远程调用时我们设置连接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。

限流无疑会影响用户的正常请求,所以必然会导致一部分用户请求失败,因此在系统处理这种异常时一定要设置超时时间,防止因被限流的请求不能 fast fail(快速失败)而拖垮系统。

可以是在客户端限流,也可以是在服务端限流

限流的实现方式既要支持 URL 以及方法级别的限流,也要支持基于 QPS 和线程的限流。

客户端限流

好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。

服务端限流

好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。

拒绝服务

CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求

在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护

像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。

总结一下

预防

建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测

管控

做好线上运行时的降级、限流和兜底保护

监控

建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警

恢复体系

遇到故障要及时止损,并提供快速的数据订正工具等

提高单次请求的效率

减少没必要的请求

秒杀核心
(准, 稳, 快)

读了有收获就请肥宅喝瓶怡宝吧!