0.前言
- 本文是许令波(淘系的高级技术专家)的一篇专栏的总结 ,截取了其中重要的部分分享出来
- 因为是收费专栏,所以不方便在网上共享资源,如需获取资源,可私信我,但请保证是个人学习所用
1. 秒杀系统架构设计都有哪些关键点?
- 秒杀其实主要解决两个问题,一个是并发读,一个是并发写,而且读多写少
- 并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据(其实不止服务端,最重要的是减少对数据库的访问,一般我们做的秒杀项目的瓶颈都在数据库)
- 并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理(数据库独立出来通常是针对淘宝这样超大的并发,像我们平时做的小项目则基本不需要,只需要做好缓存即可,减少最终打到数据库的流量)
- 另外,还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生,一般是采用限流、降级等方法
- 总结一下:
- 秒杀是一个典型的读多写少的场景,也是一个典型的高并发的场景
- 秒杀的高并发与平时所讲的高并发还有一些不同,因为秒杀并不需要处理所有的请求,秒杀系统更像一个逐级的大坝,不断拦截上流的洪水,使得下游(数据库)可以保证安全
- 解决高并发的核心思想是缓存+异步,缓存可以包含各级缓存,前端、CDN、服务器等等,异步则可以使用消息队列
2. 设计秒杀系统时应该注意的5个架构原则
2.1 架构原则:“4 要 1 不要”
2.2 数据要尽量少
- 所谓“数据要尽量少”,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)
2.3 请求数要尽量少
- 用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少
- 因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久
- 减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开
2.4 路径要尽量短
- 所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数
- 缩短请求路径不仅可以增加可用性(减少数据丢失),同样可以有效提升性能
- 要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用
2.5 依赖要尽量少
- 所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖
- 举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉
- 要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统(这些都是高级系统需要考虑的,小项目没这么复杂。。。)
2.6 不要有单点
- 系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”
3. 如何才能做好动静分离?有哪些方案可选?
3.1 何为动静数据
-
所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”
-
“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据
-
比如说:
- 很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面
- 我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了
3.2 怎样对静态数据做缓存?
- 应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。你应该根据情况,把它们尽量缓存到离用户最近的地方
3.3 从以下 5 个方面来分离出动态内容:
- URL 唯一化。商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分
- 分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取
- 分离时间因素。服务端输出的时间也通过动态请求获取
- 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适
- 去掉 Cookie。服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie
4. 流量削峰这事应该怎么做?
4.1 流量削峰的一些操作思路:排队、答题、分层过滤
4.2 排队
- 要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去
4.3 答题
- 第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊
- 第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长,从以前的 1s 之内延长到 2s~10s
4.4 分层过滤
- 大部分数据和流量在用户浏览器或者 CDN 上获取,这一层可以拦截大部分数据的读取
- 经过第二层(即前台系统)时数据(包括强一致性的数据)尽量得走 Cache,过滤一些无效的请求
- 再到第三层后台系统,主要做数据的二次检验,对系统做好保护和限流,这样数据量和请求就进一步减少
- 最后在数据层完成数据的强一致性校验
5. 秒杀系统“减库存”设计的核心逻辑
5.1 减库存操作一般有如下几个方式:
- 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量
- 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家
- 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买
5.2 大型秒杀中如何减库存?
- 目前来看,业务系统中最常见的就是预扣库存方案,像你在买机票、买电影票时,下单后一般都有个“有效付款时间”,超过这个时间订单自动释放,这都是典型的预扣库存方案
- 保证数据库中的库存字段值不能为负数(超卖),一般我们有多种解决方案:
- 一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚
- 另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错
- 再有一种就是使用判断语句(最简单的只需要在SQL语句之后加上判断库存where 数量>0),例如:update goods set num = num - 1 WHERE id = 1001 and num > 0
6.兜底方案
6.1 降级、限流和拒绝服务
6.2 降级
- 所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务
- 降级方案可以这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数
6.3 限流
- 如果说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施
6.4 拒绝服务
- 如果限流还不能解决问题,最后一招就是直接拒绝服务了