高并发不是玄学,是凌晨三点的Go代码和产品需求改了又改
我是个典型的杭州码农,每天八点准时坐到工位——不是因为我多自律,而是阿里园区附近的早高峰堵得你根本不敢晚起。在网易待过两年,现在在一家做实时数据平台的创业公司混日子,主业是搞分布式系统,副业是帮产品擦屁股。
上周五晚上十一点半,我还在盯着 Grafana 上一条陡峭的 CPU 曲线发呆。产品刚上线一个“实时排行榜”功能,说好了只给内部运营用,结果市场部偷偷把它嵌进了 App 首页。用户量从 50 QPS 瞬间飙到 8000+,我们的 Go 服务直接被打成 P99 延迟 2.3 秒,数据库连接池爆满,Redis 缓存击穿……当时我真的想把产品经理的 MacBook Pro 扔进西湖。
但骂归骂,活还得干。这篇笔记就是我在被 deadline 追着跑、靠 Cursor 自动补全续命的情况下,对高并发系统设计的一些实战思考。不讲教科书理论,只聊真正在生产环境里跑通的方案。
别再迷信“加机器就能扛住”
很多初级工程师(包括曾经的我)一听到高并发,第一反应就是:“上集群!加缓存!搞分库分表!”
但现实是:架构不是堆料,是权衡。
我们最初的设计其实挺“教科书”:Go 写的 HTTP 服务 + MySQL 主从 + Redis 缓存。看起来没问题,直到流量真的打进来。问题出在几个地方:
- 缓存穿透:排行榜请求带动态参数(比如
?region=hangzhou&category=gaming),组合爆炸,缓存命中率不到 30%。 - 数据库慢查:
ORDER BY score DESC LIMIT 100在百万级用户表上直接拖垮主库。 - Go 并发模型滥用:每个请求都开 goroutine 去查 DB + Redis,结果协程数飙到 10w+,调度开销比业务逻辑还重。
这时候 Cursor 救了我一命——我对着它狂敲:“帮我优化这个排行榜查询,要求低延迟、高吞吐,用 Go 实现”。它居然真吐出了一套混合方案。
我们最后用了什么?
1. 预计算 + 分层缓存
与其每次现场算排行榜,不如提前算好。我们引入了一个离线计算层:
- 每 10 秒跑一次 Spark Job,按 region + category 聚合 top 1000 用户
- 结果写入 Redis Sorted Set(ZSET)
- 热门榜单(如全国总榜)再加一层本地缓存(用
golang-lru)
// 伪代码:读取排行榜
func GetLeaderboard(region, category string) ([]UserScore, error) {
key := fmt.Sprintf("lb:%s:%s", region, category)
// 先查本地缓存(1ms 内)
if cached, ok := localCache.Get(key); ok {
return cached.([]UserScore), nil
}
// 再查 Redis ZSET(2-5ms)
scores, err := redis.ZRevRangeWithScores(ctx, key, 0, 99).Result()
if err != nil {
// 降级:返回空榜 or 上次快照
return fallbackLeaderboard(key), nil
}
// 回填本地缓存,TTL 500ms(避免雪崩)
localCache.Add(key, convert(scores), time.Millisecond*500)
return convert(scores), nil
}
注:本地缓存 TTL 必须短于 Redis 的更新周期,否则会展示过期数据。
2. 连接池 & 限流必须配对出现
之前我们只设了 MySQL 连接池 max=100,但没做入口限流。结果一有热点,所有请求涌进来抢连接,最后谁都拿不到。
现在我们在 API Gateway 层做了令牌桶限流(每秒 5000 请求),Go 服务内部也加了 uber-go/ratelimit:
// 每个 region-category 组合独立限流
var limiters = sync.Map{}
func getLimiter(key string) ratelimit.Limiter {
if limiter, ok := limiters.Load(key); ok {
return limiter.(ratelimit.Limiter)
}
newLimiter := ratelimit.New(100) // 100 QPS per key
limiters.Store(key, newLimiter)
return newLimiter
}
func handler(w http.ResponseWriter, r *http.Request) {
key := buildKey(r)
if !getLimiter(key).Take() {
http.Error(w, "Too Many Requests", 429)
return
}
// ...继续处理
}
3. 异步化非核心路径
产品非要加个“用户上榜通知”,还要发站内信+推送。这玩意儿根本不影响主流程,但同步调用第三方服务动不动就 500ms+。
果断拆成消息队列:
graph LR
A[排行榜更新] --> B{是否新上榜?}
B -- 是 --> C[发Kafka消息]
C --> D[通知服务消费]
D --> E[发推送/站内信]
Go 里用 sarama 异步生产,失败就丢(反正不是核心功能),主链路再也不卡了。
性能对比:改完之后有多爽?
| 指标 | 改造前 | 改造后 |
|---|---|---|
| P99 延迟 | 2300 ms | 42 ms |
| Redis 命中率 | 28% | 96% |
| MySQL QPS | 12,000 | 800 |
| 服务 CPU 使用率 | 95% | 35% |
| 错误率(5xx) | 12% | 0.03% |
最爽的是,上周双11压测,我们扛住了单机 15,000 QPS,而机器配置还是那台 4C8G 的老古董。
关于“产品”和“综合能力”的一点牢骚
说实话,技术方案再牛,也架不住产品天天改需求。
这次排行榜原本只要 Top 100,后来临时加“按城市细分”,再后来要“实时刷新+历史回溯”……每次改,架构就得跟着动。
但这也逼我意识到:高并发系统设计从来不只是技术问题,更是产品、运维、测试的综合博弈。
- 和产品沟通时,得用他们听得懂的话:“如果不限流,上线就崩,你背锅还是我背锅?”
- 和运维配合,把关键指标(缓存命中率、DB 负载)做成 Dashboard,让他们能主动预警
- 测试同学帮忙写了混沌工程脚本,模拟 Redis 宕机、网络延迟,提前暴露降级逻辑 bug
这些“软技能”,有时候比你会不会写 lock-free queue 更重要。
最后:别怕用 AI,但别当甩手掌柜
我承认,现在很多代码是 Cursor 帮我写的。但它只是工具——真正决定系统成败的,是你对业务场景的理解和权衡取舍的勇气。
比如它建议我用 Redis Stream 做排行榜更新通知,但我坚持用了 Kafka,因为团队已经有成熟的监控和重试机制;它生成的限流代码没考虑 key 动态增长导致内存泄漏,是我手动加了 LRU 淘汰。
AI 能加速你,但不能代替你思考。
现在已经是凌晨两点,窗外杭州的雨还没停。产品刚在群里@我:“明天能不能支持按小时粒度看排行榜?”
我深吸一口气,打开 VS Code,光标闪了三下,Cursor 自动补全了 type HourlyLeaderboard struct { ... }。
算了,先睡吧。毕竟,明天八点还得早起干活。

评论 0