设计支撑 10 万 QPS 的读多写少 API¶
30 秒版(开场)¶
10 万 QPS 读多写少 API 的核心是 CDN/边缘缓存 + 有界多级缓存 + 水平扩展 + 可控回源。命中率、P99 和单 Pod QPS 都应是压测结果,不是通用常数;设计重点是即使缓存部分失效,也不会把 DB 直接打穿。
3 分钟版(精讲深度)¶
- 是什么:面向 C 端或 B 端的高频查询接口(商品详情、配置、Feed),读写比通常 > 100:1,峰值 QPS 10 万+。
- 为什么:数据库能力取决于查询、数据集、硬件和连接模型,不能背固定 QPS;当入口规模远高于可验证的回源能力时,必须分层缓存、限流和隔离。
- 怎么做:接入层 CDN/边缘缓存 → 网关限流 → 应用本地缓存 + Redis 集群 → 只读副本/分库分表 → 写走 MQ 异步刷新缓存。
10 分钟版(原理 + 图示)¶
容量估算(必讲)
| 维度 | 估算 |
|---|---|
| 总 QPS | 100,000 |
| 单 Pod 能力 | 5,000 QPS(8C16G,P99<50ms) |
| 应用实例 | 100,000 / 5,000 × 1.5(冗余)≈ 30 Pod |
| 缓存命中率 | 95% → DB 实际 5,000 QPS |
| Redis 集群 | 3 主 3 从,单分片 3~5 万 QPS 足够 |
| 带宽 | 响应 2KB × 100k ≈ 1.6 Gbps(需 CDN 分担 80%+) |
flowchart TB
Client[客户端] --> CDN[CDN / 边缘缓存]
CDN -->|未命中| LB[负载均衡]
LB --> App[Go 服务 × N]
App --> L1[本地 LRU Cache]
L1 -->|miss| Redis[(Redis Cluster)]
Redis -->|miss| RO[(MySQL 只读副本)]
App -->|写| MQ[MQ] --> Writer[写服务] --> Master[(MySQL 主库)]
Writer -->|失效/更新| Redis
关键设计点
- Cache-Aside:读 miss 回源 DB 并回填;写通常先提交 DB,再失效缓存,并用 TTL/CDC/重试兜底。所谓“延迟双删”仍有竞态窗口,不是强一致协议。
- 热点 Key:本地缓存、只读副本、请求合并与复制热点值;给同一逻辑 Key 随意加前缀并不会自动消除热点,还会引入多副本一致性问题。
- 连接池:每 Pod DB 连接 ≤ 50,总连接 30×50=1500,需 PgBouncer/ProxySQL 或只读副本分流。
- Go 侧:
sync.Pool复用 buffer;singleflight防击穿;GOMAXPROCS对齐 CPU limit。
生产场景¶
- 电商商品详情:大促峰值 10 万+ QPS,CDN 缓存静态字段,动态库存单独接口。
- 配置中心/字典:变更频率低,TTL 长 + 推送失效。
- 可观测指标:QPS、缓存命中率、回源率、P50/P99、Redis 慢查询、DB 连接池使用率。
排查与工具¶
| 工具 | 用途 |
|---|---|
| Prometheus + Grafana | QPS、延迟、命中率 |
Redis INFO stats |
keyspace hits/misses |
pprof / trace |
热点 handler、GC 压力 |
| 慢查询日志 | 回源 SQL 优化 |
路径:P99 飙升 → 看命中率是否下降 → Redis/DB 慢 → 热点 Key 或连接池耗尽 → singleflight / 扩容 / 加本地缓存。
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| CDN + Redis | 读多写少、可容忍秒级延迟 | 强一致读(库存扣减) |
| 本地缓存 | 超热点、微秒级 | 多实例一致性要求高 |
| 分库分表 | 单表 > 5000 万行 | 数据量小、JOIN 多 |
| 全量内存 | 数据 < 100GB | 成本敏感、持久化要求高 |
深挖问答¶
- 如何保证缓存与 DB 一致? → Cache-Aside + TTL/CDC/重试只能做到可控的最终一致;要求线性一致的读应绕过缓存、读权威存储,或把读写纳入同一个一致性系统。
- 10 万 QPS 单机 Redis 够吗? → 集群模式水平扩展;热点 Key 需本地缓存或拆分。
- CDN 缓存什么? → 静态/半静态 JSON;带用户态的走
Cache-Control: private或 bypass。 - 如何压测验证? → 阶梯加压到 120% 峰值,观察 P99 与错误率,留 30% 冗余。
- Go 服务如何水平扩展? → 无状态 + K8s HPA 按 CPU/QPS 扩缩;会话放 Redis。
反模式与事故¶
- 缓存不设 TTL,DB 变更后长期脏读。
- 所有请求打主库「保证最新」,10 万 QPS 直接打穿。
- 本地缓存无上限,OOM 或 GC 毛刺。
- 大促前未压测,连接池默认值(如 10)导致排队超时。
代码示例¶
// Cache-Aside + singleflight 防击穿
type ProductCache struct {
rdb *redis.Client
db ProductRepo
sf singleflight.Group
}
func (c *ProductCache) Get(ctx context.Context, id int64) (*Product, error) {
key := fmt.Sprintf("product:%d", id)
if b, err := c.rdb.Get(ctx, key).Bytes(); err == nil {
var p Product
_ = json.Unmarshal(b, &p)
return &p, nil
}
v, err, _ := c.sf.Do(key, func() (any, error) {
p, err := c.db.FindByID(ctx, id)
if err != nil {
return nil, err
}
b, _ := json.Marshal(p)
c.rdb.Set(ctx, key, b, 5*time.Minute)
return p, nil
})
if err != nil {
return nil, err
}
return v.(*Product), nil
}