高并发系统设计:从理论到实践
上周五晚上 10 点半,我一边戴着耳机听周杰伦的《晴天》,一边在工位上盯着屏幕上疯狂刷屏的 GC 日志——我们新上线的秒杀活动接口又崩了。产品经理小王在企业微信群里疯狂@我:“线上挂了!用户都炸锅了!”,运维老李也在旁边幽幽地说:“兄弟,你这 Java 应用吃内存比我们家狗啃骨头还快。”
那一刻,我真的想砸电脑。
但不行啊,毕竟我才入职这家新公司俩月,副业接的外包单子还没结款,房贷还在等着还。只能深吸一口气,默默打开了 IDEA,开始排查问题。
为什么突然要搞高并发?
事情得从一个月前说起。我们产品组接到老板的“圣旨”:要在下个月双 11 前上线一个限量商品抢购功能,预计峰值 QPS 要扛住 5w+。我一听就头皮发麻——我们现在的系统架构还是单体应用,数据库是 MySQL 单点,缓存用得也不规范,连 Redis 都没做集群。
说实话,刚入职时我以为就是个 CRUD 项目,结果一上来就要搞高并发,属实有点懵。为了不被祭天,我赶紧翻出压箱底的几本书籍:《高性能 MySQL》、《Redis 设计与实现》,还有那本被无数人吹爆的《大型网站技术架构》。边看边做笔记,咖啡当水喝,头发一把一把掉。
先别急着写代码,想想架构
很多新人(包括曾经的我)一听到高并发,第一反应就是:“上 Redis!”、“加线程池!”、“用消息队列削峰!”。但其实,高并发不是靠某个中间件堆出来的,而是靠合理的架构设计和对业务场景的深入理解。
比如我们的秒杀场景,核心就三个字:快、稳、准。
- 快:用户点击“立即抢购”后,响应必须快,最好 100ms 内返回结果;
- 稳:哪怕 10w 人同时点,系统也不能崩;
- 准:不能超卖,也不能少卖,库存数字必须精确。
基于这个目标,我画了一张草图(其实就是用 draw.io 搞的简陋架构图),主要分三层:
- 接入层:Nginx + Lua 做限流,防刷;
- 服务层:Java 微服务,用 Spring Boot + Redis + RocketMQ;
- 数据层:MySQL 分库分表 + Redis 缓存预热。
别看现在说起来头头是道,当初设计时可是被架构师怼了三次。他说:“你这 Redis 直接写库存,万一宕机了怎么办?” 我当场愣住,心想:完蛋,又要改方案。
代码人生:从“能跑就行”到“优雅可靠”
以前接外包的时候,我写代码信奉“能跑就行主义”——只要客户验收通过,管它内存泄漏还是死锁。但现在不行了,公司有 Code Review,有 Sonar 扫描,还有那个眼神犀利的测试妹子天天拿着 JMeter 压我接口。
所以这次,我逼着自己把代码质量提上来。比如库存扣减,我一开始写了个简单的:
// 初版:千万别学!
public boolean deductStock(Long skuId, int num) {
Integer stock = redisTemplate.opsForValue().get("stock:" + skuId);
if (stock >= num) {
redisTemplate.opsForValue().set("stock:" + skuId, stock - num);
return true;
}
return false;
}
结果上线压测时,1000 并发就出现超卖。为啥?因为 get 和 set 不是原子操作啊!两个线程同时读到库存是 10,都以为自己能买,结果扣成负数。
后来改成 Lua 脚本:
-- Lua 脚本保证原子性
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
end
return 0
再配合 Java 里的 execute 调用,终于稳了。那一刻,我感觉自己像个真正的程序员了——不再是那个“能跑就行”的外包仔。
数据库:别让 MySQL 成为瓶颈
很多人以为上了 Redis 就万事大吉,但别忘了,最终数据还是要落到 MySQL。如果直接让高并发请求打到 DB,分分钟给你干趴。
我们的做法是:
- 库存预热:活动开始前 10 分钟,把所有 SKU 的库存加载到 Redis;
- 异步落库:用户下单成功后,只写 Redis,然后发消息到 RocketMQ,由消费者异步更新 MySQL;
- 分库分表:订单表按 user_id 哈希分 16 库,每库 16 表,避免单表过大。
当然,异步也有风险。比如 MQ 消息丢了怎么办?我们加了补偿机制:每天凌晨跑一个对账任务,对比 Redis 和 MySQL 的库存差异,自动修复。
有一次凌晨三点,报警邮件把我吵醒:“库存不一致!SKU_12345 差 2 件!” 我爬起来一看,原来是 MQ 消费者挂了,消息堆积了。赶紧重启 + 手动重推,折腾到天亮。从此以后,我把监控告警配得更细了——CPU、内存、队列长度、消费延迟,一个都不能少。
性能调优:那些深夜踩过的坑
光有架构还不够,还得调优。以下是我踩过的几个经典坑:
1. 连接池不够用
Spring Boot 默认的 HikariCP 连接池只有 10 个连接。压测时,大量请求卡在“waiting for connection”,TPS 上不去。后来调整为:
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
2. Redis 大 Key 导致阻塞
我们一开始把整个商品信息 JSON 存 Redis,结果一个 Key 几十 KB。高并发时,GET 操作阻塞主线程,Redis 延迟飙升到 50ms+。后来拆分成多个字段,用 Hash 存储:
HSET product:123 name "iPhone" price 5999 stock 100
3. JVM 参数没调
默认的 JVM 参数在高并发下 GC 频繁。我们换成 G1 垃圾回收器,并调整堆大小:
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
调完后,Full GC 从每分钟一次降到一天不到一次。
效果如何?数据说话
经过三周的迭代 + 压测 + 修复,我们在预演环境做了全链路压测。结果如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | 1200 | 58000 |
| 平均响应时间 | 850ms | 65ms |
| 错误率 | 12% | 0.02% |
| CPU 使用率 | 95% | 60% |
双 11 当天,虽然流量冲到 6w QPS,但系统稳如老狗。运维老李拍了拍我肩膀:“小伙子,可以啊。” 产品经理小王也发了个红包:“辛苦了!”
那一刻,我感觉所有的熬夜、掉发、焦虑都值了。
写在最后:高并发不是终点,而是起点
很多人觉得高并发是“高级技能”,只有大厂才需要。但其实,只要你做的产品有用户、有流量,迟早会遇到性能瓶颈。而作为开发者,我们的代码人生不应该止步于“能跑”,而要追求“优雅、高效、可靠”。
回过头看,这次经历让我明白:高并发系统设计,70% 是架构思维,20% 是工程细节,10% 是工具使用。那些书里的理论,只有在真实业务中摔打过,才能真正内化成自己的能力。
顺便说一句,我现在副业接单子,也会主动问客户:“你们有没有高并发需求?” 如果有,我就按这套思路来——毕竟,谁不想做个靠谱的斜杠程序员呢?
哦对了,如果你也在搞高并发,欢迎留言交流。说不定下次双 11,我们还能一起“肝”到天亮,听着《晴天》,看着监控曲线平稳如初。
(完)
P.S. 最近在读《Designing Data-Intensive Applications》,强烈推荐!比国内某些“高并发秘籍”靠谱多了。别再信什么“三天掌握高并发”了,那都是割韭菜的。

评论 0