1. 为什么百万QPS抢券系统是个技术分水岭?
去年双十一,某电商平台在秒杀活动开始后的第一秒就收到了超过120万次请求,而他们的系统硬是扛住了。但同一时刻,另一个平台的服务器直接宕机了——这就是高并发系统设计的生死线。当QPS(每秒查询率)突破百万级别时,系统面临的挑战会呈现指数级增长。
百万QPS意味着什么?假设每个请求需要处理10KB数据,那么仅网络带宽就需要10Gbps;如果每个请求需要访问数据库,传统MySQL单机最多支撑5万QPS,直接访问会导致数据库瞬间崩溃。这就是为什么普通系统架构在百万QPS面前会像纸房子一样倒塌。
关键认知:高并发不是简单的"更多服务器",而是从架构设计到代码细节的全方位重构。一个设计良好的系统,成本可能只有蛮力方案的1/10。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计:分层削峰策略
2.1 前端流量控制层
去年我们给一个电商平台做抢购系统时,发现90%的流量其实来自页面刷新。通过以下策略,我们直接砍掉了85%的无用请求:
- 动态令牌验证:页面加载时生成加密token,提交请求必须携带有效token。用简单的JS算法就能让恶意刷新的成本提高10倍
javascript复制// 前端生成token的示例
function generateToken(userId) {
const timestamp = Math.floor(Date.now() / 1000);
return md5(userId + '|' + timestamp + '|' + secretKey);
}
-
按钮状态控制:点击后立即禁用按钮,配合倒计时UI。实测可减少50%以上的重复点击
-
地域化延迟:不同地区用户获得不同的活动开始时间偏移(100-300ms),避免绝对时间点的流量尖峰
2.2 接入层设计
Nginx是目前最成熟的接入层方案,但需要特殊配置:
nginx复制# 关键配置项
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
location /coupon {
limit_req zone=api_limit burst=50 nodelay;
proxy_pass http://backend;
}
这个配置实现了:
- 单个IP限流100请求/秒
- 允许突发50个请求
- 直接拒绝超额请求(返回503比排队更保护系统)
我们在压力测试中发现,合理配置的Nginx集群,8台32核机器就能轻松处理200万QPS。
2.3 服务层设计
2.3.1 热点数据隔离
优惠券数据必须与主业务库分离。我们的方案是:
- 独立Redis集群:16分片,每个分片1主2从
- 特殊数据结构设计:
redis复制# 库存计数器
HSET coupon:1001 total 10000
HSET coupon:1001 available 10000
# 用户领取记录
SETBIT coupon:1001:users 123456 1 # 用户ID 123456已领取
2.3.2 异步化处理
核心公式:同步操作 = 库存检查 + 预扣减;异步操作 = 订单生成 + 最终扣减
java复制// 伪代码示例
public Response grabCoupon(Long userId, Long couponId) {
// 同步阶段
if(!redis.decrIfGT0("coupon:"+couponId+":available")) {
return Response.fail("已抢光");
}
// 异步阶段
mq.send(new GrabTask(userId, couponId));
return Response.success("抢购中...");
}
3. 数据一致性:比想象中更棘手的问题
3.1 库存超卖难题
我们曾经在压测时发现,即便使用了Redis的DECR命令,在10万QPS下仍然会出现0.1%的超卖。原因在于:
- 客户端收到成功响应但网络超时,重试导致重复扣减
- Redis主从同步延迟期间,从库读取到旧值
最终解决方案:
lua复制-- Redis Lua脚本保证原子性
local key = KEYS[1]
local userId = ARGV[1]
if redis.call('GETBIT', key..':users', userId) == 1 then
return 0
end
if redis.call('HINCRBY', key, 'available', -1) >= 0 then
redis.call('SETBIT', key..':users', userId, 1)
return 1
end
return -1
3.2 最终一致性设计
我们采用的分级补偿方案:
- 第一层:Redis事务日志,5秒内可回滚
- 第二层:MySQL的binlog监听,补偿超时任务
- 第三层:定时对账任务,每小时全量校验
4. 实战中的隐藏陷阱
4.1 缓存击穿预防
某个价值高的优惠券ID被恶意攻击时,可能导致所有请求穿透到DB。我们的防御方案:
java复制public Coupon getCoupon(Long id) {
Coupon coupon = redis.get(id);
if(coupon == null) {
// 使用互斥锁重建缓存
String lockKey = "lock:" + id;
if(redis.setnx(lockKey, "1")) {
redis.expire(lockKey, 10);
coupon = db.get(id);
redis.set(id, coupon);
redis.del(lockKey);
} else {
// 等待100ms后重试
Thread.sleep(100);
return getCoupon(id);
}
}
return coupon;
}
4.2 带宽成本控制
在一次真实活动中,我们忽略了图片资源的消耗。原价100元的优惠券展示图(200KB),在百万QPS下产生了:
code复制200KB * 1,000,000 = 200GB/s
这直接导致了CDN费用暴涨。解决方案:
- 极简页面设计(纯文本+CSS)
- 静态资源预加载
- 智能降级:当QPS超过阈值时,返回简化版响应
5. 压测必须关注的指标
我们建立的压测checklist:
| 指标项 | 达标线 | 检测方法 |
|---|---|---|
| 平均响应时间 | <200ms | JMeter聚合报告 |
| P99响应时间 | <500ms | Prometheus+Grafana |
| 错误率 | <0.01% | 日志监控系统 |
| Redis延迟 | <2ms | redis-cli --latency |
| DB连接池等待 | <10ms | Druid监控台 |
6. 成本优化实战技巧
在某次618大促中,我们通过以下调整节省了60%的服务器成本:
- 弹性扩缩容:活动前5分钟扩容,峰值后立即缩容。使用K8s+HPA实现:
yaml复制metrics:
- type: External
external:
metric:
name: qps
selector:
matchLabels:
app: coupon-service
target:
type: AverageValue
averageValue: 50000
- 智能预热:
- 提前15分钟加载热点数据到本地缓存
- 使用渐进式流量增加(5%→100% over 3分钟)
- 连接池优化:
java复制// Tomcat配置
server.tomcat.max-threads=500
server.tomcat.accept-count=100
// Druid配置
spring.datasource.druid.max-active=50
spring.datasource.druid.initial-size=5
7. 灾备方案设计
我们遇到过的最严重故障是机房网络中断。现在的方案:
-
多活架构:
- 两个机房同时提供服务
- 使用ShardingSphere进行数据分片
sql复制CREATE SHARDING TABLE RULE t_order ( RESOURCES(ds_0,ds_1), SHARDING_COLUMN=user_id, TYPE(NAME=hash_mod,PROPERTIES("sharding-count"=4)) ); -
熔断降级:
- 当错误率超过5%时,自动切换为排队模式
- 核心服务与非核心服务隔离
-
数据快速迁移:
- Redis的AOF持久化+每小时RDB备份
- MySQL的GTID复制延迟控制在10秒内
在实际开发中,我发现最容易被忽视的是"慢SQL"问题。有一次压测时QPS始终上不去,最后发现是一条SELECT ... FOR UPDATE语句没有走索引。现在我们的检查清单包括:
- 所有查询必须带EXPLAIN验证
- 禁止联表查询
- 单次事务不超过5个SQL
另一个血泪教训是日志级别。曾经因为DEBUG日志过多导致磁盘IO打满,现在强制要求:
- 生产环境只保留ERROR日志
- 关键路径日志异步写入
- 每个请求日志不超过1KB
