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

404收集者
2025-12-18 02:12
阅读 1430

上周五晚上,我正用 VSCode 写着一个 Java 接口,突然钉钉弹出一条消息:“双11预热流量压测失败,TPS掉到200了。”
那一刻,我手里的冰美式差点洒在机械键盘上——这可是我入职新公司两个月来第一次碰上这种级别的线上事故。作为一个刚转正还没多久的后端菜鸟,心里默念:“完了,不会第一天转正第二天就背锅吧?”

其实也不是第一次被高并发问题暴打。以前自己在家折腾小项目时,用 Python Flask 写个爬虫 API,几百 QPS 就崩了,但那会儿顶多自嘲一句“服务器太穷”,重启一下完事。可现在不一样,公司做的是电商平台,用户动辄百万级,老板眼睛盯着 GMV,产品经理天天在群里@你:“能不能再快一点?用户体验很重要!”

为什么我开始认真研究高并发?

说白了,是被逼的。

入职第一个月,团队让我接手一个商品详情页接口。需求很简单:根据 SKU ID 返回商品信息、库存、促销价。我以为不就是查几个表拼个 JSON 吗?结果上线第三天,大促活动一开,接口直接 502,监控图红得像番茄炒蛋。

运维同事幽幽地说:“兄弟,你这个接口没缓存、没限流、没异步,数据库连接池都打爆了……你是想让 DBA 提前退休吗?”

那一刻我才意识到:在学校写 CRUD 和在真实高并发场景下写 CRUD,完全是两个物种

而更扎心的是,我平时在本地用 Cursor + Copilot 写代码飞起,各种 AI 建议一键生成,但一到生产环境,AI 可救不了你——它能帮你写 Redis 缓存逻辑,但不能告诉你为什么缓存雪崩会导致全站瘫痪。


别再只谈“理论”了,实战才是硬道理

很多人(包括我)一开始学高并发,都是从“CAP 理论”、“BASE 原则”、“一致性哈希”这些词开始的。听起来很高大上,但真到了写代码的时候,发现连“怎么防重复提交”都没搞明白。

所以这次我决定:不讲虚的,只讲我们团队真实踩过的坑和解决方案

场景还原:商品详情页扛不住流量

我们的系统主语言是 Java(Spring Boot),部分数据处理脚本用 Python(比如定时同步第三方库存)。商品详情页的原始实现大概是这样的:

@GetMapping("/product/{skuId}")
public ProductDetail getDetail(@PathVariable String skuId) {
    Product product = productMapper.selectById(skuId);
    Stock stock = stockService.getStock(skuId); // 调内部 RPC
    Promotion promo = promoService.getCurrentPromo(skuId);
    return buildDetail(product, stock, promo);
}

看起来没问题?错!三个致命问题:

  1. 每次请求都查 DB + 调 RPC → 数据库和下游服务压力爆炸
  2. 没有缓存 → 100 万用户刷同一个爆款,DB 直接躺平
  3. 没有熔断降级 → 一旦库存服务挂了,整个详情页不可用

第一步:加缓存,但别乱加

我们首先上了 Redis 缓存。思路很简单:先查缓存,没有再查 DB,然后回填。

但很快又翻车了——缓存穿透。有人用不存在的 SKU ID 疯狂刷接口,导致每次都要查 DB。解决方法?布隆过滤器 or 缓存空值。我们选了后者(简单粗暴):

String cacheKey = "product:" + skuId;
String json = redis.get(cacheKey);
if (json != null) {
    if ("NULL".equals(json)) return null; // 空值缓存
    return JSON.parseObject(json, ProductDetail.class);
}

ProductDetail detail = loadFromDB(skuId);
if (detail == null) {
    redis.setex(cacheKey, 60, "NULL"); // 缓存空值 60 秒
} else {
    redis.setex(cacheKey, 300, JSON.toJSONString(detail));
}

小吐槽:产品经理听说我们要缓存 5 分钟,立马跳起来:“用户改了价格,5 分钟看不到?那不行!”
最后妥协:热点商品用 30 秒,普通商品 5 分钟。运维加了后台手动刷新按钮——程序员的尊严,有时候就是靠这种“临时方案”保住的。

第二步:异步化 + 降级策略

有些字段不是核心信息,比如“用户是否收藏过该商品”。这类非关键数据,完全可以异步加载降级返回默认值

我们用 Spring 的 @Async 把非核心逻辑扔到线程池:

@Async
public CompletableFuture<Boolean> checkIfFavorited(String userId, String skuId) {
    // 调用户服务,可能慢
    return CompletableFuture.completedFuture(favService.isFav(userId, skuId));
}

同时,在 Feign Client 上加了 Hystrix 熔断(虽然 Hystrix 已停更,但我们还没迁到 Sentinel):

@FeignClient(name = "stock-service", fallback = StockServiceFallback.class)
public interface StockServiceClient {
    @GetMapping("/stock/{skuId}")
    Stock getStock(@PathVariable String skuId);
}

降级类直接返回“库存充足”,至少页面能看,不至于白屏。

第三步:限流,别让系统被冲垮

再好的系统也怕流量洪峰。我们用了 Guava RateLimiter + Nginx 层限流 双保险。

Java 层限流(单机):

private final RateLimiter limiter = RateLimiter.create(1000); // 每秒 1000 请求

public ResponseEntity<?> getProduct(...) {
    if (!limiter.tryAcquire()) {
        return ResponseEntity.status(429).body("Too many requests");
    }
    // 正常处理
}

但单机限流失效于集群环境,所以我们还在 Nginx 上配了全局限流:

limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;

location /product/ {
    limit_req zone=api burst=100 nodelay;
    proxy_pass http://backend;
}

血泪教训:有一次测试没开限流,压测脚本跑疯了,把测试数据库干挂了。DBA 在群里发了个“💀”,我默默点了杯奶茶道歉。


Java vs Python:在高并发里各司其职

很多人问:“为啥不用 Python 写高并发服务?” 其实我们团队也有讨论。

  • Java:主力语言,JVM 成熟,线程模型稳定,生态强大(Spring Cloud、Dubbo、RocketMQ 全家桶),适合核心交易链路。
  • Python:用来做离线任务数据清洗运营脚本。比如每天凌晨用 Python 脚本聚合用户行为日志,生成推荐候选集,再推给 Java 推荐服务。

但 Python 的 GIL 注定了它不适合高并发 I/O 密集型 Web 服务(除非上 ASGI + asyncio,但维护成本高)。所以我们坚持:核心链路用 Java,边缘任务用 Python

举个例子:大促期间,我们会用 Python 脚本实时监控 Redis 缓存命中率,一旦低于 95%,自动告警。这种轻量级任务,Python 写 20 行搞定,何必上 Java?


生产环境的经验包:别等到线上炸了才后悔

经过几次“深夜救火”,我们总结了几条保命经验:

问题类型 解决方案 教训来源
缓存雪崩 随机 TTL + 多级缓存 去年双11,缓存集体失效
数据库慢查询 强制走索引 + 读写分离 一次全表扫描拖垮主库
线程池打满 显式配置核心/最大线程数 默认线程池无限扩容OOM
日志爆炸 异步日志 + 采样打印 磁盘写满,服务假死

另外,压测一定要贴近真实流量。我们之前用 JMeter 模拟均匀请求,结果线上还是崩了——因为真实用户是“突发性”的(比如整点抢购)。后来改用 GoReplay 录制生产流量回放,效果好多了。


最后:高并发不是炫技,而是克制

写这篇文章的时候,我刚用 Cursor 优化了一段缓存刷新逻辑。AI 建议我用 Redisson 的分布式锁防止并发重建,我点了采纳——但心里清楚,工具再强,也替代不了对系统的敬畏

高并发系统设计,从来不是堆砌“微服务、Kafka、Redis、分库分表”这些 buzzword。而是:

  • 知道什么时候该缓存,什么时候不该
  • 明白降级不是失败,而是优雅的退让
  • 理解限流不是阻碍用户,而是保护系统

现在我们的商品详情页,在 5000+ TPS 下稳如老狗。虽然偶尔还会被产品经理催“能不能再快 100ms”,但至少,我不用再担心周五晚上接到“服务挂了”的钉钉消息了。

对了,如果你也在用 VSCode + Cursor 写 Java,记得装 Redis 插件JProfiler 集成——别问我怎么知道的,问就是 OOM 到凌晨三点的痛。

共勉,打工人。

评论 0

最热最新
暂无评论
404收集者Lv.1
0
影响力
0
文章
0
粉丝