高并发系统设计:从理论到实践
上周五晚上十一点半,我还在工位上和 Redis 打架——不是那种“优雅地调试缓存穿透”,而是实实在在的线上接口 502、QPS 瞬间干到十万、告警邮件刷屏到手机卡死。那会儿我一边敲着 :wq 保存 Vim 配置(对,我就是那个公司里唯一还在用 Vim 写 Java 的老古董),一边在心里默念:“再撑两个月就跳槽了,别崩,求你了。”
我是美团外卖干了快四年的后端,主要搞订单履约链路,天天和高并发打交道。说实话,这玩意儿听起来很酷,但真上手就是一堆“背锅现场”:大促流量突增、DB 连接池爆了、MQ 积压几千条、甚至有次因为某个服务没做限流,把整个集群 CPU 干到 98%,运维兄弟直接冲进我们办公室拍桌子。
最近之所以想写这篇文章,一是被去年双11的血泪教训逼的,二是我准备换个环境了(简历都悄悄投出去三轮了),得梳理下自己的技术栈;三是……我最近迷上了 Rust,发现 Go 在高并发场景里其实也挺香,顺便拿它做了个对比实验。所以今天不讲八股文,就聊聊真实业务里怎么把高并发系统从“能跑”变成“扛得住”。
别被“高并发”吓住,先搞清楚你要扛什么
很多人一提高并发就想到“百万 QPS”、“分布式锁”、“无状态服务”,但现实是——大部分系统根本不需要那么复杂。我们团队之前就有个新人,刚入职三天就给一个日活 500 的运营后台加上了 Kafka + Flink 实时计算,结果上线一周没人用,连测试都吐槽:“这功能是我妈要的吗?”
真正的问题往往出在业务模型没想清楚。比如我们有个“限时免配送费”的运营活动,逻辑很简单:用户下单时判断是否在活动时间窗口内,如果是,自动减免配送费。听起来 trivial?但去年双11那天,这个接口直接被打挂了——因为没考虑缓存击穿 + 数据库行锁竞争。
具体来说:
- 活动开始瞬间,几万用户同时请求;
- 所有请求都 miss Redis(因为 key 是动态生成的);
- 全部打到 MySQL,查同一张优惠配置表;
- InnoDB 行锁排队,DB 连接池耗尽;
- 接口超时,用户疯狂重试,雪崩。
这哪是什么高并发架构问题?这就是典型的“低级失误+运营活动没压测”。所以第一步:明确你的并发压力来源。是突发流量?热点数据?还是慢 SQL?
技术选型:Java 老将 vs Go 新秀
我在美团主要用 Java(Spring Boot + MyBatis),但最近研究 Rust 时顺手摸了下 Go,发现它在 I/O 密集型场景确实有优势。于是拿我们一个内部运营工具做了个 PoC:一个实时查询骑手在线状态的服务。
| 维度 | Java (Spring WebFlux) | Go (Gin) |
|---|---|---|
| 启动速度 | ~3s | ~0.2s |
| 内存占用 | ~400MB | ~20MB |
| 单核 QPS (简单 JSON 返回) | ~8k | ~25k |
| 开发体验 | 熟悉但啰嗦 | 简洁但泛型刚支持 |
| 团队熟悉度 | 高 | 低 |
结论?Go 很适合做轻量级网关、中间件、运营工具,但核心交易链路(比如订单创建)我还是信 Java 的生态和稳定性。不过如果你团队小、迭代快、又不想搞 JVM 调优那一套,Go 确实是个好选择。网上教程一堆,但别照搬——很多 demo 连连接池都没配。
比如下面这个 Go 示例,注意看数据库连接池配置:
// db.go
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func InitDB() *sql.DB {
dsn := "user:pass@tcp(db-host:3306)/order?parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
// 关键!别让连接数失控
db.SetMaxOpenConns(100) // 最大连接数,根据 DB 能力定
db.SetMaxIdleConns(20) // 空闲连接,避免频繁建连
db.SetConnMaxLifetime(30 * time.Minute) // 避免连接老化
return db
}
反观 Java,HikariCP 的配置也得调:
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 80
minimum-idle: 10
connection-timeout: 3000
idle-timeout: 30000
max-lifetime: 1800000
记住:连接池不是越大越好。我们曾经把 max pool size 设成 200,结果 DB 自己先 OOM 了。后来 DBA 拍着桌子说:“你们后端能不能学点数据库常识?”
架构设计:分层防御,别把鸡蛋放一个篮子
回到高并发的核心思路:分层削峰 + 快速失败 + 异步解耦。
以我们的“下单”链路为例,现在长这样:
用户请求 → API 网关(限流/鉴权)
→ 订单服务(本地缓存 + Redis 缓存)
→ MQ(异步落库 + 发通知)
→ 下游履约服务
关键点:
- 网关层限流:用 Sentinel 或自研组件,按用户 ID / IP 做令牌桶。别等到服务层才限流,那时候资源已经占用了。
- 缓存双保险:热点数据(比如区域配送费)放本地缓存(Caffeine)+ Redis。本地缓存 TTL 短一点(比如 1s),防 Redis 雪崩。
- 写操作异步化:订单创建成功后,只返回“受理成功”,后续扣库存、发短信、推消息全走 MQ。哪怕 MQ 挂了,也有重试机制兜底。
- 降级开关:运营活动页面加个“熔断开关”,一旦监控发现错误率 > 5%,自动关闭非核心功能(比如个性化推荐)。
有一次我们搞“神券节”,产品非要加个“实时显示剩余券量”的功能。我说这不就是个计数器吗?结果上线后发现,每领一张券就要 update 一次 DB,QPS 直接干到 5w。最后改成:前端展示的是缓存值(每秒更新一次),实际库存用 Redis + Lua 原子扣减,DB 只做最终一致性校准。产品还觉得“不够实时”,但没办法,系统不能崩啊。
运维视角:高并发不只是开发的事
作为一线开发,我越来越意识到:高并发系统的稳定性 = 代码 × 监控 × 预案。
我们团队现在强制要求:
- 所有接口必须有 SLA 标注(比如 P99 < 200ms)
- 上线前必须跑 全链路压测(用内部平台模拟双11流量)
- 关键服务必须配 自动扩缩容规则(基于 CPU + QPS)
最惨痛的一次教训:某次大促前,测试说“压测通过了”,结果上线后发现压测用的是内网环境,没算公网延迟。真实用户请求 RT 直接翻倍,触发 Hystrix 熔断,订单创建失败率飙升。那晚我和运维、测试、产品一起在会议室吃泡面到凌晨四点,产品一边擦眼泪一边说:“我以为点个按钮就行……”
所以现在我们搞了个“混沌工程小分队”,每周随机 kill 一个 pod、模拟网络延迟、甚至故意删 Redis key,看系统能不能自愈。虽然有点 paranoid,但至少双11没再出大事。
写在最后:别神话高并发,也别忽视它
高并发不是银弹,也不是玄学。它就是对资源争抢的精细化管理。你在教程里看到的“百万并发”,背后可能是几百台机器 + 几十个工程师 + 几年踩坑经验堆出来的。
对我个人而言,这四年最大的收获不是学会了多少框架,而是明白了:系统设计的本质是取舍。要性能?可能牺牲一致性。要稳定?可能增加复杂度。而运营需求?永远在变,但你的底线不能变——别让系统崩在你手里。
至于我?简历还在投,Rust 也在学,说不定下家公司就用 Go + Rust 搞新架构了。但在那之前,我得先把 Vim 的 .vimrc 配置同步到新电脑上——毕竟,一个不用 IDE 的 Java 程序员,总得有点倔强吧。
“所谓高并发,不过是无数个深夜加班的你,和一行行不肯崩的代码。”

评论 0