高并发系统设计:从理论到实践
作者:一个每天和漏洞斗智斗勇的安全工程师,但最近被迫跨界搞高并发(别问,问就是“产品要得急”)
大家好啊!我是小安,干安全快两年了,平时主要任务是盯着各种 Log、挖洞、修漏洞,顺便在 PR 里疯狂加 if (input == null)(开玩笑啦)。不过最近画风有点歪——上个月我们组接了个新活儿:给一个核心产品做性能压测和架构优化,目标是支撑 10w+ QPS 的真实流量。
说实话,我一开始是拒绝的。我可是安全岗诶,又不是后端大佬!但 PM 拍着胸脯说:“你们安全团队最懂系统边界,最适合做这个!”(呵呵,这锅甩得真丝滑)再加上老板一句“年轻人多学点没坏处”,我就被扔进了高并发的深水区。
更离谱的是,项目 deadline 是上周五晚上的 23:59 —— 没错,又是周五晚上。那天我一边啃泡面一边改配置,差点把键盘泡进汤里。
所以今天这篇博客,既是复盘,也是吐苦水,更是给像我这样“半路出家”的同学一点参考。咱们不讲虚的,直接上干货,从真实场景出发,聊聊怎么把一个平平无奇的 Spring Boot 应用,硬生生“卷”成高并发系统。
起因:一个“简单”的产品需求
事情得从上个月说起。
产品经理小李(对,又是他)跑来说:“我们要上线一个实时库存扣减接口,双11大促要用。”
我:“哦,行吧,调个 Redis 就完事了?”
他:“对!而且要求延迟 < 50ms,QPS 至少 8w,不能丢请求,不能超卖,还得支持灰度发布……”
我当时就笑了:你当这是写 Hello World 呢?
但笑完就得干活。我们手头的系统是个典型的 Spring Boot 单体应用,用 MySQL + MyBatis,前端直连后端,部署在 4 台 4C8G 的虚拟机上。日常 QPS 也就 500 左右,数据库 CPU 经常飙到 70%。这种架构去扛 8w QPS?怕不是要去 ICU 报道。
于是,我们拉上了后端、DBA、运维开了个紧急会。会上 DBA 直接拍桌子:“MySQL 单机撑死 3k QPS,你们这需求等于让我拿拖拉机跑 F1!”
得,看来不动架构不行了。
第一阶段:先别想着重构,先让系统“活下来”
在真正动刀子之前,我们先做了三件事:
1. 压测基线:知道自己的底裤有多短
用 JMeter 对现有接口压测,结果惨不忍睹:
| 并发数 | 平均响应时间(ms) | 错误率 | 数据库 CPU |
|---|---|---|---|
| 100 | 120 | 0% | 65% |
| 500 | 850 | 12% | 98% |
| 1000 | 超时 | >50% | 100% |
看到这数据,我和后端老王对视一眼,默默点了杯冰美式压惊。
2. 缓存救急:Redis 上场
既然数据库是瓶颈,那就把读写尽量挡在前面。我们立刻引入 Redis 做二级缓存:
- 库存查询走 Redis
- 扣减操作用 Lua 脚本保证原子性
- 设置合理的 TTL 和空值缓存防穿透
// 简化版 Lua 脚本示例
String script = "if redis.call('GET', KEYS[1]) >= ARGV[1] then " +
"return redis.call('DECRBY', KEYS[1], ARGV[1]) " +
"else return -1 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("stock:" + productId),
String.valueOf(quantity)
);
上线后效果立竿见影:QPS 提升到 3k,响应时间压到 30ms 以内。但离 8w 还差两个数量级……
3. 异步削峰:消息队列安排上
高并发的核心思想之一就是削峰填谷。我们引入 Kafka 做请求缓冲:
- 前端请求先入 Kafka Topic
- 后台消费者批量处理库存扣减
- 失败请求重试 + 死信队列兜底
但这里有个坑:一致性怎么保证?
比如用户付了钱,但消息还没消费,页面显示“库存不足”?那可就炸了。所以我们做了状态补偿机制:前端轮询订单状态,后端通过唯一 ID 去查最终结果。
虽然用户体验打了点折扣,但在 deadline 压力下,能跑通就行(产品经理也认了,毕竟保命要紧)。
第二阶段:架构升级,向“综合”解决方案靠拢
光靠缓存和 MQ 只是止痛药,真正的解药是系统性重构。
我们定了几个原则:
- 读写分离:查询走缓存/ES,写入走异步队列
- 无状态化:服务必须能水平扩展
- 熔断降级:宁可返回默认值,也不能雪崩
- 监控闭环:Metrics + Logs + Traces 三位一体
Spring Boot 的优化清单
作为 Java 技术栈的老兵,Spring Boot 是我们的主战场。但默认配置在高并发下就是“纸老虎”。以下是我们的调优清单:
1. Web 容器调优(Tomcat → Undertow)
Tomcat 在高并发下表现一般,我们换成 Undertow:
# application.yml
server:
undertow:
io-threads: 16
worker-threads: 200
buffer-size: 1024
direct-buffers: true
实测 QPS 提升约 15%。
2. 连接池别再用默认值!
HikariCP 的默认连接池大小是 10,这在高并发下就是自残。根据公式:
connections = ((core_count * 2) + effective_spindle_count)
我们设为 50,并配合监控动态调整。
3. 接口设计:幂等 + 限流 + 熔断
- 所有写接口加
@RateLimiter注解(基于 Guava 或 Sentinel) - 关键方法加
@Idempotent标记,用 Redis Token 机制防重 - Feign 调用加 Hystrix 熔断(虽然后来换成了 Resilience4j)
@PostMapping("/deduct")
@RateLimiter(key = "#request.productId", limit = 1000, window = 1)
public Result deductStock(@RequestBody DeductRequest request) {
// ...
}
4. 日志别乱打!
高并发下日志 I/O 是隐形杀手。我们:
- 禁用 DEBUG 日志
- 异步日志(Logback AsyncAppender)
- 敏感字段脱敏(安全工程师的执念)
第三阶段:生产环境的“魔鬼细节”
理论再漂亮,线上一跑就露馅。以下是我们在生产环境踩过的几个经典坑:
坑1:Redis 连接池被打爆
初期我们用 Lettuce 默认配置,结果在 2w QPS 时大量 TimeoutException。原因是连接池太小,且未启用共享连接。
解决方案:
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(new GenericObjectPoolConfig() {{
setMaxTotal(100);
setMaxIdle(20);
}})
.build();
return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);
}
坑2:Kafka 消费者 lag 飙升
消费者线程太少,处理不过来。我们从单线程改成多线程 + 手动提交 offset:
@KafkaListener(topics = "stock-deduct", concurrency = "8")
public void consume(List<ConsumerRecord<String, String>> records) {
executorService.submit(() -> processBatch(records));
}
同时监控 consumer_lag,超过阈值自动告警。
坑3:Full GC 导致服务暂停
JVM 参数还是默认的,高峰期 Young GC 频繁,偶尔 Full GC 停顿 2s+。
调优后:
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+ParallelRefProcEnabled
加上 Arthas 实时监控,终于稳了。
最终效果:从“危在旦夕”到“稳如老狗”
经过三周的地狱式加班(期间我写了 3 次辞职信又删了),我们在双11前一周完成全链路压测:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | 500 | 92,000 |
| P99 延迟 | 850ms | 42ms |
| 错误率 | >50% | 0.02% |
| 服务器数量 | 4 台 | 12 台(但成本反而降了,因为用了更便宜的容器实例) |
双11当天,系统平稳运行,零故障。运维老哥甚至发了个朋友圈:“今年终于不用半夜爬起来救火了。”
那一刻,我觉得泡面都香了。
一些个人感悟(带点 Rust 的私货)
干安全久了,总觉得自己只会在旁边喊“这里有洞!那里不安全!”。但这次高并发实战让我明白:安全和性能其实是同一枚硬币的两面。
- 缓存击穿 → 安全是 DoS 攻击
- 接口未限流 → 安全是暴力破解
- 消息重复消费 → 安全是重放攻击
很多防护思路是相通的。这也让我最近对 Rust 更感兴趣——内存安全 + 零成本抽象,简直是高并发 + 安全的梦幻 combo。虽然现在项目还是 Java 主导,但我已经在用 Rust 写一些内部工具了(比如日志分析脚本),速度飞快还不怕溢出。
给后来者的建议
- 别迷信“银弹”:没有万能架构,只有适合业务的方案。
- 压测要早、要狠:别等到上线前一天才压,那时候改都来不及。
- 监控是眼睛:没有可观测性,高并发就是盲人骑瞎马。
- 和产品好好沟通:有时候“做不到”比“硬上”更专业。
- 留点时间给自己:技术债可以欠,但身心健康不能透支。
最后,如果你也在被高并发折磨,不妨试试从一个小点切入——比如先优化一个 SQL,或者加一层缓存。罗马不是一天建成的,但每一砖都很重要。
本文写于凌晨 2 点,刚修完一个线上 OOM Bug。
如果你觉得有用,欢迎点赞、转发,或者请我喝杯咖啡(虚拟的也行)。
下期预告:《用 Rust 写 WAF:安全工程师的自我修养》——别催,代码还在跑 CI……
Peace out ✌️

评论 0