高并发系统设计:从理论到实践

Rebase迷路人
2025-12-17 11:33
阅读 1495

上周五晚上十一点半,我还在工位上和 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(异步落库 + 发通知) 
         → 下游履约服务

关键点:

  1. 网关层限流:用 Sentinel 或自研组件,按用户 ID / IP 做令牌桶。别等到服务层才限流,那时候资源已经占用了。
  2. 缓存双保险:热点数据(比如区域配送费)放本地缓存(Caffeine)+ Redis。本地缓存 TTL 短一点(比如 1s),防 Redis 雪崩。
  3. 写操作异步化:订单创建成功后,只返回“受理成功”,后续扣库存、发短信、推消息全走 MQ。哪怕 MQ 挂了,也有重试机制兜底。
  4. 降级开关:运营活动页面加个“熔断开关”,一旦监控发现错误率 > 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

最热最新
暂无评论
Rebase迷路人Lv.1
0
影响力
0
文章
0
粉丝