2. Redis作为缓存的核心机制

把 Redis 当缓存用,不能只记住“GET/SET 很快”。缓存一致性问题的根源,往往不在业务代码,而在 Redis 自身的四个机制:键值模型决定“缓存里到底存了什么”,过期策略决定“数据什么时候逻辑消失”,内存淘汰决定“数据在压力下被谁挤掉”,持久化决定“重启后缓存与数据库是否还对得上”。理解这四层,才能判断一次不一致是暂时抖动、可自愈,还是会长期残留。

键值模型:缓存条目的身份与生命周期

Redis 是键值数据库,缓存场景下通常用字符串或哈希承载业务数据。键的设计直接决定后续能否精确失效。推荐使用“业务前缀:主键:版本或维度”的结构,例如 user:1001:profile、sku:8801:stock。这样做的好处是:单条数据可以独立过期,批量失效可以用 SCAN 按前缀匹配,而不是依赖 KEYS 阻塞主线程。

值的选择也有讲究。若缓存的是数据库整行,更新时容易覆盖掉并发写入;若缓存的是热点字段,失效粒度更细但需要更多键。示例中我们缓存用户档案的 JSON 字符串,并额外写入一个版本号键 user:1001:ver,用于判断缓存是否落后于数据库。

SET user:1001:profile '{"name":"Ada","level":3}' EX 300
SET user:1001:ver 17

这里 EX 300 表示 5 分钟后过期。过期时间不是“删除时间”,而是“逻辑上不再可信的时间点”。理解这一点,才能理解下面过期策略带来的不一致窗口。

过期策略:惰性删除 + 定期删除

Redis 不会为每个键启动定时器。它采用两种方式组合:

  • 惰性删除:访问键时才检查是否过期,过期则删除并返回空。优点是省 CPU,缺点是没人访问的过期键会一直占内存。
  • 定期删除:每秒若干次随机抽取部分设置了过期时间的键,删除其中已过期的。它是抽样,不是全量扫描,因此过期键可能在被访问前仍然存在。

对一致性的影响是:数据库已经更新,但 Redis 中旧值可能因为“还没被抽到”而继续被读到。这个窗口通常很短,但并非零。若业务要求强一致,不能只依赖过期时间,必须配合主动删除或版本校验。

内存淘汰:压力下的“被动不一致”

当 maxmemory 达到上限,Redis 会按 maxmemory-policy 淘汰键。缓存场景常用 allkeys-lru 或 allkeys-lfu。这意味着:即使某个键还没过期,也可能因为内存压力被提前删除。被淘汰后,下一次读请求会回源数据库并重建缓存,这本身是正常行为,但会带来两个一致性问题:

  • 淘汰发生在“数据库已更新、缓存尚未更新”之间时,旧值被删掉反而避免了脏读;
  • 淘汰发生在“缓存已写入新值”之后,则新值丢失,下次回源若读到旧库(如主从延迟)会重新写入旧值。

因此,淘汰策略不是纯运维参数,它会影响缓存命中率和一致性窗口。生产上建议把缓存实例与持久化实例分开,避免淘汰把需要保留的数据挤掉。

持久化:重启后缓存与数据库的“对账”

Redis 提供 RDB 和 AOF 两种持久化。缓存场景下,持久化对一致性的影响常被低估:

机制特点对一致性的影响
RDB周期性快照,文件小、恢复快两次快照之间的写入会丢失,重启后缓存可能回退到旧版本
AOF追加写命令,appendfsync everysec 最多丢 1 秒丢失窗口小,但 AOF 重放可能把已删除的旧值重新写回
不持久化重启后缓存为空全部回源,短时数据库压力大,但不会残留脏数据

关键判断:如果缓存允许丢失、回源即可,可以不持久化或只用 RDB;如果缓存里存了无法从数据库重建的数据(如会话、计数器),则必须用 AOF 并接受重放带来的状态回退风险。重启后应主动校验版本号,而不是直接信任缓存。

Redis 缓存四机制对一致性的影响链 键值模型 user:1001:profile user:1001:ver 决定失效粒度与身份 过期策略 惰性删除 + 定期抽样 EX 300 旧值可能短暂残留 内存淘汰 allkeys-lru / lfu maxmemory 触发 未过期键也可能被删 持久化 RDB / AOF 重启恢复 可能回退旧版本 数据库(权威数据源) 版本号 ver = 17 读请求 GET profile 写请求 UPDATE + DEL 不一致窗口的三种典型来源 ① 过期残留 旧值未被抽到删除 读到过期但未删的键 ② 淘汰丢失 新值写入后被 LRU 挤掉 回源读到旧库重建 ③ 持久化回退 重启后 AOF 重放旧命令 缓存版本落后于数据库 判断依据:缓存版本号 ver 是否等于数据库 ver;不等则回源重建

把四机制串成一条判断链

实际排查时,可以按下面的顺序判断一次不一致属于哪一类:

  1. 先看缓存键是否存在。不存在,可能是过期或淘汰,属于可自愈窗口,回源即可。
  2. 存在但值旧,检查版本号 ver 是否落后于数据库。落后说明写入路径没有正确失效。
  3. 版本号一致但值旧,说明写入时缓存与数据库不是同一版本,需要检查更新顺序。
  4. 重启后出现旧值,检查持久化配置与 AOF 重放范围,必要时启动时清空缓存或强制回源。
缓存一致性不是“让 Redis 永远和数据库一样”,而是“让不一致窗口可控、可检测、可自愈”。过期和淘汰带来的短暂旧值通常可接受;持久化回退和写入顺序错误带来的旧值必须主动修复。

配置示例与观察点

下面是一份面向缓存场景的 Redis 关键配置示例,参数为教学示例值,需按实际内存和业务调整:

maxmemory 4gb
maxmemory-policy allkeys-lru
appendonly yes
appendfsync everysec
save 900 1

观察点:用 INFO memory 看 evicted_keys 是否持续增长;用 INFO stats 看 expired_keys 与 keyspace_hits/misses;用 TTL user:1001:profile 确认过期时间是否符合预期。若 evicted_keys 快速增长,说明内存压力已经在改变缓存内容,此时一致性窗口会被动扩大,应优先扩容或拆分实例,而不是继续调大过期时间。

最后要明确:键值模型和过期策略决定“旧值能活多久”,内存淘汰决定“新值能活多久”,持久化决定“重启后谁说了算”。把这四者与版本号校验配合起来,缓存一致性才从“靠感觉”变成“有依据”。