高并发系统设计:从理论到实践
凌晨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. 异步化:让非核心路径飞一会儿
用户点击“立即抢购”后,其实只有两件事是实时的:
- 检查库存是否足够(Redis)
- 创建订单草稿(防止重复提交)
其他所有事情——生成支付链接、发短信、写日志、更新用户积分——统统扔进 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 重大故障。
但过程中的坑,值得记录:
- 不要迷信“高性能框架”:Go 本身很快,但如果你在循环里查 DB,再快也救不了你。
- 监控必须前置:我们加了 trace(Jaeger)、metrics(Prometheus)、logs(Loki)三位一体,否则出问题只能靠猜。
- 预案比优化更重要:我们准备了降级开关——如果 Redis 挂了,直接返回“活动火爆,请稍后再试”,避免雪崩。
- 团队沟通成本常被低估:和前端约定好幂等 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