目录
学习前言
一、基本介绍
二、解决方案
1. 秒杀中的削峰
方案一:消息队列
答题器
数据过滤
2. 秒杀中的服务性能优化
三、知识小结
四、参考文献
学习前言
提示:这个章节会重新梳理。
一、基本介绍
秒杀请求在高度集中在某一个时间点。这样一来,就会导致一 个特别高的流量峰值,它对资源的
消耗是瞬时的 。
能够抢到商品的人数是有限的,也就是说10人和1000人发 起请求的结果都是一样的。也就是说真
正开始下单时,秒杀请求并不是越多越好。
二、解决方案
1. 秒杀中的削峰
由于服务器的处理资源是恒定的,用或者不用它的处理能力都是一样的,出现峰值的话,很容易导致忙到处理不
过来,闲的时候却又没有什么要处理。为了保证服务质量,很多处理资源只能按照忙时预估,而这会导致资源浪
费。 削峰可以让服务端处理变得更加平稳,还可以节省服务器的资源成本。针对秒杀这一场景,削峰从本质上来
说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求。
常见秒杀流量削峰的一些操作思路:消息队列、答题器、数据过滤。
方案一:消息队列
其中最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,通过队
列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去,在这里,消息队列就像“水库"一样,拦蓄上游
的洪水,削减进入下游的洪峰流量。
但是,如果流量峰值持续时间达到了消息队列的处理上限,消息队列同样也会被压垮,这样虽然保护了下游的系
统,但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时,即使是有水库恐怕也无济于事。在这种情况
下,我们要把“一步的操作”变成“两步的操作”,其中增加的操作用来起到缓冲的作用,例如利用线程池加锁
等待、采用先进先出、先进后出等常用的内存排队算法。
答题器
添加答题器第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊,第二个目的就是延缓请求,起到削峰的作
用。把请求的时间从瞬时延长到了几秒,这样会大大减轻对服务器的压力。而且后续请求到达服务器时已经没有
库存了,真正的并发处理就很有限了。
答题器生成的题目不需要很复杂,为了防止被破解可以添加图片噪点。同时在CDN上缓存图片,避免成为秒杀活
动中的短板,影响用户体验。
数据过滤
这里提到的数据过滤有点像某些企业在招聘时,把简历随机抽出一部分扔掉一样,只不过抽取的过程可以设置一
定的规则,过滤掉那些无效的请求。在不同的处理层根据不同的规则有效的过滤,例如对写数据进行基于时间的
合理分片,过滤掉过期的失效请求;对写数据进行强一致性校验,只保留最后有效的数据。
这么做的目的是在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提
前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等;
在写数据系统中,主要对写的数据做一致性检查,最后在数据库层保证数据的最终准确性。
2. 秒杀中的服务性能优化
服务端性能, 一般用QPS来衡量, 还有一个和QPS息息相关的是响应时间, 它可以理解为服务器处理响应的耗
时。正常情况下响应时间越短, 一秒钟处理的请求数(QPS) 自然也就会越多, 这在单线程处理的情况下看起来是
线性的关系,即我们只要把每个请求的响应时间降到最低,那么性能就会最高。
这个两个因素到底会造成什么样的影响?首先, 我们先来看看响应时间和QPS的关系,对于大部分的Web系统而
言响应时间一般都是由CPU执行时间和线程等待时间组成,也许你会说为什么不去减少这种等待时间,其实减少
线程等待时间对提升性能的影响没有我们想象得那么大, 这点在很多代理服务器上可以做验证,如果代理服务器
本身没有CPU消耗, 我们在每次给代理服务器代理的请求加个延时, 即增加响应时间,这对代理服务器本身的吞
吐量并没有多大的影响,因为代理服务器本身的资源并没有被消耗。
真正对性能有影响的是CPU的执行时间, 因为CPU的执行真正消耗了服务器的资源, 我们应该致力于减少CPU的
执行时间。
对于Java系统可优化的地方很多,除了常见的代码优化外,以下的内容值得注意。
Java和通用的Web服务器相比,在处理大并发的HTTP请求时要弱一点, 所以一般我们都会对大流量的Web系统
做静态化改造,让大部分请求和数据直接在Nginx服务器或者Web代理服务器上直接返回 , 而Java层只需处理少
量数据的动态请求。
针对这些请求, 我们可以使用以下手段进行优化:
- 直接使用Servlet处理请求, 避免使用传统的MVC框架, 这样可以绕过一大堆复杂且用处不大的处理逻辑,
直接输出流数据。使用resp.getOutputStream)而不是resp.get Writer函数, 可以省掉一些不变字符数据的
编码, 从而提升性能。
- 数据输出时推荐使用JSON而不是模板引擎来输出页面。
- 集中式缓存为了保证命中率一般都会采用一致性Hash, 所以同一个key会落到同一台机器上。那么,该如何
彻底解决单点的瓶颈呢? 答案是采用应用层的Local Cache。你需要划分成动态数据和静态数据。
像商品中的标题和描述这些本身不变的数据,会在秒杀开始之前全量推送到缓存直到到秒杀结束。
像库存这类动态数据的方式缓存一定时间,失效后再去缓存拉取最新的。你可能还会有疑问:像库存这种频繁更
新的数据,一旦数据不一致,会不会导致超卖? 这就要用到前面介绍的读数据的分层原则了,读的场景可以允许一
定的脏数据,因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保
证最终的一致性。
三、知识小结
秒杀特点:
短时间内,大量用户涌入,集中读和写有限的库存。
解决方案:
层层拦截,将请求尽量拦截在系统上游,避免将锁冲落到数据库上。
- 第一层:客户端优化
产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求; JS层面,限制用户在x秒之内
只能提交一次请求,比如微信摇一摇抢红包。 基本可以拦截80%的请求。
- 第二层:站点层面的请求拦截(nginx层,写流控模块)
怎么防止程序员写for循环调用,有去重依据么? IP? cookie-id? …想复杂了,这类业务都需要登录,用uid即可。
在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但
最简单,比如guava本地缓存)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。 对于5s内
的无效请求,统一返回错误提示或错误页面。
这个方式拦住了写for循环发HTTP请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个
uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不
住了。
- 第三层:服务层拦截
方案一:写请求放到队列中,每次只透有限的写请求到数据层,如果成功了再放下一批,直到库存不够,队列里
的写请求全部返回“已售完”。
方案二:或采用漏斗机制,只放一倍的流量进来,多余的返回“已售完”,把写压力转换成读压力。 读请求,用
cache,redis单机可以抗10W QPS,用异步线程定时更新缓存里的库存值。
还有提示“模糊化”,比如火车余票查询,票剩了58张,还是26张,你真的关注么,其实我们只关心有票和无
票。
- 第四层:数据库层
浏览器拦截了80%,站点层拦截了99.9%并做了页面缓存,服务层又做了写请求队列与数据缓存,每次透到数据库
层的请求都是可控的。 db基本就没什么压力了,通过自身锁机制来控制,避免出现超卖。
总结:
- 尽量将请求拦截在系统上游(越上游越好);
- 读多写少的多使用缓存(缓存抗读压力);