高并发系统设计:从理论到实践(一个被线上事故逼出来的教程)
作者注:我是 GitHub Copilot 付费用户,用了快两年。日常主力是 VSCode,插件装得比头发还多(虽然发际线也在配合)。在当前公司干了三年多,最近开始琢磨跳槽的事儿——不是因为工资低(好吧,其实也有点),主要是想看看外面的世界有没有更酷的高并发场景可以折腾。顺便,我是个喜欢钻底层原理的“杠精”,看到
synchronized就想翻 JVM 源码。
上周五晚上十点半,我正用 Copilot 自动补全一段 React 组件代码(是的,前端我也要碰,别问,问就是“全栈”),手机突然疯狂震动。运维群炸了:
“【P0 级告警】订单服务 CPU 100%,接口超时率 87%!”
我一口冰美式差点喷到屏幕上。又是双十二预热?这才十一月初啊!点开监控一看,好家伙,QPS 瞬间飙到 12w+,数据库连接池直接打满,Redis 响应延迟从 1ms 飙到 500ms。产品经理还在群里@我:“亲,能不能先扛住?明天上午有个大客户演示……”
那一刻,我真的想砸电脑。
但冷静下来一想——这不就是我一直想啃的高并发实战吗?理论看了不少,《高性能MySQL》《Designing Data-Intensive Applications》都翻烂了,可纸上谈兵终究不如真刀真枪干一场。于是,我决定复盘这次事故,并把整个优化过程写成这篇带血泪教训的项目案例剖析。如果你也在做 Java 后端,或者前端同学好奇后端怎么扛流量,这篇应该能帮到你。
背景:一个看似普通的秒杀活动
我们公司做的是一套 SaaS 电商中台,客户可以在后台配置“限时折扣”、“库存抢购”等活动。原本这类功能走的是常规下单流程:前端 → Nginx → Spring Boot API → MySQL + Redis。
问题出在活动入口页面。前端用的是 React,每次进入活动页会调用 /api/activity/detail/{id} 获取商品信息、库存、倒计时等。正常情况下 QPS 不过几百,但一旦有网红带货,瞬时流量直接把后端干趴。
最致命的是:这个接口没做任何缓存穿透/击穿防护,而且每次请求都会查数据库库存!
// 伪代码:原始接口
@GetMapping("/detail/{id}")
public ActivityDetail getDetail(@PathVariable Long id) {
// 直接查 DB!连 Redis 都没走!
return activityService.queryById(id);
}
是的,你没看错。上线前测试只跑了 100 并发,QA 说“没问题”,结果生产环境一上大流量,MySQL 的 InnoDB Buffer Pool 直接被刷爆,磁盘 IO 打满,连锁反应导致整个订单链路雪崩。
第一步:紧急止血 —— 缓存 + 限流
事故当晚,我和运维兄弟通宵搞了三件事:
Nginx 层加突发限流
先用limit_req把 QPS 控死在 5000,避免服务彻底挂掉。http { limit_req_zone $binary_remote_addr zone=api:10m rate=500r/s; server { location /api/activity/ { limit_req zone=api burst=2000 nodelay; proxy_pass http://backend; } } }接口层加 Redis 缓存
把活动详情缓存 5 秒(业务允许短暂不一致),Key 设为activity:detail:{id}。数据库连接池扩容
HikariCP 从默认 10 改到 100,虽然治标不治本,但至少让服务能喘口气。
搞定后,凌晨三点,接口终于恢复。但我知道,这只是临时方案——缓存过期瞬间还是可能被打穿,而且 5 秒缓存对用户体验也不友好。
第二步:重构核心逻辑 —— 三层防护体系
周末两天,我拉着前端同学一起重做了整个活动页架构。目标:扛住 10w+ QPS,且保证数据一致性。
架构图(脑内版):
前端 (React)
↓ (HTTP)
CDN + Nginx (静态资源 + 动态接口分离)
↓
Spring Boot 集群 (Java 17 + Spring Cloud)
├── 库存服务 (独立部署)
├── 活动服务 (带多级缓存)
└── 订单服务 (异步化)
↓
Redis Cluster (缓存 + 分布式锁)
↓
MySQL (分库分表 + 读写分离)
关键点 1:前端做“智能降级”
以前前端一进页面就狂刷 /detail,现在改成了:
- 首屏数据走 SSR(服务端渲染),由 Nginx 直接返回 HTML 片段(含基础活动信息)
- 倒计时、实时库存等动态数据,通过 WebSocket 推送(减少轮询)
- 如果后端响应慢,前端自动展示“加载中”兜底图,并提示“稍后再试”
前端同事吐槽:“你们后端总算知道前端也能帮忙扛流量了?” 我回他:“不然你以为 P0 事故谁背锅?”
关键点 2:Java 服务端多级缓存
我们搞了个三层缓存策略:
| 缓存层级 | 存储介质 | TTL | 用途 |
|---|---|---|---|
| L1 | Caffeine (本地) | 1s | 防止 Redis 热点 Key |
| L2 | Redis Cluster | 10s | 主缓存,支持集群 |
| L3 | MySQL | - | 最终一致性 |
核心代码(简化版):
@Service
public class ActivityCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final Cache<Long, ActivityDetail> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
public ActivityDetail getDetail(Long id) {
// L1: 本地缓存
ActivityDetail detail = localCache.getIfPresent(id);
if (detail != null) return detail;
// L2: Redis
String redisKey = "activity:detail:" + id;
detail = (ActivityDetail) redisTemplate.opsForValue().get(redisKey);
if (detail != null) {
localCache.put(id, detail); // 回填 L1
return detail;
}
// L3: DB + 写回缓存
detail = dbQuery(id);
if (detail != null) {
// 双写:先写 Redis,再写本地
redisTemplate.opsForValue().set(redisKey, detail, 10, TimeUnit.SECONDS);
localCache.put(id, detail);
}
return detail;
}
}
注意:这里没用 @Cacheable,因为 Spring Cache 不支持多级。自己手撸虽然累点,但可控性高。
关键点 3:库存扣减 —— 异步 + 分段锁
最怕的其实是超卖。之前直接 UPDATE stock SET count = count - 1 WHERE id = ? AND count > 0,高并发下 MySQL 行锁竞争严重。
新方案:
- Redis 预扣库存:活动开始前,把库存加载到 Redis Hash,如
stock:{activityId} -> {skuId: 100} - Lua 脚本原子扣减:
local stock = redis.call('HGET', KEYS[1], ARGV[1]) if tonumber(stock) > 0 then redis.call('HINCRBY', KEYS[1], ARGV[1], -1) return 1 end return 0 - 异步落库:扣减成功后,发 MQ 消息,由消费者异步更新 MySQL 库存(最终一致性)
这样,99% 的请求在 Redis 层就完成了,DB 压力骤降。
第三步:压测与调优 —— 别信理论,信数据
重构完不能直接上生产!我们用 JMeter + Arthas 做了全链路压测。
- 模拟 15w QPS 持续 10 分钟
- 监控指标:CPU、内存、GC、RT、错误率
踩坑记录:
Caffeine 本地缓存 OOM
初始设maximumSize=100_000,结果 Full GC 频繁。后来降到10_000,配合短 TTL,稳了。Redis Pipeline 未启用
批量查询时没用 pipeline,网络 RT 太高。改成redisTemplate.executePipelined()后,吞吐提升 3 倍。前端 WebSocket 连接数爆炸
每个用户一个连接,Nginx 默认worker_connections=1024不够用。调到 65535,并启用reuseport。
最终压测结果(单机 4C8G):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 最大 QPS | ~800 | 28,000 |
| 平均 RT (ms) | 1200+ | 35 |
| 错误率 | 40%+ | 0.02% |
| CPU 使用率 | 100% | 65% |
心得体会:高并发不是银弹,而是组合拳
这次事故让我彻底明白:没有“万能架构”,只有“合适场景”。
- 如果你是初创公司,用户量小,别一上来就搞分库分表,先把缓存和限流做好。
- 如果是金融场景,强一致性优先,宁可牺牲性能也要保证数据正确。
- 如果是电商秒杀,最终一致性 + 异步化才是王道。
另外,前端真的能帮你扛很多流量!别再把所有压力都甩给后端了。像我们这次,SSR + WebSocket + 智能降级,直接砍掉了 60% 的无效请求。
最后,Copilot 在这次重构中帮了大忙——自动生成 Lua 脚本、JMeter 配置、甚至 Nginx 规则。虽然它偶尔会写出 while(true) 死循环(别问我怎么知道的),但整体效率提升至少 30%。付费?值!
写在最后:关于跳槽
这次事故后,老板拍着我肩膀说:“小张啊,公司离不开你。”
我笑了笑,默默更新了简历。
不是因为怨气,而是我发现:在舒适区待久了,技术会生锈。高并发这种硬核活儿,一年可能就遇到一两次。我想去一个每天都在和流量搏斗的地方,比如某宝、某东、某音……
如果你也在准备跳槽,建议你:
✅ 动手做一个高并发小项目(哪怕只是模拟)
✅ 搞懂缓存、消息队列、分布式锁的底层原理
✅ 学会用监控工具(Arthas、Prometheus、SkyWalking)
毕竟,面试官问“你怎么设计秒杀系统”时,你总不能只背八股文吧?
附:关键配置速查表
| 组件 | 参数 | 建议值 | 说明 |
|---|---|---|---|
| HikariCP | maximumPoolSize | 50~100 | 根据 DB 连接数上限调整 |
| Redis | maxmemory | 80% 物理内存 | 避免 OOM |
| Nginx | worker_connections | 65535 | 高并发需调大 |
| JVM | -Xmx | 4g~8g | 避免频繁 GC |
| Caffeine | maximumSize | 10_000 | 本地缓存不宜过大 |
好了,这篇带血泪的高并发实战就到这里。如果对你有帮助,欢迎点赞、转发,或者在评论区吐槽你的 P0 事故——程序员的痛,只有程序员懂。
下次见!(希望是在新公司的工位上 😎)

评论 0