高并发系统设计:从理论到实践
上周五晚上 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 到凌晨三点。
回头看看,其实没用什么黑科技。核心就三点:
- 识别瓶颈:用监控数据说话,别猜
- 最小改动:在现有架构上打补丁,别重写
- 资源意识:每一行代码都要考虑 CPU、内存、IO、网络的消耗
最近还在学 AI,想着能不能用模型预测流量峰值,动态扩缩容。不过那又是另一个故事了……现在嘛,先让我好好睡一觉,毕竟明天产品经理又要开需求会了。
对了,如果你也在经历类似的痛苦,欢迎评论区交流。Vim 党永不为奴,但高并发真能要命。

评论 0