Redis 缓存设计的三条底线:命中率、穿透保护和可恢复性
一、缓存不是把查询结果塞进去
Redis 在国内后端项目里几乎是标配,但缓存事故也集中:缓存击穿打满数据库,热 key 打爆单核,大 key 阻塞主线程,过期策略随意导致旧数据。缓存设计的核心不是“加一层 Redis”,而是明确一致性、失效方式和降级路径。
常见缓存对象可分三类:低频变更的配置数据、高频读取的详情页数据、需要快速计数或排序的临时数据。不同对象不能用同一套 TTL。配置数据可长 TTL 加主动刷新;商品详情适合短 TTL 加随机抖动;排行榜、计数器要考虑落库和重放。
| 场景 | 推荐策略 | 风险点 |
|---|---|---|
| 商品详情 | Cache Aside + TTL 抖动 | 更新后短时间旧值 |
| 计数器 | Redis 增量 + 异步落库 | 失败重试要幂等 |
| 热门榜 | ZSET + 定时重算 | 大 key 和全量读取 |
二、Cache Aside 要写完整
最常用的是 Cache Aside:读时先查缓存,未命中再查数据库并回填;写时先更新数据库,再删除缓存。这里容易犯两个错误:把删除缓存写成更新缓存,或删除失败不处理,导致旧值长期存在。
下面是 Go 写法示例,重点是空值缓存、TTL 抖动和删除失败记录。示例省略了数据库实现,但保留了关键分支。
func GetProduct(ctx context.Context, id int64) (*Product, error) {
key := fmt.Sprintf("product:%d", id)
val, err := rdb.Get(ctx, key).Result()
if err == nil {
if val == "__nil__" { return nil, sql.ErrNoRows }
var p Product
return &p, json.Unmarshal([]byte(val), &p)
}
if err != redis.Nil { log.Printf("redis get failed: %v", err) }
p, err := repo.FindProduct(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
_ = rdb.Set(ctx, key, "__nil__", time.Minute).Err()
return nil, err
}
if err != nil { return nil, err }
bs, _ := json.Marshal(p)
ttl := 10*time.Minute + time.Duration(rand.Intn(120))*time.Second
_ = rdb.Set(ctx, key, bs, ttl).Err()
return p, nil
}
func UpdateProduct(ctx context.Context, p *Product) error {
if err := repo.UpdateProduct(ctx, p); err != nil { return err }
key := fmt.Sprintf("product:%d", p.ID)
return rdb.Del(ctx, key).Err()
}
空值缓存能缓解穿透,但 TTL 必须短,否则新数据可能被旧空值挡住。TTL 抖动能避免同批 key 同时过期。写后删缓存仍有短暂不一致窗口,核心链路要结合版本号、消息补偿或直接绕过缓存。
三、热 key 和大 key 要提前治理
Redis 单个命令很快,但主线程模型决定了大 key 和慢命令会影响其他请求。几 MB 的 String、几万个元素的 Hash、一次 KEYS *,都可能让延迟突然上升。生产业务禁止使用 KEYS,排查也应使用 SCAN。
# 查慢日志
redis-cli slowlog get 10
# 观察命令统计
redis-cli info commandstats
redis-cli --scan --pattern 'product:*' | head
热 key 通常用本地缓存、读副本、拆 key 处理。首页配置可放进进程内缓存,设置 5 到 30 秒 TTL;计数类可按用户或时间片分桶再汇总。不要把所有压力都交给 Redis Cluster,集群不能消除单个热 key 的集中访问。
四、故障恢复要能演练
Redis 宕机时,最怕业务把所有请求原样打到数据库。连接池要设置超时,缓存访问要有熔断,关键接口要有降级值。对缓存来说,恢复也不是简单重启实例:如果大量 key 同时失效,恢复后会出现冷缓存,数据库仍然可能被打满。
redis:
addr: redis.internal:6379
pool_size: 100
min_idle_conns: 10
dial_timeout: 200ms
read_timeout: 100ms
write_timeout: 100ms
max_retries: 1
实践建议:先列出业务里最重要的 20 个缓存 key,逐个确认 TTL、最大体积、回源成本、删除策略和 Redis 不可用时的行为。命中率只是结果指标,真正要长期维护的是 key 设计、过期策略、慢命令治理和故障演练。
更多推荐



所有评论(0)