Redis 缓存策略与数据一致性

缓存能减少重复读取数据库的开销,但引入了数据副本,也带来了过期、并发和一致性问题。缓存策略需要和业务允许的陈旧时间、流量模式及故障处理方式一起设计。

1. Cache-Aside 读流程

最常见的旁路缓存流程是:

  1. 先读 Redis。
  2. 命中则返回缓存值。
  3. 未命中则读数据库。
  4. 把数据库结果写回 Redis,并设置过期时间。

伪代码:

1
2
3
4
5
6
7
value = redis.get(key)
if value exists:
return value
value = database.query(id)
if value exists:
redis.set(key, value, ttl)
return value

缓存键要包含业务对象和必要的隔离维度,例如 shop:detail:{shopId}。不同类型的数据需要设置合理的序列化格式和过期策略。

2. 更新时先写数据库,再删缓存

常见更新流程是先提交数据库更新,再删除对应缓存。后续读请求会从数据库取新值并重建缓存。直接更新缓存可能遇到并发写入覆盖、部分字段遗漏等问题,因此“更新数据库、删除缓存”通常更容易维护。

但这不是跨数据库和 Redis 的原子事务。数据库提交成功后,如果删除缓存失败,旧值可能继续存在到过期;删除成功后,另一个并发读也可能在数据库更新前读取旧值并重新写回。工程上可结合短 TTL、重试、消息补偿或版本机制降低风险。具体方案取决于可接受的陈旧窗口,不能只靠一句“先库后缓存”保证强一致。

3. 缓存穿透

穿透是查询大量不存在的数据,缓存持续未命中,数据库仍被访问。可选做法:

  • 对确认不存在的结果缓存短时间的空值,注意区分“空值”和“缓存故障”。
  • 使用布隆过滤器拦截明显不存在的键,但要接受误判并处理数据更新。
  • 对接口做参数校验、限流和访问监控。

4. 热点过期与缓存击穿

热点键到期时,大量并发请求可能同时回源。可以用互斥锁让少数请求重建缓存,也可以使用逻辑过期:缓存值带一个逻辑过期时间,过期后先返回旧值并由后台刷新。互斥锁会增加等待和失败处理;逻辑过期会短暂提供旧数据,应只用于业务允许陈旧的场景。

5. 大量键同时过期与缓存雪崩

许多键在同一时刻失效,或 Redis 整体不可用,都可能让请求集中到数据库。设置 TTL 随机抖动、分批预热、保护数据库并发、限流降级和准备故障演练,都能降低冲击。缓存不能被当作数据库唯一副本。

6. 淘汰策略和可观测性

Redis 达到 maxmemory 后会根据淘汰策略处理键;策略要与键是否有 TTL、访问频率和服务用途匹配。上线后关注命中率、回源量、延迟、内存、淘汰数量和错误率,而不是只看 Redis 是否存活。

7. 小结

缓存的重点不在于给查询加一个 get 和 set,而在于明确陈旧容忍度、更新失败如何补偿、热点失效如何承压,以及 Redis 故障时数据库如何保护。先用最简单的 Cache-Aside,再根据监控数据补充复杂机制。

参考:Redis 缓存与淘汰策略文档。