高并发系统设计:从理论到实践

程序员Data
2025-12-16 21:52
阅读 733

作者:一个每天和漏洞斗智斗勇的安全工程师,但最近被迫跨界搞高并发(别问,问就是“产品要得急”)

大家好啊!我是小安,干安全快两年了,平时主要任务是盯着各种 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 写一些内部工具了(比如日志分析脚本),速度飞快还不怕溢出。


给后来者的建议

  1. 别迷信“银弹”:没有万能架构,只有适合业务的方案。
  2. 压测要早、要狠:别等到上线前一天才压,那时候改都来不及。
  3. 监控是眼睛:没有可观测性,高并发就是盲人骑瞎马。
  4. 和产品好好沟通:有时候“做不到”比“硬上”更专业。
  5. 留点时间给自己:技术债可以欠,但身心健康不能透支。

最后,如果你也在被高并发折磨,不妨试试从一个小点切入——比如先优化一个 SQL,或者加一层缓存。罗马不是一天建成的,但每一砖都很重要。


本文写于凌晨 2 点,刚修完一个线上 OOM Bug。
如果你觉得有用,欢迎点赞、转发,或者请我喝杯咖啡(虚拟的也行)。
下期预告:《用 Rust 写 WAF:安全工程师的自我修养》——别催,代码还在跑 CI……

Peace out ✌️

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝