高并发系统设计:从理论到实践
上周五晚上十点半,我盯着电脑屏幕上那条不断刷屏的告警邮件,心里默默问候了产品经理全家——不是真的骂,是那种程序员式“又来了”的无奈苦笑。我们刚上线的新产品“秒杀活动”,在预热期就因为用户涌入量太大直接把后端服务干崩了,数据库 CPU 直接飙到 100%,API 响应时间从 50ms 蹭蹭涨到 5s+。
说实话,作为一个刚入职两个月的新人,本来以为就是个 CRUD 的活儿,结果第一天就被拉进“高可用架构攻坚小组”,领导拍着我肩膀说:“你不是对分布式系统有点研究吗?交给你了。” 😅
我是谁?为啥写这篇?
先简单介绍一下我自己:一个死忠 Vim 党,日常开发基本不用 IDE(JetBrains 系列在我眼里就是“资源吞噬者”),之前折腾过各种 AI 编程工具,Copilot、CodeWhisperer、TabNine 都试过,最后还是选了 Cursor —— 因为它能完美集成进我的终端流,还能用自然语言改代码,不打断心流。
现在在一家做电商 SaaS 的创业公司,团队氛围不错,但 deadline 永远比人命还紧。这次的“高并发”需求,本质上是为了支撑客户侧的一个大促活动,而我们的产品需要提供稳定、低延迟的接口能力。换句话说:产品要卖钱,系统不能崩。
所以,这篇文不是为了炫技,而是记录自己从“纸上谈兵”到“线上救火”的全过程,顺便给和我一样刚入行、被高并发吓到的小白一点参考。
问题来了:为什么一压测就崩?
我们的核心接口其实很简单:用户点击“抢购”按钮 → 后端校验库存 → 扣减库存 → 创建订单。逻辑清晰,代码也不复杂,用的是 Spring Boot + MySQL + Redis 的经典组合。
但一上 JMeter 压测(模拟 1000 并发),立马翻车:
- 数据库锁竞争激烈:
UPDATE stock SET count = count - 1 WHERE product_id = ? AND count > 0这条 SQL,在高并发下直接变成串行执行,MySQL 的 InnoDB 行锁成了瓶颈。 - 缓存穿透:有些恶意请求传不存在的
product_id,导致大量请求打到 DB。 - 超卖:虽然加了
count > 0判断,但在极端并发下,多个线程同时读到count=1,都以为还有库存,结果全扣成功,超卖了!
当时我真的想砸键盘。产品那边还在群里@我说:“能不能今晚搞定?明天客户就要演示了。” 🤦♂️
思路调整:从“能跑”到“扛得住”
痛定思痛,我翻出大学时看过的《Designing Data-Intensive Applications》,结合公司老大的建议,重新梳理了设计原则:
1. 流量分层:别让所有请求都打到数据库
- 前置限流:用 Nginx 层做 IP + 接口级别的限流(
limit_req_zone),防刷。 - 缓存兜底:Redis 预热商品库存,请求先查缓存,没命中再走 DB(但得防穿透)。
- 异步削峰:下单动作不直接创建订单,而是发消息到 Kafka,由消费者异步处理。
2. 库存扣减:原子性是底线
最初的 SQL 问题在于“先查后改”不是原子的。解决方案有两个方向:
- 数据库层面:用
UPDATE ... SET count = count - 1 WHERE product_id = ? AND count >= 1,靠数据库的行锁保证原子性(但性能差)。 - Redis + Lua 脚本:把库存存在 Redis,用 Lua 脚本保证“读-判断-写”在一个原子操作里完成。
我选了后者。毕竟 Redis 单线程模型天然适合这种场景,而且 QPS 能轻松上万。
-- stock.lua
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
else
return 0
end
Java 调用:
String script = "..."; // 上面的 Lua 脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList("stock:" + productId), "1");
if (result != null && result == 1L) {
// 扣减成功,发消息创建订单
kafkaTemplate.send("order-create", orderId);
} else {
throw new RuntimeException("库存不足");
}
3. 防缓存穿透 & 雪崩
- 空值缓存:对查不到的商品 ID,也缓存一个空值(TTL 短一点,比如 60s),避免反复查 DB。
- 随机过期时间:给缓存设置 TTL 时加个随机偏移(比如 300s ± 30s),防止同一时间大批 key 失效导致 DB 压力骤增。
4. 最终一致性:接受短暂不一致
订单创建是异步的,所以前端不能立刻返回“订单成功”,而是返回“排队中”,用户刷新后才能看到结果。产品一开始很抵触,说“用户体验不好”,但解释清楚“要么慢,要么崩”之后,也接受了。
生产环境踩坑实录
你以为改完就完了?Too young.
坑 1:Redis 连接池爆了
压测时发现 Redis 报 ERR max number of clients reached。原来 Spring Boot 默认的 Lettuce 连接池太小,高并发下不够用。
解决:调大连接池配置:
spring:
redis:
lettuce:
pool:
max-active: 200
max-wait: -1ms
max-idle: 50
min-idle: 10
坑 2:Kafka 消费积压
异步下单后,消费者处理太慢,消息堆积。查日志发现是每次消费都要查 DB、写 DB、调风控服务,太重了。
优化:
- 消费者批量处理(
max-poll-records: 50) - 本地缓存常用数据(比如用户等级、商品信息)
- 非关键逻辑(如发通知)再拆出去
坑 3:监控缺失
第一次上线没配好监控,出了问题只能靠日志 grep。后来赶紧上了 Prometheus + Grafana,关键指标包括:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
http_request_duration_seconds |
API 延迟 | P99 > 500ms |
redis_connected_clients |
Redis 连接数 | > 180 |
kafka_consumer_lag |
消息积压 | > 1000 |
效果如何?数据说话
优化前后对比(模拟 2000 并发):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 成功率 | 68% | 99.8% |
| 平均响应时间 | 2200ms | 85ms |
| DB CPU 使用率 | 95% | 35% |
| 超卖发生次数 | 12 次 | 0 次 |
双 11 预演当天,系统稳如老狗。产品终于没在群里 @ 我,反而发了个红包 🎉
心得体会:高并发不是玄学
回头看,高并发系统设计其实没那么神秘。核心就几点:
- 别让数据库当第一道防线
- 能缓存的绝不查库
- 能异步的绝不同步
- 接受最终一致性,别追求强一致(除非真需要)
最重要的是:别信“理论上可行”。一定要压测!一定要监控!一定要有回滚方案!
另外,感谢 Cursor 在我改第 17 版库存逻辑时,帮我自动生成了 Redis Lua 脚本的 Java 封装代码,省了我至少两小时 debug 时间。不然我现在可能还在加班……
给新手的建议
如果你也像我一样,刚接触高并发,别慌。记住:
- 先跑通,再优化:MVP 跑起来再说,别一上来就想搞百万 QPS。
- 善用工具:JMeter 压测、Arthas 线上诊断、SkyWalking 链路追踪,都是救命稻草。
- 多问老鸟:我们组老大一句话点醒我:“高并发的本质是控制流量,不是提升性能。”
- 保护自己:上线前写好回滚脚本,半夜报警才有底气睡觉。
最后,送一句我们团队的口头禅:“产品可以改需求,系统不能崩。”
共勉。

评论 0