2万字聊聊什么是秒杀系统(上)

1,269 阅读19分钟

大家好,我是Leo

前段时间介绍了MySQL,Redis的相关技术。大概告一段落了,只能说对这两块技术调优,原理有了初步的认识,后续整个技术栈学的差不多的时候会回来做第二版的修订。

推荐阅读

3万字聊聊什么是Redis(完结篇)

3万字聊聊什么是MySQL(初篇)

福利

每1-2周会选取前3名发一些书籍

每个节日会选取前10名发一些书籍

麻烦这三位在我公众号右下角点击联系我,加一下微信,发波书了

最近不是在学习MySQL,Redis嘛。我们就找几本经典的书送一下

比如:高性能MySQL,Redis设计与实现,Redis深度历险,

或者数据结构类的 剑指offer,代码随想录

前言

任何技术的总结都有一个业务背景,简单介绍一下有助于更好的理解,顺便输出一下自己的学习思想。

之前接的一个跨境电商项目基本需求均已实现。比如购买需求,积分,会员,购物车,用户,广告,消息通知等。

下面开始考虑一些特价,秒杀,限时,推荐类的功能了。平白无故让我写,从0到1肯定不会,自己一点一点从淘宝10年前的技术发展历程开始研究肯定不划算。问题来了如何上手开发设计一个秒杀系统呢?

当我有这个想法的时候,我也是疯了。主要有如下几点

  1. 老板没要求,我自己的事情忙的要死还要给私活白写一个功能
  2. 开发一个秒杀至少要了解淘宝的发展历程,秒杀的设计思想
  3. 硬件设施的落地实现
  4. 原有架构的数据保存模式变更

主要还是没钱支撑我这个想法,毕竟不是花点时间写代码就OK的 。所以综上所述,购买相关秒杀的课程,学习前辈们的思想,再斟酌自己的系统体量进行开发是我接下来要做的事情

学习思想

聊到学习,相信很多人会是下面这个状态。

当下信息化太爆炸,很多APP,系统根据个人的喜好很容易就被他影响了。我始终认为应该逼自己一把,给自己一些动力。做时间的掌控者,做人生的掌控者。

关于学习的顺序,建议一点一点学习当下切合自己能力的技术。很多人基础还没过,天天吵着要学微服务,分布式,大并发。然后在群里我问他为啥,他说要拿阿里20K。

20K哪有那么容易哦,技术没有捷径,一步一步才是王道。C#转Java已经快半年了,我才刚学微服务。我学微服务还是因为我leader开会的时候说后续可能要上微服务我才提前学的。要不然我吃饱了撑的没事啃微服务这个大骨头。


秒杀系统的5个原则

收!回到正题!

数据尽量要少

数据尽量要少的话主要有两点:用户发送请求附带的数据尽量要少服务端给客户端返回的数据尽量要少 绝不传无关紧要的字段。

因为数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。

第二层含义是完成某些业务,我们从数据库读取查值与保存的数据,这个是涉及到与数据库交互的。调用其他服务会涉及数据的序列化和反序列化,而这也是 CPU 的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,数据越简单、越小则越好

下面举一些例子,比如在下单这里,要根据体量进行抉择,到底是由前端传给后端对应的值来做实体类拼接处理,还是前端只传ID。根据用户的ID找到用户的等级,根据等级再找到这个用户对应的商品价格呢?

还有下列的积分ID,到底是前端传呢,还是只传用户ID,反查积分ID呢?

还有是保存用户的信息到缓存直接进行Redis incr,decr 还是反查一遍呢?如果交给缓存,那么一致性如何保证? (可以参考我之前Redis的第七篇文章)

上述只是一些思考点,不是最优的方案,代码还没优化,只是一阶段的开发。

请求数尽量要少

请求数当时我看的时候第一时间没反应过来,这里我用图文的方式介绍。

用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。

如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。所以我们要记住的是,减少请求数可以显著减少以上这些因素导致的资源消耗。

路径尽量要短

路径这里主要就是关于节点数了。这些节点可以表示为一个系统或者一个新的 Socket 连接(比如代理服务器只是创建一个新的 Socket 连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。

依赖尽量要少

依赖这里主要就是 秒杀系统跟业务系统不同的是,业务系统追求稳定,秒杀系统在那一刻追求的是效率。这里也类似CP和AP的抉择。

如下图所示,我们可以想成秒杀场景,如果某一块服务挂了,难道要影响整个系统嘛,肯定是不行的,所以这里就要注意强依赖,弱依赖的问题。

杜绝单点

这个没啥说的,万一出现意外,宕机了,不仅影响使用,有可能数据都会有影响,所以一定要采用多服务,分布式,集群思想设计系统。

学习淘宝

淘宝早期秒杀系统架构

快速搭建一个简单的秒杀系统,只需要把你的商品购买页面增加一个“定时上架”功能,仅在秒杀开始时才让用户看到购买按钮,当商品的库存卖完了也就结束了。这就是当时第一个版本的秒杀系统实现方式。

体量的增加10w/s

随着体量的增加,也遇到了很多瓶颈。因此把秒杀模块独立成一个秒杀系统。这样就可以专门对秒杀系统做一些有针对性的优化处理。

在部署上也可以把秒杀系统独立部署在一个集群,这样即可大流量把秒杀模块干倒了也不至于影响正常商品购买的机器负载。

在热点数据比如库存数量单独放在一个缓存系统中,以提高读性能,减少与数据库的交互。

在安全上,可以增加一下秒杀滑块,防止有秒杀脚本抢单,影响正常的用户体验需求。

体量的增加100w/s

在界面上进行一些动静分离,这样可以让用户秒杀时,不需要刷新整个页面。

在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。

在安全上,增加系统限流保护,防止最坏情况发生。

结尾

架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。这里所说的几点都只是一个个方向,你应该尽量往这些方向上去努力,但也要考虑平衡其他因素。

动静分离

什么是动静数据

动静分离并不是真正的活数据,死数据。所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。

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

  • 比如百度的热搜前十一样,不管是任何人访问,他都是一样的。它是一个典型的静态数据,但是是动态界面。
  • 淘宝首页的商品,会根据每个人的特征推送相关感兴趣的商品。这个就是动态数据,动态界面(但是不典型很多具有推荐算法的APP都有这类功能)

动态与静态并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。

静态数据优化

理解了动态数据和静态数据,我估计你们的脑海中就有一个优化的方案了

  1. 可以把用户的静态数据缓存到Redis中,或者缓存到离用户最近的地方。比如用户浏览器,CDN。
  2. 直接缓存HTTP连接
  3. 利用Web服务器处理大并发的静态文件请求。比如我们常用的nginx,apache

直接缓存HTTP连接我估计很多人不明白,这里介绍一下。静态化改造是直接缓存HTTP而不是缓存数据。Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。

动静分离改造

电商为例(淘宝,京东)

博客为例(掘金,CSDN)

  1. 我们可以缓存URL,使URL唯一化。可以见上述的天猫,京东,CSDN,掘金。缓存的key就是每一个唯一的一个编号id
  2. 商品/文章是动态的但是每个用户登录的信息验证都是静态的,所以我们可以把这部分的服务独立出来
  3. 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。(无关紧要的数据)
  4. 分离时间因素。服务端输出的时间也通过动态请求获取。

如下两张参考图可以理解下商品详情部分 分离的思想

分离出动态内容之后,我们可以将这些信息 JSON 化(用 JSON 格式组织这些数据),以方便前端获取。

动态数据优化

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

动静分离的架构方案

  1. 实体机单机部署;
  2. 统一 Cache 层;
  3. 上 CDN。

实体单机部署

这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。

优点

  • 没有网络瓶颈,而且能使用大内存;
  • 既能提升命中率,又能减少 Gzip 压缩;
  • 减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。

缺点

  • 一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。
  • 一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度

如下图所示,单机部署的架构图

统一 Cache 层

所谓统一 Cache 层,就是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。统一 Cache 层是个更理想的可推广方案,该方案的结构图如下

优点

  • 单独一个 Cache 层,可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好,不需要单独维护 Cache,而只关心如何使用即可。
  • 统一 Cache 的方案更易于维护,如后面加强监控、配置的自动化,只需要一套解决方案就行,统一起来维护升级也比较方便。
  • 可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击。

缺点

  • Cache 层内部交换网络成为瓶颈;
  • 缓存服务器的网卡也会是瓶颈;
  • 机器少风险较大,挂掉一台就会影响很大一部分缓存数据。

要解决上面这些问题,可以再对 Cache 做 Hash 分组,即一组 Cache 缓存的内容相同,这样能够避免热点数据过度集中导致新的瓶颈产生。

上CDN

在将整个系统做动静分离后,我们自然会想到更进一步的方案,就是将 Cache 进一步前移到 CDN 上,因为 CDN 离用户最近,效果会更好。

采用这种方案,主要有三个问题需要解决一下

  1. 失效问题
  2. 命中率问题
  3. 发布更新问题

失效和Redis有点类似,如果不及时更新缓存的话有可能在那段时间内用户看到的值都是错位的,如果及时更新的话就需要保证CDN可以在秒级时间内让分布在全国各地的Cache同时失效。这个要求还是比较高的。

命中率是缓存的命根。一个缓存系统命中率过低,那和没有缓存系统没什么两样反而还要消耗自身性能去查询缓存系统。

综上所述,把商品详情,博客详情等放到全国所有CDN节点上不太现实,我们可以采用以下方案解决这一不足

  1. 靠近访问量比较集中的地区
  2. 离主站相对较远;
  3. 节点到主站间的网络比较好,而且稳定;
  4. 节点容量比较大,不会占用其他 CDN 太多的资源。
  5. 节点不要太多。

基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下图所示:

使用 CDN 的二级 Cache 作为缓存,可以达到和当前服务端静态化 Cache 类似的命中率,因为节点数不多,Cache 不是很分散,访问量也比较集中,这样也就解决了命中率问题,同时能够给用户最好的访问体验,是当前比较理想的一种 CDN 化方案。

除此之外,CDN 化部署方案还有以下几个特点:

  1. 把整个页面缓存在用户浏览器中;
  2. 如果强制刷新整个页面,也会请求 CDN;
  3. 实际有效请求,只是用户对“刷新抢宝”按钮的点击。

这样就把 90% 的静态数据缓存在了用户端或者 CDN 上,当真正秒杀时,用户只需要点击特殊的“刷新抢宝”按钮,而不需要刷新整个页面。这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据。秒杀的动态数据和普通详情页面的动态数据相比更少,性能也提升了 3 倍以上。所以“抢宝”这种设计思路,让我们不用刷新页面就能够很好地请求到服务端最新的动态数据

二八原则

什么是二八原则

世上上80%的财富掌握在20%的人手中,热点数据也是一样。几千万的商品,每天有几百万商品被几十万用户访问。这类数据就是热点数据

在信息化时代,并不是所有的数据都是受人欢迎的,所以在处理系统热点时间时,我们可以分为两种情况

  1. 特定的热点数据(比如推荐的商品)
  2. 没有被推荐的数据,但是后期的发酵变成了热点数据

从冷静期到活跃期与活跃期

从不是热点到变成热点最典型的例子就是今日头条,微博这类APP。如果一个新闻事件慢慢发酵,系统内没有对这个事件缓存的话,有可能不一会系统就宕机了。

所以我们要先发现热点。热点分为热点操作,热点数据

  • 热点操作 用户在下拉刷新时,想看看自己留意的事件有没有上热搜。
  • 热点数据 上了热搜的事件要不断被用户访问。

对系统来说就是 读请求写请求 。这两类的处理方式大相径庭,不过读更好处理一些,因为可以减少与数据库的交互。而写请求的瓶颈往往在存储层。后续会展开介绍

热点数据还分动态热点数据(由外在因素引发的热卖商品)静态热点数据(商家推荐的商品)

如何发现动态热点数据?

  1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。
  2. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。

这是大一点的系统,对小型系统来说。可以通过ELK方案收集用户搜索商品的次数来决定是否为热点数据

如何处理动态热点数据?

主要有三种思路

  1. 优化
  2. 限制
  3. 隔离

优化

优化最简单的方式就是直接把数据缓存了。如果热点数据做了动静分离,那么可以长期缓存静态数据。但是,缓存热点数据更多的是“临时”缓存,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用 LRU 淘汰算法替换。

限制,限流

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

隔离

秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让 1% 的请求影响到另外的 99%,隔离出来后也更方便对这 1% 的请求做针对性的优化。

结尾

大概总结了

  1. 如何设计一个秒杀系统的五大原则
  2. 动静分离方案
  3. 二八原则,热冷数据的处理方案

我跟大家一样,都在学习这类系统的设计思想,接下来准备动手干一部分代码了。具体的效果可以通过【公众号】=>【项目经验】=>【跨境电商】查看

非常欢迎大家关注公众号【欢少的成长之路】有关后端方面的问题我们在群内一起讨论! 我们下期再见!