广州总部电话:020-85564311
广州总部电话:020-85564311

广州网站建设-小程序商城开发-广州小程序开发-企业微信开发公司-网站建设高端品牌-优网科技

19年
互联网应用服务商
请输入搜索关键词
知识库 知识库

优网知识库

探索行业前沿,共享知识宝库

3. 如何设计一个秒杀系统
发布日期:2025-01-30 15:45:08 浏览次数: 824 来源:疯狂打码中

一、前言

秒杀系统,作为电商领域的常见现象,其背后蕴含着丰富的可扩展性和核心系统关联。能够透彻解析秒杀机制,无疑标志着对电商业务的深刻理解,实战操作自然游刃有余。而若能精准捕捉要点,清晰阐述所面临的挑战,即便是在常见技术栈的应用上,也能游刃有余。

在探讨秒杀系统时,关键在于聚焦思维,引导话题。秒杀的应对策略繁多,有些适用于小规模的灵活应对,往往依赖Redis等工具来扛压。而有些则需进行系统性的构建,全方位考量。面试中,尤为重要的是与面试官产生共鸣,引导对话深入至自身擅长的领域,而非任由面试官引领至陌生的地带,迷失方向。

二、背景

提及秒杀,想必你已耳熟能详。近年来,从双十一的购物狂欢,到春节的红包大战,再到12306的火车票抢购,秒杀场景无处不在。简而言之,秒杀即在同一时刻,众多请求竞相购买同一商品并完成交易的过程,技术层面而言,即面对海量的并发读写请求。

并发,这一编程领域的棘手难题,无论何种语言都难以幸免。同样,对于软件而言亦是如此。构建一个秒杀系统或许易如反掌,但确保其能承受高并发访问则绝非易事。例如,如何确保系统面对百万级的请求洪流依然稳健如初?如何在高并发环境下保证数据的一致性写入?单纯依靠增加服务器数量显然并非上策。

在我看来,秒杀系统本质上是一个追求大并发、高性能与高可用性的分布式系统典范。今日,我们便一同探讨,在构建一个稳健的分布式系统基础之上,如何针对秒杀业务实现极致的性能优化。


三、深入理解秒杀

如何更深刻地把握秒杀系统的精髓呢?作为程序员,你首要的任务是从宏观视角出发,全面审视问题。在我看来,

秒杀的核心在于两大挑战:并发读与并发写

。针对并发读,优化的精髓在于减少用户向服务端发起的数据读取请求,或是让他们读取的数据量尽可能精简;而并发写的处理策略亦同,它要求我们在数据库层面单独设立一个库,进行特殊处理。此外,为秒杀系统构建防护机制,设计应急方案以应对突发状况,防止最坏结果的发生,同样至关重要。

从架构师的角度审视,构建一个能够承受超大流量并发读写、兼具高性能与高可用性的系统,需在用户请求路径的每一个环节,从浏览器至服务端,遵循以下原则:确保用户请求的数据量最小化、请求次数最小化、路径最短化、依赖最少化,并避免单点故障。这些关键要素,我将在后续文章中逐一详述。

秒杀系统的整体架构,可精炼地概括为“稳、准、快”三大关键词。

“稳”,即系统架构需确保高可用。在流量符合预期时,系统应稳健运行;即便流量超出预期,也需从容应对,确保秒杀活动圆满完成,商品顺利售出,这是最基本的前提。

“准”,则意味着秒杀活动的精准执行。例如,秒杀10台iPhone,就必须严格成交10台,多一台或少一台皆不可。库存的准确无误至关重要,一旦出错,平台将蒙受损失。因此,“准”要求数据的一致性得到绝对保障。

至于“快”,其含义不言而喻。它要求系统性能卓越,足以支撑庞大的流量。这不仅要求服务端进行极致的性能优化,还需在整个请求链路上进行协同优化。每个环节都需提速,如此,整个系统方能趋于完美。


四、稳·准·快

从技术视角审视,“稳、准、快”恰好对应了架构中的高可用、一致性和高性能三大核心要求。本专栏将紧密围绕这三个维度展开深入探讨,具体内容安排如下。

 高性能

秒杀活动伴随着海量的并发读和并发写操作,因此,如何支撑高并发访问成为了一项至关重要的任务。本专栏将细致剖析数据的动静分离策略、热点的精准识别与有效隔离、请求的削峰填谷与分层过滤机制,以及服务端的极致性能优化等四大方面,为你呈现一场高性能架构的盛宴。

 一致性

秒杀过程中商品库存的减少方式同样至关重要。试想,有限数量的商品在同一时刻遭遇数倍于己的请求同时削减库存,无论是“拍下即减库存”、“付款后减库存”,还是预扣库存等策略,在大并发更新的场景下,确保数据的准确无误无疑是一项极具挑战性的任务。为此,我将专门撰写一篇文章,深入剖析秒杀减库存方案的设计精髓。

 高可用

尽管我们已经提出了诸多极致的优化策略,但现实世界中总难免存在一些我们无法预见的突发情况。为了确保系统的高可用性和正确性,我们还需要设计一个Plan B作为兜底方案,以便在最坏情况发生时依然能够从容应对。在专栏的尾声部分,我将引领你一同思考,究竟可以从哪些关键环节入手,设计出既可靠又高效的兜底方案。


3.1 设计秒杀系统需遵循的五大架构原则

并发问题历来是程序员们的一大难题,无论采用何种编程语言皆如此。同样,对于软件而言,快速构建一个秒杀系统或许不难,但要让其能够支撑高并发访问则绝非易事。例如,如何确保系统在面对百万级请求流量时依然稳定运行?如何在高并发情境下保证数据的一致性写入?单纯依靠增加服务器数量显然并非最优解。

在我看来,

秒杀系统本质上是一个旨在满足大并发、高性能及高可用性的分布式系统

。接下来,我们将深入探讨如何在构建一个稳健的分布式系统基础上,针对秒杀业务场景进行极致的性能优化。

3.1.1 架构原则:“四要一不要”

身为架构师,在着手设计之前,你需首先勾勒出一个大致框架,思考如何构建一个能够承受超大流量并发读写、兼具高性能与高可用性的系统,这其中涉及哪些关键因素。我将这些要素精炼地总结为“四要一不要”。

  1. 数据要精简

所谓“数据要精简”,首要原则是用户请求的数据应尽可能少。这既包括上传至系统的数据,也涵盖系统返回给用户的数据(通常为网页内容)。

为何“数据要精简”呢?因为这些数据在网络上传输需要时间,同时无论是请求数据还是响应数据,服务器都需要进行处理。而服务器在写入网络数据时,通常需进行压缩和字符编码,这些操作极为消耗CPU资源。因此,减少传输的数据量能够显著降低CPU的使用率。例如,我们可以简化秒杀页面的设计,去除不必要的装饰元素等。

此外,“数据要精简”还要求系统所依赖的数据应尽可能少,这涉及系统完成某些业务逻辑所需读取和保存的数据,这些数据通常与后台服务及数据库交互。调用其他服务会涉及数据的序列化和反序列化,这也是CPU资源的一大消耗点,同时会增加延迟。

而且,数据库本身容易成为性能瓶颈,因此与数据库的交互应尽可能减少,数据越简单、越小越好。

  1. 请求数要压缩

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

例如,减少请求数的一个常用实践是合并CSS和JavaScript文件,将多个JavaScript文件合并为一个文件,在URL中用逗号隔开。这种方式在服务端文件仍单独存放,但服务端有一个组件解析该URL,然后动态将这些文件合并返回。

  1. 路径要缩短

所谓“路径”,是指用户发出请求至返回数据这一过程中,所经过的中间节点数。这些节点可表示为系统或新的Socket连接(如代理服务器仅创建一个新的Socket连接来转发请求)。每经过一个节点,通常会产生一个新的Socket连接。

然而,每增加一个连接都会带来新的不确定性。从概率统计角度看,若一次请求经过5个节点,每个节点的可用性是99.9%,那么整个请求的可用性是99.9%的五次方,约等于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 不同场景下的架构实践案例:“秒杀”篇

前文我已概述了架构设计的若干原则,那么针对“秒杀”这一特定场景,何为出色的架构呢?接下来,我将以淘宝早期秒杀系统架构的演变历程为脉络,为你细致剖析,在不同请求量级下,我眼中的理想秒杀系统架构。

若你急于构建一个简易的秒杀系统,只需在商品购买页面增设“定时上架”功能,秒杀开始时方展示购买按钮,库存售罄即宣告活动结束(此处难点在于,如何实现时钟同步)。这便是秒杀系统1.0版本的实现策略。然而,随着请求量的激增(譬如从1万次/秒跃升至10万次/秒),该简易架构迅速触及瓶颈,亟需架构重构以提升系统效能。重构要点涵盖:

  1. 剥离秒杀功能,独立打造秒杀系统,以便实施针对性优化,如精简店铺装修功能,降低页面复杂度;

  2. 部署层面亦需独立,设立秒杀专用机器集群,确保秒杀流量不会冲击正常商品购买集群的负载;

  3. 将热点数据(诸如库存数据)独立存放于缓存系统中,大幅提升“读性能”;

  4. 增设秒杀答题环节,有效抵御秒杀器的恶意抢单行为。

此刻,系统架构已蜕变为下图所示形态。核心在于,秒杀详情已成为一个全新的独立系统,核心数据被妥善安置于缓存(Cache)之中,其他关联系统亦均以独立集群的方式部署,各司其职。


图 1 改造后的系统架构


然而,即便经过上述优化,该架构依然难以承受超过100万次/秒的高并发请求。为了进一步拔高秒杀系统的性能上限,我们又对架构进行了深度升级,具体措施包括:

  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请求头也无需解析。


图 1 静态化改造

第三,选择何者来缓存静态数据同样至关重要。不同编程语言开发的Cache软件,在处理缓存数据时的效率存在显著差异。以Java为例,鉴于Java系统自身存在的局限(诸如不善于应对大量连接请求、每个连接所占用的内存较多、Servlet容器解析HTTP协议的速度较慢等),你可以选择不在Java层面进行缓存,而是直接在Web服务器层面实施,以此来规避Java语言层面的一些短板。相比之下,Web服务器(例如Nginx、Apache、Varnish)在处理高并发的静态文件请求时更为得心应手。

3.2.2 动静分离改造的实操指南

在洞悉了动静态数据的“缘由”与“内涵”之后,接下来我们将聚焦于“方法”。如何将动态页面改造成适宜缓存的静态页面呢?其实并不复杂,只需去除前文提及的几个影响因素,将它们单独剥离出来,实现动静分离即可。

接下来,我将以典型的商品详情系统为例,为你详细解读。你可以先打开京东或淘宝的商品详情页,观察这个页面中包含了哪些动静数据。我们将从以下五个方面着手,分离出动态内容:

  1. URL唯一化。商品详情系统天生具备URL唯一化的特性,比如每个商品都由一个唯一的ID来标识,那么http://item.xxx.com/item.htm?id=xxxx就可以作为该商品的唯一URL。为什么要实现URL唯一化呢?因为我们要缓存的是整个HTTP连接,而URL正是作为缓存的Key。例如,我们可以以id=xxx的格式来区分不同的缓存内容。

  2. 剥离浏览者相关因素。浏览者相关的因素,如是否已登录以及登录身份等,我们可以将它们单独拆分出来,通过动态请求来获取。

  3. 分离时间因素。服务端输出的时间信息,同样通过动态请求来获取。

  4. 异步化处理地域因素。详情页面上与地域相关的因素,我们可以做成异步方式获取。当然,你也可以选择通过动态请求来获取,但在此场景下,异步获取的方式更为合适。

  5. 移除Cookie。服务端输出的页面中包含的Cookie,我们可以通过代码软件来删除。例如,Web服务器Varnish可以通过unset req.http.cookie命令来去除Cookie。需要注意的是,这里所说的去除Cookie,并不是指用户端收到的页面就不包含Cookie了,而是指在缓存的静态数据中不包含Cookie。

在分离出动态内容之后,如何合理地组织这些内容就变得尤为关键。由于很多动态内容都会被页面中的其他模块所引用,如判断用户是否已登录、用户ID是否匹配等,因此我们应该将这些信息以JSON格式进行组织,以便于前端获取。

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

  1. ESI方案(或SSI方案):即在Web代理服务器上发起动态内容请求,并将请求的结果插入到静态页面中。当用户获取到页面时,它已经是一个完整的页面了。这种方式虽然会对服务端性能产生一定影响,但能够为用户提供更佳的体验。

  2. CSI方案:即单独发起一个异步的JavaScript请求,以向服务端获取动态内容。这种方式能够减轻服务端的性能负担,但用户端页面可能会出现一定的延迟,体验相对稍差。


3.2.3 动静分离的几种架构策略

此前,我们已通过一系列改造实现了静态数据与动态数据的分离。那么,在系统架构层面,我们又该如何重新整合这些数据,并完整无误地呈现给用户呢?

这就要求我们需对用户请求路径进行科学合理的架构设计。依据架构的复杂程度,我们有三种方案可供选择:

  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集群。这一方案不仅更为理想,而且更具推广价值。其结构图如下所示:


图 3 统一 Cache

将Cache层独立出来进行统一管理,能够显著降低运维成本,并便于其他静态化系统的接入。此外,它还具有以下显著优势:

  1. 通过设立单独的Cache层,能够减少多个应用在接入时使用Cache的成本。接入的应用只需专注于维护自身的Java系统,无需单独维护Cache,而只需关注如何高效利用即可。

  2. 统一Cache的方案在维护上更为简便,例如后续的监控加强、配置自动化等,只需一套解决方案即可完成,统一维护升级也更为便捷。

  3. 该方案能够实现内存共享,最大化地利用内存资源。不同系统间的内存可以灵活切换,从而有效应对各类攻击。然而,虽然维护上更为简便,但也带来了一些新的问题,如缓存更加集中,可能导致:

  4. Cache层内部的交换网络成为性能瓶颈;

  5. 缓存服务器的网卡也成为制约因素;

  6. 由于机器数量较少,一旦某台机器出现故障,将影响大量缓存数据的正常访问。

为了解决上述问题,可以对Cache进行Hash分组,即每组Cache缓存相同的内容,以避免热点数据过度集中导致新的性能瓶颈出现。

方案3:部署CDN

在系统实现动静分离后,我们自然会想到更进一步的优化方案,即将Cache进一步前移至CDN上,因为CDN离用户最近,能够带来更好的效果。但要实现这一点,需要解决以下几个关键问题:

  1. 失效问题。之前我们已提及缓存时效的问题,这里再详细解释一下。静态数据虽然“相对不变”,但并非“绝对不变”。例如,一篇文章现在可能不变,但一旦发现错别字,就会进行修改。如果缓存时效过长,用户在很长一段时间内看到的都可能是错误的内容。因此,在这个方案中,我们需要确保CDN能够在秒级时间内,让分布在全国各地的Cache同时失效,这对CDN的失效系统提出了很高的要求。

  2. 命中率问题。Cache的一个关键衡量指标就是“高命中率”,否则Cache的存在就失去了意义。同样地,如果将数据全部放置在全国的CDN上,必然会导致Cache分散,而Cache分散又会降低访问请求命中同一个Cache的可能性,从而影响命中率。

  3. 发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,同时还需要考虑快速回滚和便捷排查问题。从前面的分析来看,将商品详情系统放置在全国的所有CDN节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点进行尝试实施呢?答案是可以的,但这样的节点需要满足以下条件:

  4. 位于访问量较为集中的地区;

  5. 离主站相对较远;

  6. 节点到主站间的网络质量良好且稳定;

  7. 节点容量较大,不会占用其他CDN过多的资源。

最后,还有一点非常重要,那就是节点的数量不宜过多。

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


图 4 CDN 化部署方案

使用CDN的二级Cache作为缓存方案,其命中率可媲美当前的服务端静态化Cache。由于节点数量有限,Cache分布相对集中,且访问量也高度集中,这不仅解决了命中率问题,同时也为用户带来了极佳的访问体验。因此,该方案被视为当前较为理想的CDN化部署策略。

此外,CDN化部署方案还展现出以下鲜明特点:

  1. 能够将整个页面内容缓存在用户的浏览器中,进一步提升访问效率。

  2. 即便用户强制刷新整个页面,请求也会首先指向CDN,从而减轻源站压力。

  3. 实际的有效请求,主要聚焦于用户对“刷新抢宝”按钮的点击操作,这极大地减少了无效请求的数量。

通过上述方式,90%的静态数据被有效缓存在用户端或CDN上。在秒杀活动进行时,用户只需点击特制的“刷新抢宝”按钮,而无需刷新整个页面。这样,系统仅需向服务端请求极少量的有效数据,避免了重复请求大量静态数据的开销。相较于普通的详情页面动态数据,秒杀活动的动态数据更为精简,系统性能也因此提升了3倍以上。这种“抢宝”设计思路,使用户无需刷新页面即可轻松获取服务端最新的动态数据。

3.3 精准应对系统的“热点数据”挑战

设想一下,如果系统中存储着数十亿甚至上百亿的商品,且每天有数千万的商品被上亿用户访问,那么其中必然有一部分商品因受到大量用户的青睐而成为“热点商品”。

这些热点商品中的极端案例便是秒杀商品,它们在极短的时间内被大量用户进行访问、加入购物车、下单等操作,这些操作我们称之为“热点操作”。那么,这些热点对系统究竟会产生哪些影响?我们为何要对它们给予特别关注?

3.3.1 关注热点的必要性

我们必须密切关注热点,因为热点会对系统产生深远的影响。首先,热点请求会大量占用服务器资源,尽管这些热点请求可能仅占请求总量的极小部分,但它们却可能占据服务器90%以上的资源。如果这些热点请求是无价值的无效请求,那么对系统资源而言将是极大的浪费。

其次,即使这些热点请求是有效的,我们也需要识别出来并进行针对性优化,以降低处理这些请求的成本。既然热点对系统如此重要,那么热点究竟包含哪些内容呢?

3.3.2 “热点”的界定

热点主要分为“热点操作”和“热点数据”两大类。所谓“热点操作”,如大量页面刷新、大量购物车添加、双十一零点的大量下单等,均属于此类。对于系统而言,这些操作可以抽象为“读请求”和“写请求”,这两种热点请求的处理策略截然不同,读请求的优化空间相对较大,而写请求的瓶颈则通常出现在存储层,优化的思路需根据CAP理论进行权衡,具体内容将在“减库存”一文中详述。

而“热点数据”则较为直观,即用户的热点请求所对应的数据。热点数据又可细分为“静态热点数据”和“动态热点数据”。

“静态热点数据”指的是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出热点商品,并对其进行打标。此外,我们还可以利用大数据分析来预测热点商品,如分析历史成交记录、用户购物车记录等,以发现哪些商品更受欢迎、更有可能成为热点。

“动态热点数据”则是指无法提前预测的、在系统运行过程中临时产生的热点数据。例如,卖家在抖音上投放广告后,商品突然走红,导致在短时间内被大量购买。由于热点操作是用户行为,我们难以改变,但可以进行一定的限制和保护。因此,本文将主要针对热点数据的优化进行介绍。

3.3.3 热点数据的发现

前文介绍了如何对单个秒杀商品的页面数据进行动静分离,以便对静态数据进行针对性优化。那么,如何发现这些秒杀商品,或者说如何准确识别热点商品呢?

你可能会说:“参加秒杀的商品自然就是秒杀商品啊。”没错,但关键在于系统如何自动识别哪些商品参加了秒杀活动。因此,我们需要建立一个机制来提前区分普通商品和秒杀商品。

我们从静态热点和动态热点的发现两个方面来探讨。

发现静态热点数据

如前文所述,静态热点数据可以通过商业手段进行预测,如强制卖家通过报名参加的方式提前筛选出热点商品。这通常通过运营系统实现,对参加活动的商品数据进行打标,并通过后台系统对这些热点商品进行预处理,如提前缓存。然而,这种提前筛选的方式也存在一些问题,如增加卖家的使用成本、实时性较差以及灵活性不足等。

除了提前报名筛选的方式外,我们还可以利用技术手段进行预测。例如,对买家每天访问的商品进行大数据计算,统计出TOP N的商品,这些商品即可视为热点商品。

发现动态热点数据

虽然我们可以通过卖家报名或大数据预测等手段提前发现静态热点数据,但这些方法的实时性相对较差。如果系统能够在秒级内自动发现热点商品,那将是一个巨大的优势。动态地实时发现热点不仅对秒杀商品有价值,对其他热卖商品也同样具有重要意义。因此,我们需要实现热点的动态发现功能。以下是一个动态热点发现系统的具体实现方案:

  1. 构建一个异步系统,用于收集交易链路上各个环节中的中间件产品的热点Key。这些中间件包括Nginx、缓存、RPC服务框架等(部分中间件已具备热点统计模块)。

  2. 建立一个热点上报和订阅规范,以确保交易链路上各个系统(如详情、购物车、交易、优惠、库存、物流等)能够及时共享热点信息。通过这一规范,上游系统发现的热点可以透传给下游系统,使下游系统能够提前做好保护。例如,在大促高峰期,详情系统通常是最早感知到热点的系统,它可以通过Nginx模块统计热点URL,并将这些信息共享给其他系统。

  3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)即可根据这些信息知道哪些商品会被频繁调用,并采取相应的热点保护措施。

以下是一个示意图,展示了用户访问商品时经过的路径。我们主要依赖导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别出访问量高的商品,并通过这些系统中的中间件收集热点数据,并记录到日志中。


图 1 一个动态热点发现系统

我们通过部署在每台机器上的Agent,高效地将日志汇总至聚合与分析集群。随后,根据预设规则筛选出热点数据,借助订阅分发系统,精准推送至相应的系统中。这些热点数据既可以填充至Cache中,直接加速应用服务器的响应,也可以被下游系统灵活订阅,并根据各自需求进行定制化处理。在构建热点发现系统的过程中,我基于过往经验,归纳了以下几点关键注意事项:

  1. 热点服务后台在抓取热点数据日志时,应采用异步方式。这种方式不仅提升了系统的通用性,还确保了对业务系统和中间件产品主流程的无干扰运行。

  2. 热点服务发现机制与中间件自身的热点保护模块应并存不悖。每个中间件和应用仍需具备自我保护能力,而热点服务台则专注于热点数据的收集与订阅服务,实现各系统热点数据的透明化共享。

  3. 热点发现需实现接近实时(3秒内完成)的能力,以确保动态发现的及时性与有效性,为下游系统提供实时的保护屏障。

3.3.4 处理热点数据的策略

在处理热点数据时,我们通常采用以下几种策略:优化、限制与隔离。

优化:缓存热点数据是最有效的优化手段。对于动静分离的热点数据,可长期缓存静态部分。然而,更常见的做法是“临时”缓存,即利用有限长度的队列,无论是静态还是动态数据,均短暂缓存数秒钟,并采用LRU淘汰算法进行替换。

限制:限制策略更多地作为一种保护机制。例如,对被访问商品的ID进行一致性Hash分桶,每个分桶设置处理队列,以此限制热点商品请求,防止其占用过多服务器资源,影响其他请求的处理。

隔离:秒杀系统设计的首要原则是隔离热点数据,避免1%的请求影响剩余的99%。隔离后,可针对这1%的请求进行针对性优化。在秒杀业务中,隔离可在以下层次实现:

  • 业务隔离
    :将秒杀作为营销活动,卖家需单独报名参与。报名后,我们可预知热点,提前预热。
  • 系统隔离
    :通过分组部署,秒杀系统与其余99%的系统运行时隔离。秒杀可申请独立域名,确保请求落至不同集群。
  • 数据隔离
    :秒杀所调用的热点数据,如使用单独的Cache集群或MySQL数据库存储,以防少量数据影响整体。

此外,隔离措施还包括按用户区分、Cookie分配、接入层限流策略、服务层调用不同接口、数据层打标等,旨在区分热点请求与普通请求。

3.4 流量削峰策略

秒杀系统的流量监控图在秒杀开始时呈现直线上升,因请求高度集中于特定时间点,导致流量峰值极高,瞬时资源消耗巨大。然而,秒杀成功人数固定,并发度越高,无效请求越多。因此,需设计规则延缓并发请求,甚至过滤无效请求。

3.4.1 削峰的重要性

削峰旨在应对峰值带来的挑战。服务器处理资源恒定,峰值时易导致处理不过来,闲时则资源浪费。削峰使服务端处理平稳,节省资源成本。在秒杀场景中,削峰本质是延缓用户请求,减少和过滤无效请求,遵循“请求数尽量少”的原则。

以下介绍几种无损的流量削峰策略:排队、答题、分层过滤。当然,有损策略如限流、机器负载保护等也能达到削峰目的,但属于不得已措施,故不在此列。

3.4.2 排队策略

削峰最易想到的方案是使用消息队列缓冲瞬时流量。将同步调用转为异步推送,队列一端承接流量洪峰,另一端平滑推送消息。消息队列如“水库”,拦蓄上游洪水,削减下游洪峰,减免灾害。此方案示意图如下:


图 1 用消息队列来缓冲瞬时流量

然而,当流量峰值持续时间较长,以至于达到消息队列的处理极限,比如本机消息积压触及存储空间的天花板,消息队列同样可能不堪重负。这时,尽管下游系统得到了保护,但实际效果与直接丢弃请求无异。正如洪水肆虐之时,即便是水库也难以抵挡。除了消息队列,还有许多类似的排队策略可供选择:

  1. 利用线程池加锁等待,这是一种常见的排队机制,通过锁定线程来维护请求的顺序;

  2. 内存排队算法,如先进先出(FIFO)和先进后出(LIFO)等,这些算法在内存中实现请求的排队;

  3. 将请求序列化到文件中,再按顺序读取文件(例如,借鉴MySQL binlog的同步原理)以恢复请求。

这些策略的共同点在于,都将原本一步完成的操作拆分为两步,中间增加了一个缓冲步骤。或许你会质疑,这样的做法增加了请求的处理路径,似乎与我们提倡的“4要1不要”原则相悖。诚然,从表面上看这并不合理,但在某些极端场景下,若不加缓冲,系统可能会直接崩溃。因此,在实际应用中,我们需要找到一种平衡,做出必要的妥协。

3.4.3 答题机制

你是否还记得,早期的秒杀活动只是简单地刷新页面并点击购买按钮,而答题环节是后来才加入的。那么,为何要引入答题机制呢?

这主要是为了提升购买的复杂度,从而实现两个核心目标。

首要目标是防止部分买家利用秒杀器作弊。在2011年秒杀活动风靡一时之际,秒杀器也泛滥成灾,导致活动未能达到全民参与和营销的预期效果。因此,系统引入了答题机制来遏制秒杀器的使用。加入答题后,下单时间通常被控制在2秒之后,秒杀器的下单比例也大幅下降。答题页面的示意图如下所示。


图 2 答题页面

第二个目的,实质上在于延缓请求,有效对请求流量进行削峰,使系统能够更加从容地应对瞬时的流量高峰。这一关键环节的作用,在于将原本集中在1秒之内的峰值下单请求拉长至2秒至10秒之间。如此一来,请求峰值便在时间轴上得到了有效分散。这一时间分片策略对于服务端处理并发请求至关重要,能够极大地缓解其压力。同时,由于请求遵循先后顺序,当后续请求到达时,往往已无库存可供购买,因此这些请求根本无法进入最后的下单环节,从而大大减少了真正的并发写操作。这种设计思路在当今应用中极为普遍,例如支付宝曾经的“咻一咻”活动以及微信的“摇一摇”功能,都采用了类似的机制。

接下来,我将重点阐述秒杀答题的设计逻辑。


图 3 秒杀答题

如上图所示,整个秒杀答题的流程精妙地分为三大核心模块:

  1. 题库生成模块

    此模块的核心职责是生成一系列问题与答案对。值得注意的是,题目与答案本身无需过于复杂,关键在于确保它们难以被机器快速解析,从而有效抵御秒杀器的自动化答题攻击。

  2. 题库推送模块

    在秒杀答题活动正式开始前,此模块负责将题目提前推送至详情系统与交易系统。这一步骤至关重要,它确保了每位用户接收到的题目都是独一无二的,进一步筑牢了防作弊的防线。

  3. 题目图片生成模块

    此模块负责将题目转化为图片格式,并在图片中巧妙融入干扰元素。这一设计同样旨在抵御机器的自动化答题,因为只有人类才能准确理解并解答题目中的真正含义。

此外,还需特别留意的是,鉴于秒杀答题期间网络流量的激增,我们应提前将题目图片推送至CDN并进行预热处理。否则,当用户真正发起请求时,可能会遭遇图片加载缓慢的问题,从而严重影响答题体验。

至于答题逻辑本身,则显得相对直观且易于理解:当用户提交的答案与题目预设的答案相匹配时,即可顺利进入下一步的下单流程;反之,则答题失败。为了增强安全性与唯一性,我们可以采用MD5加密技术对问题与答案进行编码处理,具体密钥设计如下:

  • 问题密钥
    userId+itemId+questionId+timestamp+PK
  • 答案密钥
    userId+itemId+answer+PK

验证逻辑的具体流程,请参见下图所示。


图 4 答题的验证逻辑

请注意,此处的验证逻辑不仅涵盖了对问题答案的核实,还深入到了用户身份的校验层面,诸如确认用户是否已登录、用户的Cookie信息是否完整无缺,以及检测用户是否存在重复或频繁的提交行为等。

在确保答案正确性的基础上,我们亦可对提交答案的时间窗口加以限制,例如,从用户开始答题到系统接受答案的时间需超过1秒。这是因为,在极短的时间内(小于1秒)完成答题,很大程度上超出了人为操作的可能性范畴,此举同样能有效防范机器答题的潜在风险。

3.4.4 分层过滤机制

前文提及的排队与答题策略,要么旨在减少请求的发送量,要么对发出的请求进行缓冲处理。而在秒杀场景中,还存在一种行之有效的方法——分层过滤,它旨在剔除那些无效的请求。分层过滤机制,顾名思义,就是采用“漏斗”式的精妙设计来处理纷至沓来的请求,其运作原理如下图所示。


图 5 分层过滤

假如请求依次穿越 CDN、前台读系统(例如商品详情系统)、后台系统(例如交易系统)直至数据库这几层架构,那么整个流程将如下所述进行分层过滤:

 在最初层——用户浏览器或 CDN 层,大部分数据与流量即被捕获,此层能够拦截并满足大部分数据的读取需求;

 进入第二层——前台系统时,数据(尤其是强一致性要求的数据)尽可能通过 Cache 路由,以此剔除部分无效请求,确保数据的高效访问;

 抵达第三层——后台系统,主要承担数据的二次校验任务,同时实施系统保护与限流策略,进一步缩减数据量与请求规模;

 最终在数据层,执行数据的强一致性验证,确保数据的准确无误。

此过程形同漏斗,逐层递减数据量与请求量。分层过滤的核心精髓在于:于不同层级最大限度地剔除无效请求,确保“漏斗”末端留存的是纯粹的有效请求。为达成此效果,数据的分层校验不可或缺。分层校验的基本原则如下:

  1. 将动态请求的读数据缓存至 Web 端,预先排除无效的数据读取请求;

  2. 对读数据实施非强一致性校验,规避因一致性校验产生的瓶颈问题;

  3. 对写数据进行基于时间的合理分片处理,剔除过期的无效请求;

  4. 对写请求实施限流保护策略,屏蔽超出系统承载能力的请求;

  5. 对写数据进行强一致性校验,确保仅保留最终有效的数据记录。

分层校验旨在读系统中,尽可能减少一致性校验带来的系统瓶颈,同时提前执行不影响性能的检查,例如用户秒杀资格验证、商品状态检查、用户答题正确性确认、秒杀活动状态、请求合法性以及营销资源是否充足等;在写数据系统中,则主要关注写数据(如“库存”)的一致性检查,最终在数据库层确保数据的终极准确性(如避免“库存”减至负数)。

3.5 影响性能的关键因素及系统性能提升策略

欲提升系统性能,首要之务是明确哪些因素对系统性能影响最为显著,随后针对这些具体因素制定优化策略,此逻辑无疑至关重要。

那么,究竟哪些因素会对性能产生影响呢?在深入探讨之前,我们先对“性能”进行界定,不同服务设备对性能的定义存在差异,例如 CPU 主要关注主频,磁盘则侧重于 IOPS(每秒输入/输出操作次数)。

今日我们聚焦于系统服务端性能,通常以 QPS(每秒查询数)作为衡量标准,而响应时间(RT)与 QPS 息息相关,它反映了服务器处理响应的耗时。

通常情况下,响应时间越短,一秒钟内能处理的请求数自然越多,在单线程处理模式下,这看似呈现线性关系,即只要将每个请求的响应时间降至最低,性能即可达到最优。

然而,响应时间存在极限,无法无限缩短,因此多线程处理请求的模式应运而生。理论上,“总 QPS =(1000ms / 响应时间)× 线程数量”,性能因此与两个因素紧密相关:单次响应的服务端耗时与处理请求的线程数。接下来,我们共同探究这两个因素的具体影响。

首先,我们分析响应时间与 QPS 的关系。

对于大多数 Web 系统而言,响应时间由 CPU 执行时间与线程等待时间(如 RPC、IO 等待、Sleep、Wait 等)构成,即服务器在处理请求时,一部分时间是 CPU 在执行运算,另一部分时间则处于各种等待状态。

理解了服务器处理请求的机制后,你可能会问,为何我们不努力减少这种等待时间呢?遗憾的是,根据我们的实际测试,减少线程等待时间对性能提升的影响远不及我们预期的那般显著,它并非线性提升。这一点在许多代理服务器上可以得到验证。

如果代理服务器本身不消耗 CPU 资源,我们在每次代理的请求中增加延时,即延长响应时间,但这对代理服务器的吞吐量影响甚微,因为代理服务器本身的资源并未被充分占用,可以通过增加处理线程数来弥补响应时间对代理服务器 QPS 的影响。

实际上,真正对性能产生决定性影响的是 CPU 的执行时间。这不难理解,因为 CPU 的执行真正消耗了服务器的资源。经过实际测试,如果 CPU 执行时间减半,QPS 即可翻倍。因此,我们应致力于缩减 CPU 的执行时间。

其次,我们再来探讨线程数对 QPS 的影响。

仅从“总 QPS”的计算公式来看,线程数似乎越多,QPS 就越高,但这真的始终正确吗?显然不是,线程数并非越多越好,因为线程本身也占用资源,并受到其他因素的制约。例如,线程增多会增加系统的线程切换成本,且每个线程都会占用一定内存。那么,如何设置最合理的线程数呢?其实,

许多多线程场景都存在一个默认配置,即“线程数 = 2 * CPU 核数 + 1”。除此之外,还有一个基于最佳实践得出的公式:


线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量


当然,寻找最佳线程数的最佳途径是通过性能测试。换言之,要提升性能,我们既要设法缩减 CPU 的执行时间,又要设定一个合理的并发线程数,双管齐下,从而显著提升服务器的性能。

现在,你已经掌握了快速提升性能的方法,接下来你可能会问,我该如何定位系统中 CPU 资源消耗最大的部分呢?

3.5.1 如何发掘性能瓶颈

服务器性能瓶颈可能潜藏于多个环节,如 CPU、内存、磁盘以及网络等。此外,不同系统对瓶颈的关注点也各不相同,例如,缓存系统的瓶颈往往是内存,而存储型系统的瓶颈则更可能是 I/O。

在本专栏中,我们聚焦的秒杀场景,其瓶颈更多地聚焦于 CPU。

那么,如何精准定位 CPU 的瓶颈呢?其实,有多种 CPU 诊断工具能助我们一臂之力,其中 JProfiler 和 Yourkit 尤为常用。它们能够详尽列出整个请求中每个函数的 CPU 执行时间,让我们一目了然哪个函数消耗的 CPU 时间最多,从而有的放矢地进行优化。

当然,还有一些方法能够近似地统计 CPU 的耗时,例如通过 jstack 定时打印调用栈。如果某些函数调用频繁或耗时较长,它们就会频繁出现在系统调用栈中,这种采样方式同样能帮助我们发现耗时较多的函数。

尽管秒杀系统的瓶颈大多集中在 CPU,但这并不意味着其他方面就高枕无忧。例如,当海量请求如潮水般涌来,且页面较大时,网络就可能成为瓶颈。

如何简单判断 CPU 是否为瓶颈呢?一个有效的方法是,当 QPS 达到极限时,观察服务器的 CPU 使用率是否超过 95%。如果未达到,那么 CPU 还有潜力可挖,可能是受到锁限制,或是存在过多的本地 I/O 等待。现在,你既已知晓需要优化的因素,又发现了瓶颈所在,接下来就该关注如何优化了。

3.5.2 系统优化策略

Java 系统优化空间广阔,这里我重点介绍几种高效手段供你参考:减少编码、减少序列化、Java 极致优化以及并发读优化。接下来,我们逐一探讨。

  1. 减少编码

Java 编码运行较慢,这无疑是 Java 的一大短板。在许多场景下,涉及字符串的操作(如输入输出操作、I/O 操作)都颇为耗 CPU 资源,无论是磁盘 I/O 还是网络 I/O,都需要将字符转换成字节,这个转换过程必须依赖编码。

每个字符的编码都需要查表,而这种查表操作非常消耗资源。因此,减少字符到字节或相反的转换,即减少字符编码,将极大提升性能。

那么,如何减少编码呢?例如,网页输出可以直接采用流输出,即利用 resp.getOutputStream() 函数写数据。将静态数据提前转化为字节,在真正输出时直接使用 OutputStream() 函数,从而减少静态数据的编码转换。我在《深入分析 Java Web 技术内幕》一书中介绍的“Velocity 优化实践”章节,正是通过提前将静态字符串编码成字节并缓存,然后直接输出字节内容到页面,从而大幅降低了编码的性能消耗,网页输出的性能相比未提前进行字符到字节转换时提升了约 30%。

  1. 减少序列化

序列化同样是 Java 性能的一大障碍,减少 Java 中的序列化操作也能显著提升性能。由于序列化往往与编码同时发生,因此减少序列化也就意味着减少了编码。序列化大多发生在 RPC 中,所以避免或减少 RPC 就能减少序列化。当然,当前的序列化协议已进行了诸多优化以提升性能。还有一种新方案,即将多个关联性较强的应用进行“合并部署”,从而减少不同应用之间的 RPC,进而减少序列化的消耗。

所谓“合并部署”,就是将原本部署在不同机器上的两个应用合并到同一台机器上,并且不仅要在同一台机器上,还要在同一个 Tomcat 容器中,且不能通过本机的 Socket 通信,这样才能避免序列化的产生。针对秒杀场景,我们还可以采取更极致的优化手段,接下来我们探讨第 3 点:Java 极致优化。

  1. Java 极致优化

相较于通用的 Web 服务器(如 Nginx 或 Apache 服务器),Java 在处理大并发的 HTTP 请求时稍显逊色。因此,我们通常会对大流量的 Web 系统进行静态化改造,让大部分请求和数据直接在 Nginx 服务器或 Web 代理服务器(如 Varnish、Squid 等)上返回,从而减少数据的序列化与反序列化。Java 层仅需处理少量数据的动态请求。针对这些请求,我们可以采用以下优化手段:

直接使用 Servlet 处理请求,绕开传统的 MVC 框架,从而省去一大堆复杂且实用性不高的处理逻辑,节省约 1ms 时间(具体时间取决于你对 MVC 框架的依赖程度)。

直接输出流数据,使用 resp.getOutputStream() 而不是 resp.getWriter() 函数,可以省去一些不变字符数据的编码,从而提升性能;在数据输出时,推荐使用 JSON 而非模板引擎(通常都是解释执行)来输出页面。

  1. 并发读优化

或许有读者会觉得这个问题易如反掌,无非就是将数据放入 Tair 缓存中。集中式缓存为保证命中率,通常会采用一致性 Hash,因此同一个 key 会落在同一台机器上。尽管单台缓存机器能支撑高达 30w/s 的请求,但仍远不足以应对像“大秒”这种级别的热点商品。那么,该如何彻底解决单点的瓶颈呢?

答案是采用应用层的 LocalCache,即在秒杀系统的单机上缓存商品相关数据。那么,又该如何缓存数据呢?你需要将动态数据和静态数据分别处理:

像商品中的“标题”和“描述”这类不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束。

像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(通常是数秒),失效后再去缓存中拉取最新数据。

你可能还会有疑问:像库存这种频繁更新的数据,一旦数据不一致,是否会导致超卖?这就需要用到前面介绍的读数据的分层校验原则了。读的场景可以容忍一定的脏数据,因为这里的误判仅会导致少量原本无库存的下单请求被误认为有库存,可以在真正写数据时保证最终的一致性。通过在高可用性和一致性之间找到平衡,从而解决高并发的数据读取问题。

3.6 秒杀系统“减库存”设计的核心逻辑

如果要设计一套秒杀系统,你的老板肯定会先强调:千万别超卖,这是大前提。

如果你是秒杀系统的新手,可能会觉得库存 100 件就卖 100 件,在数据库中减到 0 不就完事了?理论上确实如此,但具体到业务场景中,“减库存”就没那么简单了。

例如,我们平时购物时,看到心仪的商品就下单,但并不是每个下单请求都会转化为付款。那么,系统是应该在用户下单时就算商品卖出,还是等到用户真正付款时才算卖出呢?这确实是个棘手的问题!

我们可以根据减库存是发生在下单阶段还是付款阶段,将减库存策略进行划分。

3.6.1 减库存的方式有哪些

在正常的电商平台购物场景中,用户的实际购买流程一般分为两步:下单和付款。你想买一部 iPhone 手机,在商品页面点击“立即购买”按钮,核对信息后点击“提交订单”,这一步就称为下单。下单后,只有真正完成付款才算真正购买,也就是常说的“尘埃落定”。

那么,如果你是架构师,会在哪个环节完成减库存的操作呢?总的来说,减库存操作一般有如下几种方式:

下单减库存,即买家下单后,立即在商品总库存中减去购买数量。这是最简单的减库存方式,也是控制最精确的一种。下单时通过数据库的事务机制控制商品库存,确保不会出现超卖。但要知道,有些买家下单后可能不会付款。

付款减库存,即买家下单后并不立即减库存,而是等到付款后才真正减库存。这种方式可能导致买家下单后付不了款,因为商品可能已被其他买家买走。

预扣库存,这种方式相对复杂。买家下单后,库存为其保留一定时间(如 10 分钟),超过时间则自动释放,释放后其他买家可继续购买。在买家付款前,系统会校验该订单的库存是否保留:若未保留,则再次尝试预扣;若库存不足(即预扣失败),则不允许继续付款;若预扣成功,则完成付款并实际减去库存。

以上几种减库存方式都存在一些问题,下面我们一起来分析。

3.6.2 减库存策略中潜在的问题剖析

在购物流程中,由于包含多个操作步骤,因此在不同步骤执行库存扣减操作时,可能会暴露出被恶意用户利用的漏洞,特别是恶意下单的现象。

若我们采取“下单即减库存”的策略,即用户一旦下单,库存随即减少。在常规情况下,买家下单后付款的概率颇高,看似并无大碍。然而,在特定情境下,如卖家参与促销活动时,这段时间往往是商品的热销时段。若竞争对手利用恶意下单手段,将卖家商品悉数锁定,致使库存清零,该商品将无法继续正常销售。这些恶意下单者往往并无真实购买意图,这正是“下单即减库存”策略的软肋所在。

鉴于“下单即减库存”可能招致恶意下单,进而影响卖家的商品销量,我们是否另有良策呢?或许你会想到,采用“付款才减库存”的方式是否可行?诚然,这确实可以避免恶意下单的问题。但随之而来的是另一个棘手问题——库存超卖。

假设库存为100件商品,却可能出现300人成功下单的情况,因为下单时并不扣减库存,所以下单成功数远超实际库存数的情况时有发生,尤其是在促销活动期间。这将导致众多买家下单成功却无法完成支付,购物体验大打折扣。

显而易见,无论是“下单即减库存”还是“付款才减库存”,都难以确保库存与实际销售情况完全吻合。要想将商品准确无误地售出,着实不易!

既然“下单即减库存”与“付款才减库存”各有弊端,我们能否将二者结合,将两次操作前后关联,即下单时预扣库存,若在规定时间内未付款则释放库存,也就是采用“预扣库存”策略呢?

此方案确能在一定程度上缓解上述问题。但问题是否就此迎刃而解了呢?实则不然!针对恶意下单,即便我们将有效付款时间设定为10分钟,恶意买家仍可在10分钟后再次下单,或一次性大量下单以耗尽库存。针对此情况,仍需结合安全与反作弊措施加以遏制。

例如,对频繁下单不付款的买家进行标记(被标记买家下单时不扣减库存)、为特定类目设置最大购买数量(如活动商品每人限购3件),以及对重复下单不付款行为进行次数限制等。

至于“库存超卖”问题,即便在10分钟内,下单数量仍可能超出库存。对此,我们需区别对待:对于普通商品超卖情况,可通过补货解决;而对于不允许库存为负的卖家,则需在买家付款时提示库存不足。

3.6.3 大型秒杀活动中的库存扣减策略

目前,业务系统中最为常见的便是预扣库存方案。如购买机票、电影票时,下单后通常设有“有效付款时间”,超时则订单自动取消,库存释放,这便是典型的预扣库存应用。那么在秒杀场景中,哪种方案更为适宜呢?

鉴于秒杀商品往往“手快有手慢无”,成功下单后不付款的情况较少,加之卖家对秒杀商品库存有严格把控,因此秒杀商品采用“下单即减库存”策略更为合理。此外,从逻辑复杂度与性能角度来看,“下单即减库存”相较于“预扣库存”及涉及第三方支付的“付款才减库存”更为简洁高效。在数据一致性方面,“下单即减库存”需确保大并发请求下库存数据不为负数,即数据库中的库存字段值不得为负。对此,我们通常采用多种解决方案:一种是在应用程序中通过事务控制,确保减库存后不为负数,否则回滚;另一种是将数据库字段设置为无符号整数,当库存值小于零时,SQL语句将直接报错;还有一种方法是使用CASE WHEN判断语句,例如这样的SQL语句:



UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventoryEND

3.6.4 秒杀减库存的深度优化策略

在交易流程中,“库存”不仅是核心数据,更是高频访问的热点数据,因为交易的每一步都可能牵涉到库存的查询。正如我在前面分层过滤的讲解中所提及,秒杀场景对库存的一致性读要求并不严苛,将库存数据缓存(Cache)起来,能显著提升读取性能。

面对大并发的读取需求,我们可以借助LocalCache(即在秒杀系统的本地机器上缓存商品相关数据)和数据分层过滤的手段来缓解压力。然而,减库存这一大并发写操作却难以避免,它无疑是秒杀技术挑战中的核心难题。

接下来,我将深入探讨秒杀场景下减库存的优化策略,涵盖缓存减库存与数据库减库存两大方面。

秒杀商品与普通商品在减库存机制上存在差异。鉴于秒杀商品数量有限,交易时段短暂,我们是否可以大胆设想,将秒杀商品的减库存操作直接置于缓存系统中完成?也就是说,直接在缓存中扣减库存,或者在一个具备持久化功能的缓存系统(例如Redis)中执行?

如果你的秒杀商品减库存逻辑相对简单,不涉及复杂的SKU库存与总库存联动关系,那么这一设想完全可行。但一旦减库存逻辑变得复杂,或者需要用到事务处理,你就必须在数据库中执行减库存操作了。

MySQL的数据存储特性决定了同一数据在数据库中是以单行形式存储的。因此,在高并发场景下,会有大量线程竞争InnoDB的行锁。并发度越高,等待的线程就越多,TPS(每秒事务处理数)会随之下降,响应时间(RT)会延长,数据库的吞吐量将受到严重影响。

这可能会引发一个严重问题:单个热点商品可能会拖垮整个数据库的性能,导致0.01%的商品影响到99.99%商品的销售,这显然是我们不愿看到的局面。一个可行的解决方案是遵循前面提到的隔离原则,将热点商品放入单独的热点库中。但这无疑会增加维护的复杂性,比如需要实现热点数据的动态迁移,以及搭建单独的数据库等。

然而,将热点商品分离到单独的数据库并未从根本上解决并发锁的问题。那么,我们应该如何应对呢?解决并发锁问题,主要有两种策略:

其一,应用层排队。按照商品维度设置队列,顺序执行减库存操作。这样既能降低同一台机器对数据库同一行记录操作的并发度,又能控制单个商品占用数据库连接的数量,防止热点商品过度占用数据库连接资源。

其二,数据库层排队。应用层排队只能实现单机的并发控制,但应用机器数量众多,这种排队方式控制并发的能力有限。因此,如果能在数据库层实现全局排队,那将是最理想的选择。阿里的数据库团队为此开发了针对MySQL InnoDB层的补丁程序,能够在数据库层对单行记录实现并发排队。

你可能会问,排队和锁竞争不都是需要等待吗?它们有何不同?

如果你熟悉MySQL,就会知道InnoDB内部的死锁检测以及MySQL Server与InnoDB的切换会消耗大量性能。淘宝的MySQL核心团队还进行了其他方面的优化,如开发了COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL补丁程序,配合SQL中的提示(hint)使用,可以在事务中无需等待应用层提交(COMMIT),而是在数据执行完最后一条SQL后,直接根据TARGET_AFFECT_ROW的结果进行提交或回滚,从而减少网络等待时间(平均约0.7ms)。据我所知,目前阿里MySQL团队已经将包含这些补丁程序的MySQL版本开源。

此外,除了前面提到的热点隔离和排队处理外,对于某些场景(如商品的lastmodifytime字段)的频繁更新问题,如果多条SQL可以合并,那么在一定时间内只需执行最后一条SQL即可,以减少对数据库的更新操作。







优网科技,优秀企业首选的互联网供应服务商

优网科技秉承"专业团队、品质服务" 的经营理念,诚信务实的服务了近万家客户,成为众多世界500强、集团和上市公司的长期合作伙伴!

优网科技成立于2001年,擅长网站建设、网站与各类业务系统深度整合,致力于提供完善的企业互联网解决方案。优网科技提供PC端网站建设(品牌展示型、官方门户型、营销商务型、电子商务型、信息门户型、DIY体验、720全景展厅及3D虚拟仿真)、移动端应用(手机站APP开发)、微信定制开发(微信官网、微信商城、企业微信)、微信小程序定制开发等一系列互联网应用服务。


我要投稿

姓名

文章链接

提交即表示你已阅读并同意《个人信息保护声明》

专属顾问 专属顾问
扫码咨询您的优网专属顾问!
专属顾问
马上咨询
联系专属顾问
联系专属顾问
联系专属顾问
扫一扫马上咨询
扫一扫马上咨询

扫一扫马上咨询

和我们在线交谈!