mysql本身无二级缓存,需应用层结合redis实现:一级缓存(如ristretto)负责进程内高速读取,二级缓存(redis)负责跨实例共享、过期控制与原子操作,二者职责分离,协同提升性能与一致性。

redis 和 mysql 组合是最常见、最可控的二级缓存落地方式。它不依赖复杂中间件,能明确控制每一层的过期、更新、穿透逻辑,适合绝大多数业务场景。
为什么一级用内存、二级用 redis 是合理选择
一级缓存(如 ristretto 或 bigcache)快但不持久、不共享;二级缓存(redis)稍慢但可跨进程/实例共享,且自带过期、原子操作和发布订阅能力。两者不是“谁替代谁”,而是职责分离:
-
ristretto.Get(key)命中 → 直接返回,毫秒级 - 未命中 → 查
redis.Get(ctx, key),若存在则写回一级并返回 - 两级都未命中 → 查
mysql,写入redis.Set(ctx, key, val, 60*time.Second),再写回一级
注意:不要把一级缓存设成无过期时间的全量镜像,否则 redis 过期后数据不一致风险极高。
go-redis/v8 与 ristretto 的初始化要点
两个客户端必须独立管理生命周期,且 ristretto 的 MaxCost 要按实际 value 大小估算(不是条目数),否则会频繁驱逐:
ristretto.NewCache(&ristretto.Config{NumCounters: 1e7, MaxCost: 1 —— <code>MaxCost单位是字节,1-
redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "", DB: 0})—— 必须显式调用client.Ping(ctx).Err()验证连接,否则首次Get失败时 panic 不易定位 - 避免在
main()外全局初始化ristretto.Cache,它不是线程安全的单例,应作为依赖注入或包级变量统一管理
缓存穿透与空值缓存的硬编码处理
redis.Nil 错误必须显式判断,否则 rdb.Get(ctx, key).Result() 会返回 "" 和 redis.Nil,直接转 string 可能掩盖空结果:
- 查
redis得到redis.Nil→ 说明数据库也无此 key,应写入空值缓存(如"null")并设较短过期(如5*time.Second),防止反复穿透 - 不能用
nil作空值标记,因为ristretto.Set(key, nil, cost)会 panic;建议统一用json.RawMessage([]byte("null"))或自定义空结构体 - 所有
Set操作必须带duration,哪怕只是 1 秒——避免因程序异常退出导致一级缓存残留脏数据
更新时如何避免双写不一致
写数据库成功但写 redis 失败,会导致后续读取拿到旧缓存。没有银弹,但可降低风险:
- 优先用「先删缓存,再写库」:删除
redis和一级缓存后,再db.Exec;失败则缓存已清,下次读自动回源,无脏数据 - 禁止「先写库,再删缓存」:中间窗口期可能有并发读命中旧缓存
- 删除操作本身也要容错:若
rdb.Del(ctx, key).Err()失败,记录日志但不中断业务流程;可异步重试或靠 TTL 自愈 - 对强一致性要求高的 key(如账户余额),跳过二级缓存,直连数据库
真正难的不是代码怎么写,而是哪些 key 允许不一致、能容忍多久不一致——这得和产品一起定 SLA,而不是在 Set 里硬加重试逻辑。











