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 并接受重放带来的状态回退风险。重启后应主动校验版本号,而不是直接信任缓存。
把四机制串成一条判断链
实际排查时,可以按下面的顺序判断一次不一致属于哪一类:
- 先看缓存键是否存在。不存在,可能是过期或淘汰,属于可自愈窗口,回源即可。
- 存在但值旧,检查版本号
ver是否落后于数据库。落后说明写入路径没有正确失效。 - 版本号一致但值旧,说明写入时缓存与数据库不是同一版本,需要检查更新顺序。
- 重启后出现旧值,检查持久化配置与 AOF 重放范围,必要时启动时清空缓存或强制回源。
配置示例与观察点
下面是一份面向缓存场景的 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 快速增长,说明内存压力已经在改变缓存内容,此时一致性窗口会被动扩大,应优先扩容或拆分实例,而不是继续调大过期时间。
最后要明确:键值模型和过期策略决定“旧值能活多久”,内存淘汰决定“新值能活多久”,持久化决定“重启后谁说了算”。把这四者与版本号校验配合起来,缓存一致性才从“靠感觉”变成“有依据”。