02.秒杀系统架构
秒杀系统需应对瞬时高并发、巨大流量和有限库存
并发量
:每秒25万请求;带宽
:约977Mb,需优化数据传输前端优化
静态资源缓存、CDN分发、预加载技术,减少页面加载时间
前端限流(如滑块验证码)过滤无效流量
服务层设计
Redis缓存:防止缓存击穿(分布式锁)和穿透(布隆过滤器)
库存扣减:Redis原子操作确保库存准确,未支付订单超时回补库存
分层限流
Nginx限流:基于IP和速率控制,防止恶意请求
网关限流:Redis+Lua脚本实现精准限流
服务限流:滑动窗口算法控制请求速率
# 01.需求分析
# 1、功能需求
“秒杀”这个词在电商行业中出现的频率较高,如京东或者淘宝平台的各种“秒杀”活动,最典型的就是“双11抢购”
“秒杀”是指在有限的时间内对有限的商品数量进行抢购的一种行为,这是商家以“低价量少”的商品来获取用户的一种营销手段
其实,整个秒杀的业务场景并不复杂,可即查看参与秒杀的商品信息,加上购买和支付的动作,如下图所示
秒杀业务最大的挑战在于3点:
**瞬时:**持续时间极短,对于热门且具备极强竞争力的商品通常只有一秒
**流量巨大:**因为价格低廉,商品性价比高,而且正常买是需要很高的价格,所以才会吸引大量的用户来争抢
**数量有限:**因为商品的低价且性价比高,所以只有很有限的商品数量参与秒杀
防止遭到恶意破坏等,特此增加如下需求
1)用户在秒杀页面无需一直刷新“抢购”按钮,待秒杀活动开始时,按钮自动点亮
2)在公平以及防止恶意破坏的原则下,在下单之前增加验证码的录入,或者答题的相关环节
3)库存不能出现问题,即不多扣也不少扣
4)整个秒杀活动过程持续10分钟
# 2、性能指标预估
- 通过秒杀的需求描述可得出,当前秒杀活动主要需要预估三块的性能指标:存储容量、并发量、网络带宽
1)存储容量
- 由于是秒杀活动,且参与的商品基本都是低价高性价比的,数量是非常有限的
- 所以,在订单存储上基本不用去过多考虑
2)并发量
- 针对5000万用户平均每人访问2次,则并发量为每秒16.7万左右(
5000w*2/10*60
) - 在预留一部分,可以预估到每秒25万左右(也可以进行double下)
- 针对5000万用户平均每人访问2次,则并发量为每秒16.7万左右(
3)网络带宽
- 在带宽方面,需要进行相关优化,采取数据传输越少越好
- 假设
单条
传输在0.5KB
,则根据并发量预估网络带宽为:977Mb
左右(25w*0.5KB=122MB*8bit=977Mb
)
# 02.功能设计
# 0、分层结构图说明
# 1、前端&网络优化
# 1)浏览器端优化
静态资源缓存:
- 利用
浏览器缓存机制缓存静态页面、CSS、JS等资源
,减少不必要的重复请求和服务器压力 - 同时,确保缓存策略能自动清除旧的资源缓存(如利用版本号或hash机制)
- 利用
购物车缓存与校验:
- 将用户的
购物车信息缓存到浏览器本地
(如使用localStorage
) - 在用户
完成购买前校验库存,避免用户提交无效订单
- 将用户的
预加载与占位符:
- 通过预加载图片、静态资源,减少秒杀页面的加载时间
- 同时,使用占位符技术优化用户等待体验
# 2)网络层优化
CDN分发与缓存
:- 使用内容分发网络(CDN)缓存静态资源(如HTML、CSS、JS、图片等),加速用户访问的响应速度,并减轻源服务器的压力
- CDN节点的智能调度可以将用户请求路由到离用户最近的服务器,减少网络延迟
- 就近路由:通过配置
智能DNS
,根据用户地理位置选择最优的CDN节点
,进一步减少延迟
# 3)负载层设计
- 负载均衡(SLB/ELB):
- 使用云端的负载均衡服务(如腾讯云的SLB或阿里云的SLB)
- 配置多区域、全局负载均衡,确保请求被合理分发到多个服务器,减少单点故障
- 反向代理与缓存(Nginx+Keepalived):
- 使用Nginx作为反向代理,结合Keepalived实现高可用性,避免单台Nginx节点失效
- Nginx还可配置缓存机制,缓存部分动态页面或API响应,减少后端服务器压力
- Nginx限流与流量控制:
- 在Nginx层设置请求限流与流量控制规则,针对秒杀活动
设置每秒访问限制
,避免服务器过载 - 比如
每个IP、每个用户账号一秒内只能发起有限次数的请求,防止恶意刷单
- 在Nginx层设置请求限流与流量控制规则,针对秒杀活动
# 2、服务层设计
# 1)redis缓存
缓存击穿
商品缓存失效
,请求同时去查缓存中没有数据,然后又同时访问数据库(导致击穿)可以在查询redis没有数据时需要根据商品ID获取一把分布式锁,只有获取到锁才能够查询MySQL
从MySQL查询完数据后,把数据写入Redis,后续请求就可以直接从Redis获取数据
缓存穿透
- 如果有大量的请求传入
不存在的商品ID
,导致大量无用请求查询MySQL - 先从布隆过滤器中查询该id是否存在,如果不存在,则直接返回失败
- 如果有大量的请求传入
# 2)库存扣减
- 在提交订单的第一步减少 Redis 中的库存,之后再生成订单并等待用户支付
- 对于未支付或取消的订单,需要在一定时间后将库存加回
- Redis 脚本实现原子扣减
if redis.call('get', KEYS[1]) > '0' then
return redis.call('decr', KEYS[1])
else
return -1
end
2
3
4
5
# 3、分层限流
# 1)前端限流
如滑块验证码、前端漏桶算法
筛选掉无效流量和部分恶意流量
# 2)Nginx限流
- 基于 IP、访问速率等做全局流量控制
http {
# 定义限流区: 按客户端 IP 限制, 这里设置为每秒 10 个请求
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;
server {
listen 80;
server_name example.com;
location / {
# 应用限流规则: 在达到 rate 速率限制时,多出的请求会被缓冲到突发队列
limit_req zone=ip_limit burst=20 nodelay;
proxy_pass http://backend_server;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3)网关限流
Redis + Lua 限流
- Key: 例如
rate_limit:user:12345
- Value: 当前时间窗口内的请求计数
- TTL: 过期时间,设置为 1 秒,用于限制请求频率
- Key: 例如
import redis
# 初始化 Redis 连接
redis_client = redis.StrictRedis(host='localhost', port=6379, decode_responses=True)
# Lua 脚本
lua_script = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])
local current = tonumber(redis.call("GET", key) or "0")
if current + 1 > limit then
return 0
else
redis.call("INCRBY", key, 1)
redis.call("EXPIRE", key, expire_time)
return 1
end
"""
# 限流参数
user_id = "12345"
rate_limit_key = f"rate_limit:user:{user_id}"
limit = 5 # 每秒最多 5 次请求
expire_time = 1 # 时间窗口为 1 秒
# 调用 Lua 脚本
result = redis_client.eval(lua_script, 1, rate_limit_key, limit, expire_time)
if result == 1:
print("Request allowed")
else:
print("Rate limit exceeded")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 4)服务令牌桶限流
- ① 将时间分为多个较小的时间片段(窗口)
- ② 每当有请求到来时,系统会计算当前时间窗口以及前几个时间窗口内的总请求数
- ③ 如果总请求数超过了预设的阈值,新的请求会被拒绝
- ④ 随着时间的推移,旧的时间片段会逐渐过期,窗口不断滑动