Loading

【沉淀笔记】秒杀系统设计

秒杀系统

[!NOTE]

在今年的秋招面试中,“秒杀系统”也是本人遇到的一个相对高频考点,所以在秋招结束后,我学习了极客时间的《如何设计一个秒杀系统》课程,并总结了一些要点,面试可从这几点来阐述。

目标 原则
1. 高性能。数据动静分离、热点发现和隔离、请求的削峰和分层过滤、服务端的性能优化。
2. 一致性。减库存方案。
2. 高可用。兜底方案。
1. 数据尽量少
2. 请求数量尽量少(热点发现和处理)
3. 路径尽量短
4. 依赖尽量少
5. 不要有单点

1 动静分离

最早的时候,秒杀系统其实是要刷新整个页面的,但在后来更先进的方案中,只需要点击“刷新抢宝”按钮就够了,因为系统中用了动静分离的设计,使得在秒杀场景中,大幅减少了客户端请求的数据量

1.1 静态和动态

  • 静态数据:不管是谁来访问,都是一样的。
  • 动态数据:页面输出的数据与URL、浏览者、时间、地域,或cookie中包含的私密数据相关。比如访问淘宝的主页,包含了很多根据访问者特征推荐的个性化信息,因此每个人看到的页面是不一样的。

1.2 静态数据的缓存

  1. 将静态数据缓存到离用户最近的地方,如用户浏览器、CDN,或服务端的cache中。
  2. 静态化改造:直接缓存HTTP连接,web代理服务器根据请求URL,直接取出对应的HTTP响应头和响应体,并直接返回。省去了HTTP协议的组装、请求头的解析过程。
  3. 直接在web服务器做缓存,而不是在Java层。Java系统不擅长处理大量连接请求,每个连接消耗的内存更多。相比之下,web服务器(如Nginx、Apache、Varnish)更擅长处理高并发的静态文件请求。

1.3 动静分离改造

分离动态数据

  1. URL唯一化。每个商品对应一个URL,如http://item.xxx.com/item.htm?id=xxxx。这样可以使用URL作为缓存的key,来缓存HTTP连接。
  2. 分离浏览者相关信息。通过动态请求获取登录信息。
  3. 分离时间信息。动态请求获取服务端输出的时间。
  4. 分离地域信息。异步获取。
  5. 分离cookie。缓存的静态数据中不包含cookie。

处理动态数据

  1. ESI方案(Edge Side Includes):在web代理服务器上处理动态请求,并将请求插入到静态页面中。优点:用户拿到的是完整的页面;缺点:对服务端性能有影响。
  2. CSI方案(Client Side Includes):客户端单独发起异步JavaScript请求,向服务端获取动态内容。优点:服务端性能好;缺点:用户页面延迟,体验差。

1.4 动静分离架构方案

方案 分析 优点 缺点
方案一:实体机单机部署。 将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。 1. 没有网络瓶颈,能使用机器上的大内存。
2. 提升命中率,减少Gzip压缩。
3. 减少cache压力。(定时失效)
1. 造成了CPU浪费,因为一个Java进程很难利用完全整个实体机的CPU。
2. 单机既部署Java应用又作为cache,运维复杂。
方案二:统一cache层 将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。 1. 减少运维成本,只需要一套解决方案。
2. 减少其他应用接入成本。接入的应用只需要维护自己的Java系统,不关心cache。
3. 可以共享内存,最大化利用内存。
1. cache层内部交换网络瓶颈。
2. 缓存服务器的网卡瓶颈。
3. 机器少,挂掉一台影响很大一部分缓存数据。
方案三:使用CDN 将 Cache 进一步前移到 CDN 上,因为 CDN 离用户最近,效果会更好。 1. CDN在地理上分散,缩短数据传输距离,使静态内容更快到达用户端。
2. 使用CDN的二级cache可以保持较高的命中率。(二级cache节点数更少,容量更大)
3. CDN上方便做主动失效、缓存更新。
1. 缓存失效问题。静态数据也可能会发生变化。需要保证CDN在秒级内,让分布在各地的cache同时失效。
2. 命中率问题。数据分布在全国的CDN上,导致cache分散。(使用CDN二级缓存,可以解决)
3. 发布更新问题。

CDN 是一种分布式网络,通过将内容缓存到离用户近的节点,提升网站访问速度和可用性。

一级缓存 是最靠近用户的缓存节点,通常用于缓存频繁访问的热点内容,命中率较高。

二级缓存 是相对靠近源站的缓存节点,用于缓存访问频率较低或长尾数据,减少源站压力。

2 热点发现和处理

  • 热点操作:双十一零点下单、添加购物车等。分读请求和写请求处理。
  • 静态热点数据:能够通过卖家报名、数据分析等方式提前预测热点数据。
  • 动态热点数据:系统运行时临时产生的热点数据。

2.1 热点数据发现

静态热点数据发现

  1. 卖家报名,通过运营系统提前做缓存。
  2. 数据分析,提前统计TOP N商品。
  3. 缺点:增加卖家使用成本,实时性差不灵活。

动态热点数据发现

构建热点发现系统,收集交易链路上的热点key,将上游发现的热点透传给下游系统,下游系统做热点保护处理。

比如以来上游的导购决策页面,如首页/搜索/商品详情,提前识别热点商品,通过上游系统的中间件收集热点数据,记录到日志中。对日志进行聚合和分析,把符合规则的热点数据通过订阅分发系统推送到相应的下游系统中,进行热点保护(填充到缓存中或应用服务器的内存等等)。

热点发现系统注意点

  1. 热点服务异步抓取热点数据。异步保证通用性,且不影响主流程。
  2. 热点服务发现系统与中间件自身的热点保护模块并存。
  3. 热点发现要接近实时,才能对下游系统做动态的保护。

2.2 热点数据处理

三种思路:优化,限制,隔离。

  1. 优化:使用队列,临时缓存热点数据。
  2. 限制:对商品ID做一致性hash分桶,每个分桶设置一个处理队列,可以将热点商品限制在一个请求队列里,防止热点数据影响到其他请求。
  3. 隔离:
    1. 业务隔离:热点请求单独做秒杀业务,卖家报名,提前做预热。
    2. 系统隔离:集群做分组部署,秒杀业务通过单独的域名进行分组隔离。
    3. 数据隔离:启用单独的cache集群或mysql数据库存放热点数据。
    4. 其他:接入层针对URL中的不同path设置限流策略;服务层调用不同的服务接口进行区分;数据层给数据打标进行区分。目的是识别出热点请求。

3 削峰

三种方式:排队,答题,分层过滤

3.1 排队

  • 使用消息队列来缓冲瞬时流量
  • 线程池加锁等待
  • 将请求序列化到文件中顺序读文件

3.2 答题

防止秒杀器,延长请求峰值时间。

3.3 分层过滤

通过CDN、前台读系统、后台写系统、数据库四层,进行无效请求过滤。需要做数据的分层校验:

  1. 读数据不做强一致性校验,减少瓶颈。
  2. 对写数据进行基于时间的分片,过滤掉过期请求。
  3. 对写请求做限流,将超出系统承载能力的请求过滤。
  4. 对写数据做强一致性校验,只保留有效数据。

在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;

在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。

4 性能优化

4.1 性能指标和影响因素

性能指标:QPS,响应时间

  1. 总QPS = (1000ms / 响应时间)* 线程数
  2. 响应时间:CPU执行时间+线程等待时间

真正影响性能的因素:CPU执行时间。因为CPU的执行真实消耗了服务器资源。

4.2 多线程

线程数不是越多越好,因为线程切换需要成本,每个线程也会耗费一定的内存

线程数配置

  1. 默认配置:线程数 = 2 * CPU 核数 + 1
  2. 线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量
  3. 通过性能测试来找到最佳配置

4.3 性能瓶颈

可能产生瓶颈的地方:CPU、内存、磁盘、网络

秒杀系统的瓶颈更多发生在CPU上。

可以通过CPU诊断工具,判断出请求中函数的CPU执行时间,有针对性地优化。

如何判断CPU是否是瓶颈:

看当 QPS 达到极限时,你的服务器的 CPU 使用率是不是超过了 95%,如果没有超过,那么表示 CPU 还有提升的空间,要么是有锁限制,要么是有过多的本地 I/O 等待发生。

4.4 优化手段

  1. 减少编码。字符串的IO操作需要将字符转为字节,消耗资源。可以将静态数据提前转换为字节,直接输出字节内容到页面。

  2. 减少序列化。减少RPC调用,将多个应用进行合并部署。

  3. Java优化。做静态化改造,将大部分请求和数据直接在Nginx服务器或web代理服务器上直接返回,Java层只处理少量的动态请求:

    1. 直接使用servlet,避免MVC框架的复杂处理逻辑。
    2. 直接输出流数据。
  4. 并发读优化。解决单点缓存瓶颈:使用应用层Local Cache,在秒杀系统单机上缓存商品相关数据。

    1. 静态数据:类似商品标题和描述,在秒杀开始前全量推送到秒杀机器上,缓存到秒杀结束。
    2. 动态数据:类似库存,采用被动失效缓存一段时间(几秒),失效后再去缓存拉取最新数据。

    数据不一致(超卖)问题?读场景允许一定的脏数据,在真正写数据的时候才会保证一致性。

  5. 其他手段。

    1. 减少数据;
    2. 数据分级,次要信息异步加载;
    3. 减少中间环节;
    4. 做好应用基线……

Servlet是 Java Web 应用的基础组件,直接与 HTTP 请求和响应交互,提供了底层的控制。这意味着开发者需要手动处理请求参数、路由等,负责数据的输入、输出和渲染视图的每一步工作。

性能稍高,因为 Servlet 是底层技术,少了许多框架的封装层,执行效率更高。高并发或性能要求特别高的场景下,直接使用 Servlet 可以减少资源占用。

5 减库存

5.1 减库存的几种方式

  1. 下单减库存。问题:恶意下单不付款,使商品无法正常售卖。
  2. 付款减库存。问题:库存超卖,买家下单成功数远大于库存数量,下单成功却无法付款,买家购物体验差。
  3. 预扣库存,下单时先预扣,在规定时间内不付款再释放库存。问题:没法完全解决恶意下单和库存超卖的现象。
    1. 恶意下单:标记恶意买家、给商品设置最大购买数量、重复下单不付款操作次数限制。
    2. 库存超卖:补货;付款时提示库存不足。

5.2 大型秒杀场景的常用策略

  1. 使用“下单减库存”策略,原因:
    1. 秒杀场景下,一般来说成功下单却不付款的情况较少。
    2. 下单减库存相比付款减库存、预扣库存在实现逻辑上更简单,性能上更好。
  2. 保证数据一致性(库存不为负数):
    1. 通过事务来判断,保证库存不为负数,否则回滚。
    2. 设置数据库字段为无符号整数,这样一旦库存为负数,SQL语句执行时就会报错。
    3. 使用CASE WHEN判断语句。

5.3 热点数据(库存)的优化

  1. 若减库存逻辑非常单一,可以使用缓存(如redis)完成。
  2. 若减库存逻辑复杂,或需要使用事务,则必须使用数据库。

由于 MySQL 存储数据的特点,同一数据在数据库里肯定是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,即每秒处理的消息数)会下降,响应时间(RT)会上升,数据库的吞吐量就会严重受影响。

所以单个热点数据会影响整个数据库的性能。

解决方法:

  1. 应用层排队。按照商品维度设置队列顺序执行(控制单个商品占用数据库连接的数量)。
  2. 数据库层面排队。对mysql innodb做补丁,实现在数据库层面对单行记录做到排队。

6 兜底

高可用系统建设

阶段 内容
架构阶段 异地容灾,异步化,分组隔离,避免单点
编码阶段 限流保护,超时处理,异步线程,错误捕获
测试阶段 beta测试,自动化对比测试
发布阶段 分批发布,多版本发布
运行阶段 数据对账,自动降级,过载保护,实时监控报警
故障发生 快速恢复,故障定位

6.1 降级

  • 当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
  • 通过预案系统和开关系统实现降级。
  • 例子:在双 11 零点时,如果优惠券系统扛不住,可能会临时降级商品详情的优惠信息展示,把有限的系统资源用在保障交易系统正确展示优惠信息上,即保障用户真正下单时的价格是正确的。

6.2 限流

  • 客户端限流:在客户端设置阈值,限制请求的发出。难以设置合理的阈值。
  • 服务端限流:可以根据服务端性能设置出合理的阈值。需要额外处理无效的请求。

6.3 拒绝服务

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

总结

秒杀系统是为了高并发下保障服务稳定以及用户体验而设计的,该系统的构建主要围绕三个核心目标:高性能、一致性、高可用。

  1. 高性能数据动静分离热点发现和处理是应对高并发的关键手段。通过静态和动态分析提前预测热点商品,将其缓存至合适的位置,减轻服务器负担。同时,为减少服务器压力,系统设计了削峰策略,包括排队、答题、和分层过滤等方式来控制突发流量,确保关键请求能被优先处理。
  2. 一致性:减库存机制是保障数据一致性的关键之一。通过“下单减库存”的方式来实时更新库存,结合库存保护手段(如事务处理和无符号整数字段),可以防止库存变负数,并避免恶意下单对系统的冲击。
  3. 高可用性:为确保秒杀系统在高峰时段的稳定性,采用了异地容灾、限流、降级等措施,保障系统的核心业务。降级策略会在非核心功能负载过大时关闭,保留资源给最核心的交易业务,避免系统宕机。限流拒绝服务策略可有效防止系统过载,通过客户端和服务端双层限流,使秒杀系统在压力下仍能高效地服务用户。

总的来说,秒杀系统的构建是一个从架构、编码到实际运行的全链条过程,通过数据缓存、性能优化、降级容灾等手段应对秒杀场景中的高并发压力,保障系统的稳定性和用户体验。

posted @ 2024-11-13 14:11  rthete  阅读(23)  评论(0编辑  收藏  举报