高并发秒杀系统设计:从 CDN 到数据库的全链路架构
秒杀系统的流量洪峰不只是后端的事——CDN 扛不住静态资源会拖慢首屏、Nginx 限流配置不当会把异常流量放行、网关熔断策略缺失可能让整个微服务链路雪崩。任何一层出问题,用户感知到的都是“系统崩了”。本文从全链路视角梳理秒杀系统的分层架构,讲清楚 CDN、Nginx、API 网关、Redis+Lua、MQ 异步下单这几层如何协同工作,以及每一层具体该怎么做。
每年大促,总能看到“某某平台秒杀又崩了”的热搜。
弹幕里一片骂声,但作为技术人员,我其实挺能理解的——流量洪峰是全链路的,从用户点击按钮到数据库落库,中间任何一个环节扛不住,整个系统就崩了。
这篇文章想分享一个完整的全链路视角。不是只盯着后端优化,而是从 CDN 到数据库,把每一层的职责、技术和配置都讲清楚。
核心思想只有一个:分层削峰。
几亿人点击,最后落到数据库的订单可能只有几千单。每一层都要把流量“砍”掉一大半,不让无效请求穿透到下一层。
每一层的目标不同,但逻辑一致:尽量把请求拦截在上游,减少对核心资源的竞争。
下面逐层拆解。
第一层:前端——把无效点击挡在手机里
前端是距离用户最近的一层,也是成本最低的拦截点。能让用户手机自己解决的,千万别发到服务器来。
按钮防抖是最基础也是最有用的操作。 用户点击秒杀按钮后,立即置灰 5 秒,禁止重复点击。就这一个操作,能挡掉 80% 的无效点击。
seckillBtn.addEventListener('click', function() {
this.disabled = true;
// 发送请求...
setTimeout(() => {
this.disabled = false;
}, 5000);
});验证码或滑块不只是防脚本,它还有一个隐藏作用——把同一秒的点击拉平到几秒内。有人反应快有人反应慢,服务器峰值得到了自然的平滑。
客户端限流在 App 场景下特别好用。App 可以内置逻辑:检测到并发过高或接到服务器降级指令后,用户点击直接提示“人太多”,请求都不发出去。
还有一个小细节:时间校准。 用户手机时间可能不准,倒计时必须以服务器时间为准。App 启动时拉取服务器时间,防止用户修改手机时间提前抢。
第二层:CDN——静态资源别来占源站带宽
秒杀开始前,用户会疯狂刷新商品详情页。如果这些请求全部打到源站,再强的服务器也扛不住。
最直接的做法是页面静态化。秒杀商品的图片、文案、CSS、JS 全部静态化,部署到 CDN 节点。用户访问时直接从 CDN 加载,不经过后端服务。秒杀页面的静态内容占比通常超过 80%,这一层能显著降低源站压力。
还有一招是动静分离。秒杀页面的 HTML、JS、CSS、图片全部放到 CDN 上。用户刷新页面走的是 CDN 流量,不占用源站带宽。只有秒杀按钮需要服务端动态判断。
CDN 在这个场景下有个特殊要求:秒级失效能力。 如果商品临时下架或价格调整,CDN 缓存必须能快速失效,不能让用户看到旧数据。
第三层:Nginx 接入层——入口处的流量安检
流量出了手机,经过 CDN,到达的第一道服务器关口就是 Nginx。这一层要做的事情像机场安检:拦截恶意流量、控制速率、均匀分发请求。
IP 限流是最基础的配置。 Nginx 的 limit_req_zone 模块可以基于 IP 维度限制请求频率。比如单 IP 每秒最多 5 次请求,超出直接返回 429。
http {
limit_req_zone $binary_remote_addr zone=seckill:10m rate=100r/s;
server {
location /seckill {
limit_req zone=seckill burst=50 nodelay;
proxy_pass http://seckill_backend;
}
}
}这个 rate 设多少要结合实际业务场景。不是越大越好,也不是越小越好——太小会误伤正常用户,太大则起不到拦截作用。我的经验是:先根据正常用户的访问频率估算一个值,上线后观察限流命中率再逐步调整。
黑名单拦截也可以在 Nginx 层做。维护一个恶意 IP 名单,黑名单请求直接返回 403,不进入后端。
连接池优化容易被忽略。配置 upstream 的 keepalive,保持长连接,减少每次请求的三次握手开销。在高并发场景下,握手开销本身就是不小的成本。
upstream seckill_backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 1000;
}如果需求更复杂——比如基于用户 ID 而不是 IP 做限流——可以用 OpenResty(Nginx + Lua)实现,灵活性高很多。
第四层:API 网关——微服务架构的流量总闸
如果系统是微服务架构,Nginx 后面通常还有一层 API 网关(比如 Spring Cloud Gateway 或 Kong)。网关层的职责比 Nginx 更细:限流、熔断、降级、鉴权。
接口级限流用令牌桶算法,设置每秒最大处理能力(比如 1000 QPS),超出部分直接返回“秒杀火爆,请稍后再试”。Sentinel 和 Hystrix 都可以做这件事。
熔断机制也很关键。当后端服务响应时间过长或错误率超过阈值(比如 50%),熔断器自动打开,阻止流量进入后端,执行降级策略。这能防止一个服务的故障扩散到整个系统。
# Sentinel 配置示例
流量控制规则:
- 资源名: /seckill/order
QPS阈值: 1500
超出后: 直接拒绝
熔断降级规则:
- 资源名: /seckill/order
超时阈值: 500ms
错误率阈值: 50%
熔断时长: 30s统一鉴权放在网关层做也很合适。JWT 在网关层解密,用户身份不合法直接踢回去,不需要查数据库。这种“不合法请求不进业务层”的思路,每一层都应该贯彻。
服务降级是另一层保障。非核心功能(如商品评价、推荐)在 QPS 超过阈值时自动关闭,释放资源给核心秒杀链路。
第五层:业务层——Redis + Lua + MQ
这一层是秒杀系统的核心,在上篇文章里已经详细讲过了,这里只简要回顾它的位置和作用。
Redis 缓存库存是必须的。将商品库存预加载到 Redis,使用 Lua 脚本保证“查库存、判库存、扣库存、记录用户”的原子性。Redis 单机 QPS 可达 10 万+,远高于数据库。
MQ 异步下单的作用是削峰。扣库存成功的请求进入消息队列,后端消费者异步创建订单。流量从“尖峰”变成“平缓曲线”,数据库不会被打死。异步化的代价是用户体验从“立即反馈”变成“排队等待”,但在秒杀场景下,这是值得的取舍。
应用层限流同样不能少。用 Sentinel 或 Hystrix 对秒杀接口做限流,设置每秒最大处理能力,作为网关层限流之后的第二道保险。
第六层:数据层——最终一致性兜底
经过前面五层的层层拦截,到达数据库的流量已经很少了。但数据层仍然需要为最终的数据一致性兜底。
分库分表按用户 ID 哈希分散写入压力。读写分离让主库负责写、从库负责读。最终一致性是秒杀场景的合理选择——不追求强一致性,订单异步写入,通过数据库唯一键约束防止重复下单。
完整链路串起来
把每一层串起来,一个秒杀请求的完整路径是这样的:
每一层拦截的流量类型不同,手段也不同:
| 层级 | 拦截目标 | 拦截手段 |
|---|---|---|
| 前端 | 重复点击、脚本请求 | 按钮防抖、验证码、请求合并 |
| CDN | 静态资源请求 | 页面静态化、缓存策略 |
| Nginx | 恶意IP、高频IP | IP限流、黑名单 |
| API网关 | 超量请求、异常服务 | 令牌桶限流、熔断降级 |
| Redis | 库存不足的请求 | Lua脚本库存判断 |
| MQ | 数据库写入压力 | 异步削峰 |
监控与压测
架构搭好了,不压测等于没搭。
全链路压测是必须的。用 JMeter 模拟千万级并发,逐步加压直到发现系统瓶颈。建立从用户点击到数据库事务的流量模型,精确计算每层服务的容量配比,避免出现“应用层已扩容但数据库成为瓶颈”的木桶效应。
核心监控指标要覆盖每一层:
- 基础设施:CPU、内存、网络、磁盘 IO
- 接入层:Nginx 连接数、限流命中率、响应时间
- 业务层:QPS、RT、错误率、库存扣减成功率
- 数据层:数据库连接数、慢查询、MQ 积压量
链路追踪能帮你在出现问题时快速定位瓶颈在哪个环节。如果用了 SkyWalking 或 Zipkin,可以看到一个请求在每一层的耗时分布。
写在最后
秒杀系统的全链路架构,本质上就是“漏斗模型”在每一层的落地。
- 前端挡掉 80% 的无效点击
- CDN 挡掉 80% 的静态资源请求
- Nginx 挡掉恶意 IP 和超频请求
- API 网关做限流熔断,保护后端不被冲垮
- Redis 在内存里完成库存扣减,只放行少量有效请求
- MQ 把下单异步化,保护数据库
每一层都只做一件事,但每一件事都必不可少。
秒杀系统的设计从来不是某一个环节的优化,而是从用户点击到数据库落库的每一个环节都要经得起考验。理解了这个全链路视角,再去看具体的代码实现和配置细节,方向就不会偏。