初识高并发系统:一个电商秒杀场景的启示
第 1 章:初识高并发系统:一个电商秒杀场景的启示
问题的起点:为什么一个简单的抢购会让系统崩溃
2019 年双十一的前三分钟,某电商平台的服务器监控系统突然发出警报。技术团队眼睁睁看着数据库的 CPU 使用率从 30% 瞬间飙升到 98%,响应时间从正常的 50 毫秒蹿升到令人绝望的 8 秒。客服电话被打爆,用户抱怨订单提交不了、支付失败、页面一片空白。这场持续了十几分钟的系统瘫痪,直接导致了数千万元的订单损失。
事后复盘时,团队发现罪魁祸首竟然是一个看似再普通不过的功能——限量版手机的抢购。用户点击“立即抢购”按钮的那一刻,系统接收到了一瞬间涌入的 50 万个并发请求。这些请求同时去查询同一个商品的库存,同时去扣减同一个数字,同时去创建订单,最终把数据库这个“单车道”彻底堵死。
这个场景揭示了一个让无数开发者困惑的问题:为什么一个简单的商品抢购功能,会让整个系统濒临崩溃?在日常开发中,我们写的增删改查接口明明运行得好好的,为什么一到促销场景就原形毕露?
答案在于我们日常面对的是“普通并发”,而促销场景面对的是“极端并发”。普通并发指的是请求量稳定、可预期、彼此独立的场景,比如正常的商品浏览、用户登录、订单查询。而极端并发指的是请求量在短时间内急剧增长、大量请求同时访问同一资源、对响应时间有严格要求的场景。理解这两种并发的本质差异,是掌握高并发系统设计的第一把钥匙。
本章将以电商秒杀这个最具代表性的高并发场景为切入点,带领读者认识什么是高并发系统、高并发带来了哪些独特挑战,以及为什么传统的软件开发方法难以应对这些挑战。通过分析秒杀场景中那些看似不可思议的故障,我们将逐步建立起对高并发系统的直观认知,为后续章节的深入学习奠定基础。
高并发系统的定义与核心指标
什么是高并发系统
高并发系统并没有一个严格的数学定义,因为它本质上是一个相对概念。同样是每秒处理一万个请求,对于一个个人博客来说可能是灭顶之灾,对于淘宝双十一的峰值流量来说却是九牛一毛。因此,我们判断一个系统是否属于高并发系统,不能只看绝对数字,而要看系统的处理能力是否能够满足业务需求。
从更本质的角度来看,高并发系统的核心特征是:系统的负载超过了资源的供给能力,导致性能下降甚至服务不可用。这个定义揭示了高并发问题的本质——资源与需求之间的不平衡。要么是需求太大超出了资源上限,要么是资源太少无法承载预期需求,要么是两者都在变化但节奏不匹配。
在软件工程领域,我们通常用一组量化指标来描述高并发系统的特征。理解这些指标,是分析任何高并发场景的第一步。
核心量化指标
同时在线用户数(Concurrent Users) 指的是在某一特定时刻同时与系统保持活跃连接的用户数量。这里的“活跃”通常定义为在过去几分钟内有操作的用户,而不是仅仅打开页面就离开的用户。对于一个电商网站来说,同时在线用户数可能达到几十万甚至上百万,但其中真正在下单、支付的“核心用户”可能只占 10% 到 20%。
每秒请求数(Requests Per Second, QPS) 是衡量系统吞吐量的核心指标,它表示系统在一秒内能够处理的请求总量。QPS 与同时在线用户数的关系取决于用户的平均行为模式:如果用户平均每分钟发起 3 次请求,那么 10 万同时在线用户对应的 QPS 大约是 5000。需要注意的是,QPS 是一个平均值,在高并发场景下,请求的分布往往极不均匀,峰值 QPS 可能是平均值的 5 到 10 倍。
响应时间(Response Time) 描述的是从用户发起请求到收到完整响应所需要的时间。在高并发场景下,响应时间通常会被划分为几个百分位:
- **P50(中位数)**:50% 的请求在多长时间内完成
- **P95**:95% 的请求在多长时间内完成
- **P99**:99% 的请求在多长时间内完成
对于用户体验来说,P95 和 P99 往往比平均值更重要。一个系统平均响应时间是 100 毫秒,但如果 P99 是 5 秒,仍然会让大量用户感到卡顿。在高并发设计中,我们不仅要优化平均性能,更要关注长尾延迟。
系统吞吐量 指的是单位时间内系统能够处理的工作量,通常用 TPS(Transactions Per Second,每秒事务数)来衡量。与 QPS 不同,TPS 更强调“完整事务”的概念——一次商品购买从浏览、加入购物车、支付到下单完成,是一个完整的事务。TPS 往往低于 QPS,因为一个用户操作可能产生多个后端请求。
资源利用率 描述的是系统各组件对计算资源的使用程度,包括 CPU 使用率、内存使用率、磁盘 I/O、网络带宽、数据库连接池等。当某项资源的利用率接近 100% 时,往往就是系统瓶颈所在。识别资源瓶颈是高并发系统分析的关键环节。
高并发的门槛在哪里
既然高并发是一个相对概念,那么什么情况下我们需要采用高并发系统设计的思维和方法?以下几个信号可以作为判断标准:
流量特征呈现明显的波峰波谷。如果你的系统日均 QPS 是 1000,但某几个小时的 QPS 会突然飙升到 50000,这种巨大的流量波动就意味着你需要考虑高并发设计。如果流量曲线是平稳的,传统架构通常能够应对。
用户行为高度集中。当大量用户同时去做同一件事时——比如抢购同一件商品、同时签到、同时抢红包——就会产生“热点”问题。即使总 QPS 不高,但所有请求都打到同一个数据库行或同一个缓存 key,单点压力会成为系统的阿喀琉斯之踵。
业务对响应时间有严格要求。如果系统要求 99.9% 的请求在 200 毫秒内完成,当 QPS 上升到一定程度时,这个 SLA 就很难保证。高并发设计不是为了炫技,而是为了满足真实的业务需求。
系统出现过性能问题或故障。如果你曾经遇到过数据库连接池耗尽、服务器 CPU 被打满、请求超时率飙升等问题,那就说明你的系统已经触碰到了高并发的门槛。
以下是一个简化的判断参考表,帮助你快速评估自己的系统是否进入高并发领域:
| 指标 | 常规系统 | 高并发系统 | 超高并发系统 | |------|----------|------------|--------------| | 日活用户 | < 10万 | 10万 - 1000万 | > 1000万 | | 峰值 QPS | < 1000 | 1000 - 10万 | > 10万 | | 流量波动比 | < 5倍 | 5 - 50倍 | > 50倍 | | 典型场景 | 日常功能 | 促销活动 | 顶级盛会 |
需要强调的是,这些数字仅供参考。真正决定是否需要高并发设计的,是你的业务特点和性能要求。一个峰值 QPS 只有 500 的系统,如果要求所有请求在 10 毫秒内完成,同样需要精心的高并发优化。
电商秒杀场景的业务特点
为什么秒杀是最典型的高并发场景
在所有高并发场景中,电商秒杀堪称“皇冠上的明珠”。它几乎涵盖了高并发系统的所有典型挑战:流量巨大且集中、热点数据严重、对一致性要求极高、用户体验要求苛刻。一个能扛住秒杀压力的系统,应对其他高并发场景也会游刃有余。
秒杀活动通常有两种形式。第一种是限量促销,比如“前 100 名用户半价购买”,核心挑战是确保有限的库存精准分配。第二种是抢购预约,比如“双十一凌晨开售,前 10000 名预约用户有购买资格”,核心挑战是在极短时间内处理海量预约请求。无论哪种形式,共同特点是活动开始前系统风平浪静,活动开始瞬间流量如核弹爆炸。
让我们通过一个具体的数字来感受秒杀的恐怖流量。某电商平台的日常日活用户是 500 万,平时每秒下单量大约 200 单。在一次“99 元抢购 iPhone”的活动中,开售前 5 分钟有 150 万用户涌入系统,集中在开售的 30 秒内,系统瞬间需要处理超过 10 万 QPS 的下单请求——是平时的 500 倍。而库存只有 500 台,意味着 99.9997% 的请求最终会失败返回。
这种巨大的流量落差,是秒杀场景最显著的特征。传统的系统设计思路是按照平均负载配置资源——平时够用就行。但如果按照这个思路配置秒杀系统,要么造成巨大的资源浪费,要么在峰值时刻彻底崩溃。正确的做法是采用弹性伸缩、请求削峰、异步处理等手段,让系统在峰值时仍然稳定运行。
三大业务特征分析
特征一:瞬时流量激增
秒杀活动的流量曲线不是平缓的山坡,而是近乎垂直的悬崖。在活动开始的 1 分钟内,可能会涌入占总流量 80% 的请求。这种流量模式给系统带来的挑战是全方位的:
瞬时并发数剧增意味着 TCP 连接数、HTTP 连接数、线程数都会在瞬间达到峰值。如果服务器配置了固定数量的工作线程,线程池会迅速耗尽,后续请求只能排队等待甚至被拒绝。同时,网络带宽可能在瞬间被占满,导致请求无法到达服务器。
缓存层级可能失效。正常情况下,热点数据会停留在缓存中,减少数据库压力。但在秒杀场景下,缓存可能在活动开始前就被大量预热请求填满,真正有价值的库存数据反而被挤出缓存。
负载均衡器可能成为瓶颈。如果负载均衡器本身处理能力有限,在流量激增时可能先于后端服务崩溃。
特征二:资源竞争激烈
与流量巨大同样棘手的是,秒杀的资源竞争极其集中。想象一下,10 万个用户同时抢购 500 台手机,每个请求都要做三件事:查询库存、扣减库存、创建订单。这三件事都指向同一个数据库表、同一个商品 ID 的同一行数据。
在数据库层面,这意味着所有请求都在争夺同一把锁。MySQL 的 InnoDB 引擎在处理行锁时,会让等待的事务排队。如果并发度太高,锁等待会指数级增长,最终导致大量请求超时。极端情况下,一个慢查询可能会卡住整个数据库连接池,让所有业务都瘫痪。
在应用层面,资源竞争表现为内存竞争和 CPU 竞争。所有请求都在创建对象、分配内存、触发 GC(垃圾回收)。如果对象分配速度超过 GC 回收速度,内存会持续增长,最终触发 Full GC,导致所有线程停顿。在高并发下,一次 Full GC 可能持续数秒,这期间系统完全不可用。
特征三:数据一致性要求极高
秒杀的商业逻辑要求库存不能超卖——卖出 500 台手机,实际发货就只能是 500 台,不能因为并发问题多卖出去 50 台。这听起来是一个理所当然的需求,但在高并发场景下实现它却异常困难。
问题的根源在于“检查-操作”模式的时间窗口。假设库存是 1,线程 A 检查发现库存大于 0,准备扣减;线程 B 也检查发现库存大于 0,也准备扣减。两个线程同时扣减,结果库存变成了负数。要避免这种情况,必须让检查和扣减变成原子操作,不能被其他线程插入。
数据一致性要求的另一个维度是分布式环境下的数据同步。在微服务架构中,订单服务、库存服务、支付服务可能是独立的进程甚至独立的机器,它们之间通过网络通信。网络是不可靠的,消息可能丢失、延迟、乱序。如果订单服务声称扣减成功,但库存服务的扣减请求在网络中被延迟,库存就会出现不一致。
秒杀请求的处理链路
为了更清晰地理解秒杀场景的挑战,让我们追踪一个秒杀请求从用户端到数据库的完整路径:
```mermaid flowchart TD A[用户点击抢购] --> B[DNS 解析] B --> C[CDN 边缘节点] C --> D[负载均衡器] D --> E[Web 服务器集群] E --> F[API 网关] F --> G[订单服务] G --> H{库存充足?} H -->|是| I[创建订单] I --> J[库存服务扣减库存] J --> K[支付服务] K --> L[返回下单成功] H -->|否| M[返回库存不足] L --> N[(MySQL 数据库)] J --> N K --> N
style D fill:#ffcccc style E fill:#ffcccc style G fill:#ffcccc style N fill:#ffcccc ```
这个流程图中的红色节点——负载均衡器、Web 服务器、订单服务、数据库——都是潜在的瓶颈点。在正常流量下,这些组件的性能冗余充足,请求能够顺畅流转。但在秒杀场景下:
- 负载均衡器可能因为新建连接数过多而耗尽资源
- Web 服务器的工作线程可能被占满,新请求无法分配
- 订单服务作为单点,所有请求都必须经过它
- 数据库的连接池有限,大量请求排队等待
更危险的是,这些瓶颈会形成连锁反应。数据库变慢导致订单服务线程等待,线程等待导致 Web 服务器线程池耗尽,线程池耗尽导致负载均衡器认为后端不健康而移除节点,节点移除导致剩余服务器压力更大。这种正反馈循环,就是系统雪崩的典型模式。
高并发系统面临的四大挑战
挑战一:性能瓶颈
性能瓶颈是高并发系统面临的首要挑战。当请求量超过系统处理能力时,必然会有请求等待,而等待会导致响应时间增加,响应时间增加又会加剧资源竞争,形成恶性循环。
性能瓶颈的表现形式多种多样。在 CPU 层面,可能是计算密集型任务占满 CPU,也可能是大量线程上下文切换带来的开销。在内存层面,可能是对象分配过快导致 GC 压力,也可能是内存不足触发 swap 导致性能断崖。在 I/O 层面,可能是磁盘读写成为瓶颈,也可能是网络带宽被占满。
识别性能瓶颈需要借助监控工具和压力测试。常用的监控指标包括:
- **CPU 使用率和系统负载**:top 命令显示的 cpu us(用户态)和 cpu sy(系统态)比例,能反映是否存在 CPU 密集型瓶颈
- **线程状态分布**:通过 jstack 或 Arthas 工具,可以查看线程的运行状态(Runnable、Blocked、Waiting、Timed Waiting),Blocked 线程过多通常意味着锁竞争
- **GC 频率和耗时**:通过 jstat 或 GC 日志,可以分析 GC 是否过于频繁,是否存在内存泄漏
- **数据库慢查询**:通过 MySQL 的 slow_query_log,可以找出那些拖慢系统的 SQL 语句
一个典型的性能瓶颈案例是“缓存雪崩”。在正常情况下,系统使用 Redis 缓存热点数据,Redis 命中率 99%,数据库几乎不承担压力。但如果 Redis 突然宕机,或者大量缓存同时过期,所有请求都会穿透到数据库。数据库承受不住大量请求,开始响应缓慢,超时增多,最终导致系统崩溃。
这个案例的教训是:高并发系统的瓶颈往往是意想不到的地方。开发者在设计时通常会关注核心链路上的组件,但容易忽略边边角角的依赖——日志写入、本地缓存、第三方 SDK、监控系统本身,都可能在特定场景下成为短板。
挑战二:可用性风险
高并发场景下的可用性问题,本质上是系统在极端负载下保持稳定运行的能力。系统可能在低负载下表现完美,但一旦流量超过阈值,服务质量会急剧下降。
可用性通常用 SLA(Service Level Agreement)来衡量。常见的 SLA 指标包括:
- **可用时间比例**:一年 99.9% 的可用性意味着大约 8.76 小时的不可用时间
- **错误率**:请求失败或超时的比例
- **响应时间**:P99 响应时间不超过某个阈值
高并发对可用性的威胁主要来自两个方面。第一是过载——系统的处理能力有上限,当请求量超过这个上限时,要么拒绝服务,要么响应极慢。无论哪种方式,用户体验都会严重受损。
第二是级联失败——分布式系统中,一个组件的故障可能导致其他组件也故障。想象这样的场景:数据库响应变慢(可能是因为某条慢查询),应用服务器的数据库连接池被占满,应用服务器开始大量超时,上游服务感知到下层服务不可用也开始超时,最终整个系统崩溃。这种一个故障引发连锁反应的现象,叫做服务雪崩。
应对可用性风险的核心思路是降级和熔断。降级指的是在系统压力过大时,主动关闭一些非核心功能,保证核心功能的可用性。比如在秒杀场景下,可以临时关闭商品推荐、用户评论等非核心功能,把资源留给下单流程。
熔断指的是当某个下游服务的错误率超过阈值时,自动切断与它的通信,避免大量请求积压导致更严重的故障。熔断器会进入“半开”状态,尝试放行少量请求探测服务是否恢复,如果成功则恢复正常,否则继续保持熔断。
挑战三:一致性保证
在单机系统中,操作的原子性和一致性由操作系统和数据库事务机制保证。但在分布式高并发系统中,数据分布在多个节点,请求由多个服务协作完成,一致性保证变得异常复杂。
高并发场景下的一致性挑战体现在三个层面。
原子性挑战:一个简单的库存扣减操作,在代码层面可能需要三步——从缓存读取库存、扣减库存、写入缓存。如果在写入缓存时进程崩溃,库存数据就会丢失。如果先写数据库再删缓存,可能导致缓存中残留脏数据。这些看似简单的边界情况,在高并发下会被放大。
隔离性挑战:数据库的隔离级别(Read Uncommitted、Read Committed、Repeatable Read、Serializable)决定了并发事务之间的可见性。在高并发秒杀场景下,如果隔离级别设置不当,可能出现脏读(读到其他事务未提交的修改)、不可重复读(同一行数据两次读取结果不同)、幻读(同一查询两次结果集不同)等问题。
持久性挑战:用户下单后,系统需要确保订单数据不会丢失。但在高并发场景下,如果为了追求性能而减少同步刷盘,可能导致故障时数据丢失。反过来,如果每次操作都强制同步,性能会严重下降。这是一个需要权衡的设计问题。
CAP 定理告诉我们:在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者只能同时满足两个。对于大多数高并发系统,选择的是 AP(保证可用性和分区容错性),在一致性和可用性之间寻找平衡。具体做法是:允许短暂的数据不一致,但通过异步补偿、重试机制、最终一致性等技术手段,保证系统整体是正确的。
挑战四:资源浪费
高并发设计的一个常见误区是:既然峰值流量很大,那就按照峰值配置所有资源。这种做法在理论上能保证系统不崩溃,但在经济上往往是不可接受的。
考虑一个实际的例子:某电商平台平时日均 QPS 5000,峰值 QPS 10万。如果按照峰值配置服务器,需要 200 台服务器。但一天中只有 1 小时的峰值流量,其他 23 小时这些服务器基本闲置。这意味着 99% 的计算资源被浪费了。
资源浪费的问题不仅存在于服务器层面,还存在于数据库连接池、线程池、内存、带宽等各个方面。更隐蔽的是,即使我们愿意付出成本购置足够资源,这些资源的运维管理也是巨大的挑战——200 台服务器的部署、监控、扩容、缩容,需要大量的人力和工具投入。
解决资源浪费问题的核心思路是弹性伸缩和**按