高并发系统设计:从理论到实践
去年双11前夜,我蹲在成都办公室的角落里啃着冷掉的冒菜,盯着 Grafana 上那条不断飙升的 QPS 曲线,手心全是汗。那一刻我真想把产品经理拉过来问问:“你确定‘简单加个缓存’就能扛住流量洪峰?”——当然,这只是幻想。现实中我只能默默重启第三个 Redis 实例,祈祷它别再 OOM。
我是京东的一名后端工程师,入行五年,经历过六次大促,从最初的“看监控就腿软”到现在能一边喝冰美式一边淡定地切流量,也算是在高并发这口高压锅里炖熟了。最近团队在重构一个核心交易链路,正好趁这个机会把这几年踩过的坑、攒下的经验整理一下,顺便记录下自己从 Java 老兵向 Rust 新手过渡的一些思考。
一切始于一个“不可能”的需求
事情得从上个月说起。产品那边突然丢过来一个 PRD,说要搞个“秒杀专区”,要求支持 10w+ QPS,响应时间 < 50ms,99.99% 可用性。我当场就笑了:“兄弟,咱们现在主站峰值也就 3w QPS,你这直接翻三倍?数据库怕不是要当场去世。”
但笑归笑,活儿还是得干。毕竟在互联网公司,deadline 是第一生产力。于是我们拉了个小群,名字就叫“别崩就行”,开始了这场高并发攻坚战。
架构不是画出来的,是压测压出来的
很多人以为高并发系统就是堆中间件:Redis、Kafka、Nginx、CDN……但真正的问题从来不在工具本身,而在于怎么组合它们。
我们最初的设计很“教科书”:
- 前端用 JavaScript 发起请求(对,JS 也是高并发链条里的一环!)
- 网关层限流
- 服务层查库存、扣减
- 数据库写订单
结果第一次压测就炸了。MySQL CPU 直接飙到 100%,大量 Deadlock found when trying to get lock 报错刷屏。运维老哥在群里发了个“?”的表情包,我默默回了个“对不起”。
问题出在哪?热点数据 + 行锁竞争。所有请求都在争抢同一行库存记录,数据库成了瓶颈。
缓存不是万能的,但没有缓存是万万不能的
我们立刻加了 Redis 缓存库存。但光缓存还不够——如果缓存失效瞬间涌入大量请求,还是会打穿 DB(这就是经典的“缓存击穿”)。于是我们做了三层防护:
- 本地缓存(Caffeine):热点商品 ID 在 JVM 内缓存,减少 Redis 访问
- 分布式锁:用 Redis 的
SETNX控制缓存重建,避免雪崩 - 异步队列削峰:非核心操作(如发券、日志)扔进 Kafka,解耦主流程
// Java 伪代码:带本地缓存 + 分布式锁的库存查询
public Integer getStock(Long skuId) {
// 先查本地缓存
Integer stock = localCache.getIfPresent(skuId);
if (stock != null) return stock;
// 尝试获取分布式锁
String lockKey = "lock:stock:" + skuId;
if (redis.setnx(lockKey, "1", 3)) {
try {
// 双重检查
stock = localCache.getIfPresent(skuId);
if (stock == null) {
stock = db.queryStock(skuId); // 查 DB
redis.setex("stock:" + skuId, 60, stock);
localCache.put(skuId, stock);
}
} finally {
redis.del(lockKey);
}
} else {
// 没拿到锁,短暂等待后重试或降级
Thread.sleep(10);
return getStock(skuId);
}
return stock;
}
这段代码上线后,DB 压力直接降了 80%。但新问题来了:本地缓存一致性怎么保证?比如库存变了,其他实例的 Caffeine 还是旧值。
我们最后用 Redis 的 Pub/Sub 通知各节点失效本地缓存。虽然有点“重”,但在高并发场景下,一致性比性能更重要(除非你能接受超卖)。
JavaScript 不是旁观者,而是发起者
很多人觉得前端只是“展示层”,但在高并发场景下,前端策略直接影响后端压力。
我们的前端同学一开始用的是轮询:“每 100ms 问一次后端能不能下单”。好家伙,10w 用户就是 100w QPS 打过来。我直接在群里喊话:“求你们改成长轮询或者 WebSocket 吧!”
后来他们用上了 Server-Sent Events (SSE),配合后端的库存状态推送,请求量直接砍掉 90%。而且 JS 层还加了“按钮防抖”和“失败重试指数退避”,极大减少了无效请求。
所以说,高并发不是后端一个人的事。前端、网关、服务、DB,每一环都要优化。
数据库:别再用 ORM 一把梭了
在高并发写场景下,Hibernate/MyBatis 这些 ORM 框架的自动 SQL 生成往往不够高效。我们最终对核心表做了手动 SQL 优化 + 分库分表。
比如库存扣减,我们不用 UPDATE stock = stock - 1 WHERE id = ?,而是用 CAS(Compare And Set):
UPDATE product_stock
SET stock = stock - 1
WHERE sku_id = ? AND stock > 0;
然后通过 JDBC 返回的 affectedRows 判断是否成功。这种方式天然避免超卖,还不需要显式加锁。
另外,我们把订单表按 user_id 哈希分了 64 库,每个库 16 表。虽然 ShardingSphere 配置起来有点反人类,但至少 DB 不再是单点瓶颈。
| 方案 | QPS | 平均延迟 | 超卖风险 |
|---|---|---|---|
| 原始 ORM + 单表 | ~2k | 120ms | 高 |
| 手动 SQL + CAS | ~15k | 40ms | 无 |
| + 分库分表 | ~50k+ | 35ms | 无 |
异步化:让系统“喘口气”
同步阻塞是高并发的大敌。我们把能异步的全异步了:
- 下单成功后,发消息到 Kafka
- 消费者处理积分、优惠券、通知等
- 前端通过轮询或 SSE 获取最终状态
这样主链路从 8 个 RPC 调用减少到 2 个,RT 从 200ms 降到 35ms。而且就算下游挂了,也不影响下单。
不过异步也有代价:状态不一致。用户可能看到“下单成功”但没收到券。我们加了补偿任务 + 对账系统,每天凌晨跑一遍,确保最终一致。
最近在折腾 Rust:Java 老兵的新玩具
说个题外话。最近被 Go 和 Rust 刷屏,加上 ChatGPT 写 Rust 代码越来越稳,我也开始学 Rust 了。不是说要抛弃 Java——毕竟 Spring Cloud 生态太香了——但 Rust 的内存安全和零成本抽象,真的适合做高性能网关或中间件。
上周我用 Rust 写了个简单的限流组件,对比 Java 版本,在同样配置下吞吐高了 40%,内存占用少了一半。虽然现在团队主力还是 Java(毕竟五年的屎山不能说扔就扔),但我觉得未来某些边缘服务可以用 Rust 重写。
总结:高并发没有银弹,只有权衡
经过三轮压测 + 两次线上灰度,我们的秒杀系统终于能在 12w QPS 下稳如老狗。回头看,其实没用什么黑科技,就是把缓存、异步、削峰、分片、降级这些老套路组合好。
几点心得送给大家:
- 别迷信理论:CAP、BASE 很美好,但线上事故往往死于一个没加索引的 SQL
- 监控要前置:Metrics、Tracing、Logging 三件套必须在开发阶段就集成
- 压测要真实:用 JMeter 模拟的流量和真实用户行为差很远,最好用影子库+流量回放
- 留好逃生通道:开关、降级、熔断,关键时刻能保命
最后,感谢我的队友们——特别是那个总在凌晨三点帮我捞日志的运维小哥。也感谢 ChatGPT,上周五晚上我卡在一个 Redis Lua 脚本 bug 里,它三句话就指出了我漏了 KEYS 和 ARGV 的区分。
高并发系统设计,说到底不是技术问题,而是工程问题。它考验的不是你会不会写算法,而是你能不能在 deadline 前,用最稳妥的方式,把一堆不完美的组件拼成一个能跑的东西。
就像我们成都人常说的:“慢慢来,比较快。”——前提是,你得先跑起来。
(完)
P.S. 如果你也在备战大促,记得备份冒菜店电话。有时候,一顿热乎的串串比任何架构图都管用。

评论 0