一、缓存不是把查询结果塞进去

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 设计、过期策略、慢命令治理和故障演练。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐