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

线程池保洁员
2025-12-18 00:52
阅读 1818

上周五晚上 10 点,我一边在 Vim 里敲着 :wq,一边盯着屏幕上不断跳动的 Grafana 监控曲线——QPS 又崩了。说实话,刚入职这家新公司才两个月,我就差点被线上事故送走两次。产品经理还在 Slack 里疯狂 at 我:“这个活动明天上线,流量预估 5w QPS,能扛住吧?” 我心里一万个草泥马奔腾而过,但表面上还得回个“OK,没问题”。

我是谁?一个前大厂搬砖人,去年裸辞在家躺了半年,天天琢磨职业方向,顺便把《深度学习入门》翻烂了。结果没忍住,还是回来写 Java 了。不过这次换了个赛道——搞高并发 + AI 融合系统。说白了,就是既要扛住用户刷屏抢券,又要给推荐模型喂实时特征。听起来很酷,做起来……想砸电脑。

今天这篇不是教科书式的八股文,而是我这两个月踩坑、背锅、通宵、改配置、被 SRE 教育后的真实复盘。如果你也在准备面试题挑战,或者正被运营催着“赶紧上活动”,希望这篇文章能帮你少掉几根头发。


起因:一场“预期内”的雪崩

事情得从上周那个“小”活动说起。运营同学拍脑袋定的“限时秒杀+AI个性化推荐”,要求同一时间支持 5 万用户并发请求。数据库是 MySQL,缓存 Redis,服务用 Spring Boot 写的,部署在 Kubernetes 上。

上线前压测:2w QPS,稳如老狗。
上线当天 10:00:3w QPS,CPU 飙到 90%,GC 频繁,接口 P99 延迟 800ms。
10:02:4.5w QPS,Redis 连接池打满,DB 主库 CPU 100%,大量超时错误。
10:03:SRE 报警群炸了,“@后端快看!订单服务挂了!”

我当时真的想原地辞职。但转念一想:这不就是传说中的“高并发系统设计”实战题吗?面试官最爱问的“如果让你设计一个秒杀系统,你怎么搞?”——现在它活生生砸我脸上了。


技术选型:别信“银弹”,只信“组合拳”

很多人一提高并发,张口闭口 Kafka、RocketMQ、Redis Cluster、分库分表……仿佛堆技术栈就能解决问题。但现实是:资源有限,时间紧迫,老板要的是“今晚能跑”而不是“架构图好看”

我们团队只有 3 个后端(包括我),运维就 1 个人,测试还在对接另一个项目。所以必须在最小改动成本下提升系统吞吐。以下是我们对比的几种方案:

方案 优点 缺点 适用场景
直接扩容 Pod 快,K8s 一键 scale 成本高,DB 成瓶颈 短期峰值,临时救火
本地缓存(Caffeine) 减少 Redis 压力,响应快 数据一致性难保证 读多写少,容忍短暂不一致
异步化(MQ 解耦) 削峰填谷,解耦下单/通知 增加系统复杂度 非核心链路(如发券、日志)
接口限流(Sentinel) 防止雪崩,保护系统 用户可能被拒绝 所有入口必须加
库存预热 + 分段库存 避免 DB 热点 需要提前计算库存分片 秒杀类场景

最后我们选择了 本地缓存 + 限流 + 库存分段 + 异步落库 的组合。为什么?因为改得最少,见效最快。


实战:代码和配置比嘴炮有用

1. 本地缓存:用 Caffeine 挡住第一波流量

Redis 虽好,但网络开销不可忽视。对于商品信息这种几乎不变的数据,直接用本地缓存:

// 商品缓存,TTL 5分钟,最多缓存1000个
private final Cache<Long, Product> productCache = Caffeine.newBuilder()
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .maximumSize(1000)
    .build();

public Product getProduct(Long id) {
    return productCache.get(id, this::loadFromDb);
}

注意:这里用了 get(key, mappingFunction),避免缓存穿透。同时配合布隆过滤器(BloomFilter)防止恶意 ID 查询。

2. 限流:Sentinel 不是摆设

以前觉得限流是“防君子不防小人”,直到线上被打爆。现在所有对外接口都加上了 Sentinel 注解:

@SentinelResource(
    value = "createOrder",
    blockHandler = "handleOverload"
)
public Order createOrder(OrderRequest req) {
    // 业务逻辑
}

public Order handleOverload(OrderRequest req, BlockException ex) {
    throw new BusinessException("系统繁忙,请稍后再试");
}

规则配置(通过 Nacos 动态推送):

  • QPS 阈值:3000/实例
  • 熔断策略:慢调用比例 > 50% 且 RT > 200ms,熔断 30s

3. 库存分段:避免单行锁竞争

传统秒杀库存 update stock = stock - 1 where id = ? and stock > 0,在高并发下会锁表。我们改成分段库存

  • 将 10000 件商品分成 100 个库存段(每段 100 件)
  • 请求随机分配到某一段(segmentId = userId % 100
  • 更新时只锁对应段
-- 库存表结构
CREATE TABLE item_stock_segment (
    item_id BIGINT,
    segment_id INT,
    stock INT,
    PRIMARY KEY (item_id, segment_id)
);

这样,原本 1 行的竞争变成了 100 行,MySQL InnoDB 行锁压力骤降。

4. 异步化:非核心操作扔进 MQ

用户下单成功后,发优惠券、更新推荐特征、记录行为日志……这些统统异步:

@Transactional
public Order placeOrder(OrderRequest req) {
    // 1. 扣减库存(本地事务)
    // 2. 创建订单(本地事务)
    
    // 3. 发送MQ消息(事务消息 or 最终一致性)
    mqProducer.send(new OrderCreatedEvent(orderId, userId));
    
    return order;
}

我们用的是 RocketMQ 的事务消息,确保“订单创建”和“消息发送”最终一致。虽然复杂了点,但换来的是主流程 RT 从 400ms 降到 120ms。


运维视角:光写代码不够,得看监控

作为前大厂人,我深知“可观测性”不是口号。这次事故后,我和 SRE 同学一起加了几个关键指标:

  • JVM GC 频率 & 暂停时间(用 Prometheus + JMX Exporter)
  • Redis 连接池使用率redisson.connection.pool.size.used
  • DB 慢查询数量(MySQL slow log + pt-query-digest)

特别吐槽一下:很多团队只监控 QPS 和错误率,却忽略了资源水位。比如这次,DB CPU 打满是因为有个 N+1 查询没加 JOIN FETCH,结果每笔订单查 10 次用户信息。加个 @EntityGraph 就解决了,但没人看 SQL 监控就永远发现不了。


面试题挑战?不如先搞定线上

很多人刷 LeetCode、背八股文,以为“知道 CAP 理论”就能设计高并发系统。但现实是:线上系统的稳定性,90% 靠细节,10% 靠架构

  • 你有没有设置合理的线程池队列大小?(别用无界队列!)
  • 你的连接池 maxActive 是多少?会不会把 DB 打死?
  • 缓存穿透、击穿、雪崩,你真的处理了吗?
  • 日志打太多,磁盘写满导致服务假死?

这些才是真实世界的“面试题”。我在前东家时,Leader 曾说过一句让我记到现在的话:“能在线上跑一年不出事的系统,比任何 PPT 架构都牛逼。


总结:高并发不是炫技,是克制

折腾两周后,系统终于稳住了。5w QPS 下,P99 延迟 < 200ms,DB CPU 保持在 60% 以下。虽然离“百万并发”还差得远,但至少没再被运营 at 到凌晨三点。

回头看看,其实没用什么黑科技。核心就三点:

  1. 识别瓶颈:用监控数据说话,别猜
  2. 最小改动:在现有架构上打补丁,别重写
  3. 资源意识:每一行代码都要考虑 CPU、内存、IO、网络的消耗

最近还在学 AI,想着能不能用模型预测流量峰值,动态扩缩容。不过那又是另一个故事了……现在嘛,先让我好好睡一觉,毕竟明天产品经理又要开需求会了。

对了,如果你也在经历类似的痛苦,欢迎评论区交流。Vim 党永不为奴,但高并发真能要命。

评论 0

最热最新
暂无评论
线程池保洁员Lv.1
0
影响力
0
文章
0
粉丝