高并发系统不是堆机器就能搞定的

马超
2026-01-05 12:03
阅读 1698

上个月我正式从字节裸辞了。说“裸辞”其实有点夸张,毕竟攒了两年年假换来的缓冲期,再加上北京这鬼天气——每天早上六点爬起来挤地铁一小时去西二旗,到工位时脑子已经死机一半。辞职前那阵子,我负责的商品秒杀服务刚撑过618大促,QPS峰值冲到12万,数据库差点原地升天。那天晚上我盯着 Grafana 面板看 Redis 命中率掉到70%以下,心里只有一个念头:老子不干了。

现在在家躺平快一个月,一边刷 LeetCode 保持手感(别笑,算法真没那么玄乎,但面试官就爱问),一边琢磨 AI 方向——最近在用 Go 写个小模型推理服务练手。但说到底,我还是个后端老狗,看到高并发三个字就条件反射般心跳加速。今天这篇,就想聊聊那些年我们在大厂里踩过的坑、熬过的夜、背过的锅。

为什么高并发系统总在半夜报警?

很多人以为高并发就是加机器、堆缓存、上 CDN。但真相是:架构设计错了,你加十台服务器也救不了

我在字节带的一个新人曾问我:“哥,咱们系统为啥每次大促都要提前两周压测?不能等用户来了再扩容吗?” 我当时苦笑:“等用户来了,DB 已经挂了,老板在群里@你的时候,你连 SSH 都连不上。”

高并发的核心,从来不是“扛”,而是“疏导”和“隔离”。就像北京早高峰的地铁,你不能指望所有人都挤进同一节车厢——得靠限流、排队、分流机制来维持秩序。

技术选型:Go 还是 Java?别吵了,看场景

先说结论:如果你团队熟悉 Go,且业务对延迟敏感,Go 是更好的选择

我在字节的秒杀项目里,最初用的是 Java + Spring Boot。不是不好,但 GC 停顿在高并发下太致命。有一次压测,JVM Full GC 卡了 800ms,整个链路雪崩。后来我们把核心下单链路重写成 Go,用 goroutine + channel 实现异步削峰,延迟稳定在 15ms 以内。

下面是我整理的一个简单对比表:

维度 Go Java (Spring Boot)
启动速度 毫秒级,适合 Serverless 秒级,冷启动慢
内存占用 极低(几 MB 起) 较高(通常 512MB+)
并发模型 CSP(goroutine + channel) 线程池 + 锁
生态成熟度 中(Web 框架够用) 高(全栈生态完善)
团队学习成本 低(语法简单) 中高(泛型、注解、AOP)
GC 表现 STW 极短(<1ms) 可能出现百毫秒级停顿

当然,如果你公司已经有成熟的 Java 微服务体系,强行切 Go 也是作死。技术选型不是比谁更“潮”,而是看团队能力 + 业务特性。我们当时敢切 Go,是因为组里三个后端都玩过 Kubernetes 和 etcd,对并发模型有共识。

关键组件怎么搭?别只盯着数据库

很多人一谈高并发就喊“上 Redis!上 Kafka!”。但 Redis 用错一样炸。

举个真实案例:我们早期把商品库存存在 Redis 的 INCR 里,结果某次活动超卖了——因为 INCR 不是原子的库存扣减逻辑。正确做法是用 Lua 脚本保证原子性:

-- stock.lua
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
    return redis.call('DECR', KEYS[1])
else
    return -1
end

在 Go 里调用也很简单:

func DeductStock(client *redis.Client, skuID string) (bool, error) {
    script := redis.NewScript(`
        local stock = redis.call('GET', KEYS[1])
        if tonumber(stock) > 0 then
            return redis.call('DECR', KEYS[1])
        else
            return -1
        end
    `)
    result, err := script.Run(client.Context(), client, []string{"stock:" + skuID}).Result()
    if err != nil {
        return false, err
    }
    return result.(int64) >= 0, nil
}

除了缓存,消息队列也是削峰填谷的利器。我们用 Kafka 把下单请求异步化,前端返回“排队中”,后端慢慢消费。但这里有个坑:消费者不能无脑 ack。我们曾因为 consumer 处理失败却提前 ack,导致数据丢失。后来改成了手动提交 offset,并加入重试 + 死信队列机制。

数据库:别让 MySQL 成为你的瓶颈

高并发下,数据库永远是最脆弱的一环。我们的经验是:

  1. 读写分离:主库写,多个从库读。但要注意主从延迟——秒杀场景下,你不能让用户查到“已抢光”却还能下单。
  2. 分库分表:用 user_id % 16 分 16 个库,每个库再分 64 张表。ShardingSphere 能搞定,但跨分片查询要避免。
  3. 热点更新:比如热门商品库存,单行更新会锁表。解决方案:
    • 库存预热:活动前把库存拆成 100 份,分散到不同行
    • 最终一致性:先扣缓存库存,异步同步 DB

有一回双11,我们某个 SKU 的库存行被锁了 3 秒,整个下单链路堆积如山。运维在群里吼:“DB CPU 100%!谁写的 SQL?!” 我默默关掉了终端——那条 SQL 是我三个月前写的,当时觉得“反正就一个商品,不会有问题”。

接口设计:少即是多

高并发接口要遵循“最小权限原则”:

  • 返回字段越少越好(别一股脑 SELECT *
  • 参数校验前置(避免无效请求打到 DB)
  • 状态码明确(比如库存不足返回 429 Too Many Requests,而不是 200 {code: -1}

我们曾有个接口返回 50 个字段,其中 40 个是冗余的。优化后,响应体从 8KB 降到 1.2KB,带宽省了 85%,而且前端渲染快了一倍。

另外,幂等性必须考虑。用户手抖点两次“立即购买”,不能创建两个订单。我们的做法是在 Redis 里放一个 order:uid:request_id 的 token,5 分钟内重复请求直接拒绝。

运维视角:监控比代码更重要

再牛的架构,没有监控等于裸奔。我们在生产环境至少要盯住这几项:

  • P99 延迟:别只看平均值,长尾请求会拖垮整个系统
  • 错误率:HTTP 5xx / 业务异常码
  • 资源水位:CPU、内存、连接数、磁盘 IO
  • 缓存指标:命中率、淘汰率、大 Key

有一次,Redis 突然命中率暴跌,排查发现是某个新上线的服务在疯狂查无效 key,导致缓存穿透。我们立刻加上布隆过滤器,并在 Nginx 层做了空值缓存(proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m;)。

最后一点真心话

高并发系统不是一天建成的。它是一堆妥协、权衡、深夜加班和线上事故堆出来的。我在大厂那几年,最大的收获不是技术,而是敬畏心——敬畏流量,敬畏数据,敬畏每一个可能出错的环节。

现在休息在家,反而更能看清:技术只是工具,解决问题才是目的。Go 很香,但如果你团队只会 Python,硬切 Go 反而增加风险;算法很重要,但在工程落地中,往往一个合理的缓存策略比最优 O(1) 算法更有效。

如果你也在纠结职业方向,不妨问问自己:你是享受解决复杂问题的过程,还是只想追新框架? 前者能走得远,后者容易 burnout。

对了,我最近在用 Go 写一个轻量级的 AI 推理服务,打算开源。如果你对高并发 + AI 结合感兴趣,欢迎留言交流——反正我现在有的是时间回邮件(笑)。


本文写于北京家中,窗外正下着今年第一场雪。MacBook Pro 风扇安静,不用再担心凌晨三点被 PagerDuty 叫醒。愿所有后端工程师,都能睡个好觉。

评论 0

最热最新
暂无评论
马超Lv.1
0
影响力
0
文章
0
粉丝