【Java】如何设计一个秒杀系统

一、问题分析

说起秒杀,我想你肯定不陌生,这两年,从双十一购物到春节抢红包,再到12306抢火车票,“秒杀”的场景处处可见。简单来说,秒杀就是在同一个时刻有大量的请求争抢购买同一个商品并完成交易的过程,用技术的行话来说就是大量的并发读和并发写。

不管是哪一门语言,并发都是程序员们最为头疼的部分。同样,对于一个软件而言也是这样,你可以很快增删改查做出一个秒杀系统,但是要让它支持高并发访问就没那么容易了。比如说,如何让系统面对百万级的请求流量不出故障?如何保证高并发情况下数据的一致性写?完全靠堆服务器来解决吗?这显然不是最好的解决方案。

在我看来,秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。今天,我们就来聊聊,如何在满足一个良好架构的分布式系统基础上,针对秒杀这种业务做到极致的性能改进。

如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高维度出发,从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。

而从一个架构师的角度来看,要想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上从浏览器到服务端我们要遵循几个原则,就是要保证用户请求的数据尽量少、请求数尽量少、路径尽量短、依赖尽量少,并且不要有单点。这些关键点我会在后面的文章里重点讲解。

其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。

所谓“稳”,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。

然后就是“准”,就是秒杀10台iPhone,那就只能成交10台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。

最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。

所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求,我们的专栏也将主要围绕这几个方面来展开,具体如下。

  • 高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。本专栏将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。
  • 一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,我将用一篇文章来专门讲解如何设计秒杀减库存方案。
  • 高可用。 虽然我介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。专栏的最后,我将带你思考可以从哪些环节来设计兜底方案。

3.1 设计秒杀系统应该注意的5个架构原则

不管是哪一门语言,并发都是程序员们最为头疼的部分。同样,对于一个软件而言也是这样,你可以很快增删改查做出一个秒杀系统,但是要让它支持高并发访问就没那么容易了。比如说,如何让系统面对百万级的请求流量不出故障?如何保证高并发情况下数据的一致性写?完全靠堆服务器来解决吗?这显然不是最好的解决方案。

在我看来,秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。今天,我们就来聊聊,如何在满足一个良好架构的分布式系统基础上,针对秒杀这种业务做到极致的性能改进。

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

如果你是一个架构师,你首先要勾勒出一个轮廓,想一想如何构建一个超大流量并发读写、高性能,以及高可用的系统,这其中有哪些要素需要考虑。我把这些要素总结为“4要1不要”。

  1. 数据要尽量少

所谓“数据要尽量少”,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。

为啥“数据要尽量少”呢?因为首先这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗CPU,所以减少传输的数据量可以显著减少CPU的使用。例如,我们可以简化秒杀页面的大小,去掉不必要的页面装修效果,等等。

其次,“数据要尽量少”还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。调用其他服务会涉及数据的序列化和反序列化,而这也是CPU的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,数据越简单、越小则越好。

  1. 请求数要尽量少

用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的CSS/JavaScript、图片,以及Ajax请求等等都定义为“额外请求”,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的DNS解析,可能会耗时更久。所以你要记住的是,减少请求数可以显著减少以上这些因素导致的资源消耗。

例如,减少请求数最常用的一个实践就是合并CSS和JavaScript文件,把多个JavaScript文件合并成一个文件,在URL中用逗号隔开(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个URL,然后动态把这些文件合并起来一起返回。

  1. 路径要尽量短

所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。

通常,这些节点可以表示为一个系统或者一个新的Socket连接(比如代理服务器只是创建一个新的Socket连接来转发请求)。每经过一个节点,一般都会产生一个新的Socket连接。

然而,每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过5个节点,每个节点的可用性是99.9%的话,那么整个请求的可用性是:99.9%的5次方,约等于99.5%。

所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。

要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成JVM内部之间的方法调用。在《大型网站技术架构演进与性能优化》一书中,我也有一章介绍了这种技术的详细实现。

  1. 依赖要尽量少

所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。

举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。

要减少依赖,我们可以给系统进行分级,比如0级系统、1级系统、2级系统、3级系统,0级系统如果是最重要的系统,那么0级系统强依赖的系统也同样是最重要的系统,以此类推。

注意,0级系统要尽量减少对1级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是0级系统,而优惠券是1级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个1级系统给拖垮。

  1. 不要有单点

系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。

那如何避免单点呢?我认为关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。

如何那把服务的状态和机器解耦呢?这里也有很多实现方式。例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。

应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景一般要通过冗余多个备份的方式来解决单点问题。

前面介绍了这些设计上的一些原则,但是你有没有发现,我一直说的是“尽量”而不是“绝对”?

我想你肯定会问是不是请求最少就一定最好,我的答案是“不一定”。我们曾经把有些CSS内联进页面里,这样做可以减少依赖一个CSS的请求从而加快首页的渲染,但是同样也增大了页面的大小,又不符合“数据要尽量少”的原则,这种情况下我们为了提升首屏的渲染速度,只把首屏的HTML依赖的CSS内联进来,其他CSS仍然放到文件中作为依赖加载,尽量实现首屏的打开速度与整个页面加载性能的平衡。

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

3.1.2 不同场景下的不同架构案例

前面我说了一些架构上的原则,那么针对“秒杀”这个场景,怎样才是一个好的架构呢?下面我以淘宝早期秒杀系统架构的演进为主线,来帮你梳理不同的请求体量下,我认为的最佳秒杀系统架构。

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

但随着请求量的加大(比如从1w/s到了10w/s的量级),这个简单的架构很快就遇到了瓶颈,因此需要做架构改造来提升系统性能。这些架构改造包括:

  1. 把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化,例如这个独立出来的系统就减少了店铺装修的功能,减少了页面的复杂度;
  2. 在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;
  3. 将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
  4. 增加秒杀答题,防止有秒杀器抢单。

此时的系统架构变成了下图这个样子。最重要的就是,秒杀详情成为了一个独立的新系统,另外核心的一些数据放到了缓存(Cache)中,其他的关联系统也都以独立集群的方式进行部署。

图1 改造后的系统架构

然而这个架构仍然支持不了超过100w/s的请求量,所以为了进一步提升秒杀系统的性能,我们又对架构做进一步升级,比如:

  1. 对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
  2. 在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
  3. 增加系统限流保护,防止最坏情况发生。

经过这些优化,系统架构变成了下图中的样子。在这里,我们对页面进行了进一步的静态化,秒杀过程中不需要刷新整个页面,而只需要向服务端请求很少的动态数据。而且,最关键的详情和交易系统都增加了本地缓存,来提前缓存秒杀商品的信息,热点数据库也做了独立部署,等等。

图2 进一步改造后的系统架构

从前面的几次升级来看,其实越到后面需要定制的地方越多,也就是越“不通用”。例如,把秒杀商品缓存在每台机器的内存中,这种方式显然不适合太多的商品同时进行秒杀的情况,因为单机的内存始终有限。所以要取得极致的性能,就要在其他地方(比如,通用性、易用性、成本等方面)有所牺牲。

3.2 如何才能做好动静分离?有哪些方案可选?

如果你在一个业务飞速发展的公司里,并且你在深度参与公司内类秒杀类系统的架构或者开发工作,那么你迟早会想到动静分离的方案。为什么?很简单,秒杀的场景中,对于系统的要求其实就三个字:快、准、稳

那怎么才能“快”起来呢?我觉得抽象起来讲,就只有两点,一点是提高单次请求的效率,一点是减少没必要的请求。今天我们聊到的“动静分离”其实就是瞄着这个大方向去的。

3.2.1 何为动静数据

那到底什么才是动静分离呢?所谓“动静分离”,其实就是把用户请求的数据(如HTML页面)划分为“动态数据”和“静态数据”。

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

  1. 很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。
  2. 我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。

这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的HTML页面,它也可能是经过Java系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。

还有一点要注意,就是页面中“不包含”,指的是“页面的HTML源码中不含有”,这一点务必要清楚。

理解了静态数据和动态数据,我估计你很容易就能想明白“动静分离”这个方案的来龙去脉了。分离了动静数据,我们就可以对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。

那么,怎样对静态数据做缓存呢?我在这里总结了几个重点。

第一,你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN上或者在服务端的Cache中。你应该根据情况,把它们尽量缓存到离用户最近的地方。

第二,静态化改造就是要直接缓存HTTP连接。相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存HTTP连接而不是仅仅缓存数据,如下图所示,Web代理服务器根据请求URL,直接取出对应的HTTP响应头和响应体然后直接返回,这个响应过程简单得连HTTP协议都不用重新组装,甚至连HTTP请求头也不需要解析。

3.2 如何才能做好动静分离?有哪些方案可选?

如果你在一个业务飞速发展的公司里,并且你在深度参与公司内类秒杀类系统的架构或者开发工作,那么你迟早会想到动静分离的方案。为什么?很简单,秒杀的场景中,对于系统的要求其实就三个字:快、准、稳

那怎么才能“快”起来呢?我觉得抽象起来讲,就只有两点,一点是提高单次请求的效率,一点是减少没必要的请求。今天我们聊到的“动静分离”其实就是瞄着这个大方向去的。

3.2.1 何为动静数据

那到底什么才是动静分离呢?所谓“动静分离”,其实就是把用户请求的数据(如HTML页面)划分为“动态数据”和“静态数据”。

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

  1. 很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。
  2. 我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。

这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的HTML页面,它也可能是经过Java系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。

还有一点要注意,就是页面中“不包含”,指的是“页面的HTML源码中不含有”,这一点务必要清楚。

理解了静态数据和动态数据,我估计你很容易就能想明白“动静分离”这个方案的来龙去脉了。分离了动静数据,我们就可以对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。

那么,怎样对静态数据做缓存呢?我在这里总结了几个重点。

第一,你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN上或者在服务端的Cache中。你应该根据情况,把它们尽量缓存到离用户最近的地方。

第二,静态化改造就是要直接缓存HTTP连接。相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存HTTP连接而不是仅仅缓存数据,如下图所示,Web代理服务器根据请求URL,直接取出对应的HTTP响应头和响应体然后直接返回,这个响应过程简单得连HTTP协议都不用重新组装,甚至连HTTP请求头也不需要解析。

图1 静态化改造

第三,让谁来缓存静态数据也很重要。不同语言写的Cache软件处理缓存数据的效率也各不相同。以Java为例,因为Java系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet容器解析HTTP协议较慢),所以你可以不在Java层做缓存,而是直接在Web服务器层上做,这样你就可以屏蔽Java语言层面的一些弱点;而相比起来,Web服务器(如Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。

3.2.2 如何做动静分离的改造

理解了动静态数据的“why”和“what”,接下来我们就要看“how”了。我们如何把动态页面改造成适合缓存的静态页面呢?其实也很简单,就是去除前面所说的那几个影响因素,把它们单独分离出来,做动静分离。

下面,我以典型的商品详情系统为例来详细介绍。这里,你可以先打开京东或者淘宝的商品详情页,看看这个页面里都有哪些动静数据。我们从以下5个方面来分离出动态内容。

  1. URL唯一化。商品详情系统天然地就可以做到URL唯一化,比如每个商品都由ID来标识,那么http://item.xxx.com/item.htm?id=xxxx就可以作为唯一的URL标识。为啥要URL唯一呢?前面说了我们是要缓存整个HTTP连接,那么以什么作为Key呢?就以URL作为缓存的Key,例如以id=xxx这个格式进行区分。
  2. 分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
  3. 分离时间因素。服务端输出的时间也通过动态请求获取。
  4. 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
  5. 去掉Cookie。服务端输出的页面包含的Cookie可以通过代码软件来删除,如Web服务器Varnish可以通过unset req.http.cookie 命令去掉Cookie。注意,这里说的去掉Cookie并不是用户端收到的页面就不含Cookie了,而是说,在缓存的静态数据中不含有Cookie。

分离出动态内容之后,如何组织这些内容页就变得非常关键了。这里我要提醒你一点,因为这其中很多动态内容都会被页面中的其他模块用到,如判断该用户是否已登录、用户ID是否匹配等,所以这个时候我们应该将这些信息JSON化(用JSON格式组织这些数据),以方便前端获取。

前面我们介绍里用缓存的方式来处理静态数据。而动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和CSI(Client Side Include)方案。

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

3.2.3 动静分离的几种架构方案

前面我们通过改造把静态数据和动态数据做了分离,那么如何在系统架构上进一步对这些动态和静态数据重新组合,再完整地输出给用户呢?

这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度,有3种方案可选:

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

方案1:实体机单机部署

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

这里我给出了实体机单机部署方案的结构图,如下:

图2 Nginx+Cache+Java结构实体机单机部署

实体机单机部署有以下几个优点:

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

这个方案中,虽然把通常只需要虚拟机或者容器运行的Java应用换成实体机,优势很明显,它会增加单机的内存容量,但是一定程度上也造成了CPU的浪费,因为单个的Java进程很难用完整个实体机的CPU。

另外就是,一个实体机上部署了Java应用又作为Cache来使用,这造成了运维上的高复杂度,所以这是一个折中的方案。如果你的公司里,没有更多的系统有类似需求,那么这样做也比较合适,如果你们有多个业务系统都有静态化改造的需求,那还是建议把Cache层单独抽出来公用比较合理,如下面的方案2所示。

方案2:统一Cache层

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

图3 统一Cache

将Cache层单独拿出来统一管理可以减少运维成本,同时也方便接入其他静态化系统。此外,它还有一些优点。

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

这种方案虽然维护上更方便了,但是也带来了其他一些问题,比如缓存更加集中,导致:

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

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

方案3:上CDN

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

但是要想这么做,有以下几个问题需要解决。

  1. 失效问题。前面我们也有提到过缓存时效的问题,不知道你有没有理解,我再来解释一下。谈到静态数据时,我说过一个关键词叫“相对不变”,它的言外之意是“可能会变化”。比如一篇文章,现在不变,但如果你发现个错别字,是不是就会变化了?如果你的缓存时效很长,那用户端在很长一段时间内看到的都是错的。所以,这个方案中也是,我们需要保证CDN可以在秒级时间内,让分布在全国各地的Cache同时失效,这对CDN的失效系统要求很高。
  2. 命中率问题。Cache最重要的一个衡量指标就是“高命中率”,不然Cache的存在就失去了意义。同样,如果将数据全部放到全国的CDN上,必然导致Cache分散,而Cache分散又会导致访问请求命中同一个Cache的可能性降低,那么命中率就成为一个问题。
  3. 发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且你还要考虑有问题时快速回滚和排查问题的简便性。

从前面的分析来看,将商品详情系统放到全国的所有CDN节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:

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

最后,还有一点也很重要,那就是:节点不要太多。

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

图4 CDN化部署方案

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

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

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

这样就把90%的静态数据缓存在了用户端或者CDN上,当真正秒杀时,用户只需要点击特殊的“刷新抢宝”按钮,而不需要刷新整个页面。这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据。

秒杀的动态数据和普通详情页面的动态数据相比更少,性能也提升了3倍以上。所以“抢宝”这种设计思路,让我们不用刷新页面就能够很好地请求到服务端最新的动态数据。

3.2 如何才能做好动静分离?有哪些方案可选?

如果你在一个业务飞速发展的公司里,并且你在深度参与公司内类秒杀类系统的架构或者开发工作,那么你迟早会想到动静分离的方案。为什么?很简单,秒杀的场景中,对于系统的要求其实就三个字:快、准、稳

那怎么才能“快”起来呢?我觉得抽象起来讲,就只有两点,一点是提高单次请求的效率,一点是减少没必要的请求。今天我们聊到的“动静分离”其实就是瞄着这个大方向去的。

二、粉丝福利

  • 我根据我从小白到架构师多年的学习经验整理出来了一份50W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴斯我一下,或者评论区扣“求分享

java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值