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

码农Tech
2025-12-17 23:08
阅读 2028

凌晨2:17,VSCode 的 tab 栏还亮着 14 个文件,咖啡已经凉了第三杯。刚入职这家新公司两个月,就赶上“618大促”预热阶段,产品经理画的大饼还没消化完,后端就得先把高并发这头猛兽驯服了。

我是服务端开发,平时写 Go 写到手抽筋,K8s 配置文件比情书还熟。今天这篇文章不是为了炫技,纯粹是被线上流量打脸之后的血泪总结——高并发系统设计,光看论文真不行,得在实战里摸爬滚打


被流量“教育”的一天

事情得从上周五说起。

那天下午 5 点,产品 PM 小王突然在群里@我:“兄弟,咱们那个秒杀活动,预计峰值 QPS 要冲到 8万+,能不能扛住?”

我当时正调试一个诡异的 Redis 连接池泄漏问题(后来发现是某位同事用了 defer conn.Close() 却没考虑复用),随口回了句:“理论上没问题。”

结果当天晚上 9 点一开抢,监控告警像过年放炮一样炸了:

[ALERT] CPU usage > 95% on pod-xxx
[ALERT] Redis latency spiked to 300ms+
[ERROR] MySQL Deadlock detected in order_service

数据库直接锁表,API 响应时间从 50ms 飙到 2s+,用户疯狂刷新,雪崩效应拉满。运维老哥在 Slack 里发了个“?”表情包,测试妹子默默发来一张“你行你上”的 meme 图。

那一刻,我真的想砸电脑——但转念一想,电脑是公司的,砸了还得赔。

于是,通宵重构开始了。


为什么“理论”不够用?

在学校或者面经里,我们总听到这些词:缓存、限流、异步、分库分表、读写分离……听起来很美,但真到了生产环境,你会发现:

  • 缓存穿透?实际是某个运营配置错了商品ID,全查空。
  • 限流漏桶 vs 令牌桶?选错了算法,用户投诉“点一下就失败”。
  • 异步队列?消息堆积了没人消费,订单状态卡死。

高并发不是单点优化,而是一套综合工程体系。你得同时考虑架构、代码、中间件、部署、监控,甚至团队协作流程。

我们这次的目标很明确:支撑 10w QPS 的秒杀请求,核心接口 P99 < 100ms,系统可用性 99.95%+


架构拆解:Go + 云原生怎么玩

1. 请求入口层:别让流量一股脑冲进来

第一道防线必须是流量整形

我们用 Nginx 做了简单的 IP + 用户 ID 维度限流,但这远远不够。真正的杀手锏是在 Go 服务里集成 golang.org/x/time/rate 实现的令牌桶限流器。

type RateLimiter struct {
    limiters sync.Map // key: userID, value: *rate.Limiter
}

func (rl *RateLimiter) Allow(userID string) bool {
    limiter, _ := rl.limiters.LoadOrStore(userID, rate.NewLimiter(rate.Every(100*time.Millisecond), 5))
    return limiter.(*rate.Limiter).Allow()
}

这里有个坑:不能为每个用户都新建 limiter,否则内存爆炸。我们限制了最多缓存 10w 个活跃用户,超了就 LRU 淘汰。

另外,前端配合做了按钮防抖 + 提交后 loading 状态,减少无效请求。产品经理终于理解了“用户体验不只是 UI”。

2. 缓存策略:Redis 不是万能胶水

早期我们把商品库存直接存在 MySQL 里,每次扣减都要 UPDATE stock SET count = count - 1 WHERE id = ? AND count > 0,结果高并发下死锁频发。

现在改用 Redis + Lua 脚本原子操作

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

Go 调用:

res, err := redisClient.Eval(ctx, luaScript, []string{"stock:" + skuID}).Result()
if err == nil && res.(int64) == 1 {
    // 扣减成功,发消息到 MQ
}

但缓存也有风险:缓存击穿

我们给热门商品加了互斥锁(Redis SETNX),冷门商品直接用空值缓存 + 短 TTL 防穿透。

吐槽一句:运维说 Redis 集群内存快满了,让我“省着点用”。我回他:“那你给我加机器啊”,他回我:“预算审批要两周。” 我:???

3. 异步化:让非核心路径飞一会儿

用户点击“立即抢购”后,其实只有两件事是实时的:

  1. 检查库存是否足够(Redis)
  2. 创建订单草稿(防止重复提交)

其他所有事情——生成支付链接、发短信、写日志、更新用户积分——统统扔进 Kafka。

我们用 Go 写了一个轻量级消费者池:

func StartConsumers(topic string, handler func(msg []byte)) {
    for i := 0; i < 10; i++ { // 10 个并发消费者
        go func() {
            for msg := range kafkaReader.Messages {
                handler(msg.Value)
            }
        }()
    }
}

好处很明显:主链路响应时间从 300ms 降到 60ms。坏处是——消息积压时排查困难。后来我们加了 Prometheus 监控 lag,并设置了自动扩容策略(K8s HPA 基于 consumer lag)。

4. 数据库:别再让 MySQL 当独苗

MySQL 依然是我们的最终一致性保障,但绝不让它直面高并发。

  • 读写分离:读走从库,写走主库(用 GORM 的 DB Resolver)
  • 分库分表:订单表按 user_id hash 分 16 库,每库 64 表(用 ShardingSphere 中间件)
  • 热点数据隔离:秒杀订单单独建表,避免影响普通订单查询

最骚的操作是:预热库存

活动开始前 10 分钟,我们把库存从 DB 加载到 Redis,并设置初始值。这样即使 DB 挂了,也能撑一段时间(当然,这只是兜底)。


云原生加持:K8s 不只是部署工具

作为刚入职的新员工,我很庆幸这家公司全面上云,用的是 K8s + Helm + ArgoCD。

我们给秒杀服务做了精细化资源配置:

资源类型 请求值 限制值 说明
CPU 500m 1000m 避免突发占用过多
Memory 512Mi 1Gi Go GC 对内存敏感
Replicas HPA 自动扩缩 min=10, max=100 基于 CPU 和 QPS

HPA 配置示例:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: seckill-service
  minReplicas: 10
  maxReplicas: 100
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"

上线前我们用 k6 做了全链路压测:

k6 run -e SKU_ID=12345 --vus 10000 --duration 5m script.js

结果发现:Go 的 GC 在高并发下会 STW(Stop-The-World),虽然只有几毫秒,但 P99 会被拉高。

解决方案:

  • 升级到 Go 1.22(GC 更平滑)
  • 减少堆分配:用 sync.Pool 复用对象
  • 避免在 hot path 里频繁创建 slice/map

教训与心得

经过三轮压测 + 两次灰度发布,618 预热活动终于稳住了。峰值 QPS 达到 9.2w,P99 响应时间 87ms,0 重大故障。

但过程中的坑,值得记录:

  1. 不要迷信“高性能框架”:Go 本身很快,但如果你在循环里查 DB,再快也救不了你。
  2. 监控必须前置:我们加了 trace(Jaeger)、metrics(Prometheus)、logs(Loki)三位一体,否则出问题只能靠猜。
  3. 预案比优化更重要:我们准备了降级开关——如果 Redis 挂了,直接返回“活动火爆,请稍后再试”,避免雪崩。
  4. 团队沟通成本常被低估:和前端约定好幂等 token,和测试对齐压测场景,和运维确认资源配额……这些比写代码花的时间还多。

最后:加班的意义是什么?

写完这篇稿子,窗外天已经蒙蒙亮。工位对面的同事还在 debug 一个奇怪的 TCP TIME_WAIT 问题(他说是 client 端端口耗尽,我觉得是他连接池没关)。

有人问我:“天天加班,图啥?”

我说:“图系统别崩,图用户能抢到货,图自己简历上能写‘主导高并发系统设计’。”

其实吧,高并发没有银弹,只有不断权衡:一致性 vs 可用性、性能 vs 成本、速度 vs 稳定性。

但当你看到监控大盘上那条平稳的曲线,看到用户评论“这次秒杀好流畅”,那一刻,所有的熬夜、debug、会议扯皮,都值了。

对了,产品经理刚才又在群里@我:“下个月双11,我们要支持 20w QPS……”

我默默关掉了 VSCode,打开了招聘软件。

(开玩笑的,我已经爱上这种痛并快乐着的感觉了。)


附:关键组件版本参考

组件 版本 说明
Go 1.22.3 启用新调度器和 GC
Redis 7.2 开启 lazy-freeing
Kafka 3.5 副本数=3,ISR=2
K8s 1.28 CNI 用 Calico
MySQL 8.0 InnoDB,buffer pool 32G

共勉。

评论 0

最热最新
暂无评论
码农TechLv.1
0
影响力
0
文章
0
粉丝