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