缓存降级、恢复与治理:派生状态失效时如何守住权威存储
Redis 断开后直接查数据库,看起来是最自然的降级,却可能把原本由缓存吸收的十倍流量瞬间压到权威存储。真正的降级先决定哪些请求值得保、哪些数据允许旧、数据库还能接多少,然后才选择旁路。
缓存 provider 的健康只代表缓存自身可访问,不代表无界回源安全。Redis keyspace event 是 fire-and-forget,Caffeine 本地副本又不跨进程;治理必须在应用层保存容量、陈旧度与恢复策略。
缓存故障先保护权威存储
缓存超时、部分 keyspace 不可用、命中值损坏、拓扑抖动和全量清空是不同故障。简单的“catch 后查数据库”会在 Redis 故障时把全部峰值转移给数据库。降级决策必须同时知道数据库剩余预算、数据允许的陈旧度、功能优先级和当前回源放大率。
可陈旧读只适合已经保存最后可信版本、带生成时间和版本的数据。权限、余额、库存扣减与幂等状态通常不能靠陈旧值继续写。功能降级要返回明确业务语义,不能把“推荐为空”“排行榜稍旧”和“支付状态未知”都包装成正常最新结果。
恢复时最危险的是所有实例同时预热。按 key 分片、租户优先级和流量百分比渐进放量,single-flight 仍限制同 key 回源,数据库 admission control 决定总并发。预热列表来自可审查的热点统计,不是把整个数据库复制进 Redis。恢复完成的证据是错误率、放大率、数据库负载和陈旧年龄回到基线,而不只是 Redis ping 成功。
治理面需要 cache catalog:cache name、owner、权威源、key schema、value schema、TTL、容量、敏感级别、一致性承诺、旁路策略和删除联系人。key/value schema 变更使用版本前缀或双读迁移,禁止直接改变同名 key 的编码。高风险 flush、批量删除和 TTL 变更走审计与灰度。
演练覆盖延迟而不仅是宕机:注入客户端排队、少量超时、大 reply、重定向风暴、命中率骤降和 keyspace 清空,观察应用是否在 deadline 内收敛。旁路开关本身也要限时、限范围并自动回收,避免一次事故后永久绕过缓存。
这一节是 Redis 开发落地最容易出事故的地方。缓存问题往往不是某一行命令错了,而是容量、归属、TTL、热点、一致性和降级长期没人治理。
Key 规范
现象:缓存很多,但没人知道哪个模块写的,改一个业务要全库 SCAN 找 key。
直接做法:
key 必须包含应用、环境、业务域、对象、ID、版本。每类 key 要有 owner。key 结构升级必须升级版本,不能让新旧 value 混用。
禁止使用真实手机号、身份证、token 等敏感信息直接做 key。
验证:
SCAN 0 MATCH shop:prod:order:* COUNT 100生产建议:缓存契约要进代码评审。没有 key 规范的缓存,不允许进入核心链路。
命令复杂度治理
现象:Redis CPU 不高但接口 P99 抖动,SLOWLOG 里反复出现 HGETALL、SMEMBERS、大范围 ZRANGE 或 Lua 脚本。
直接做法:
COMMAND INFO HGETALL
COMMAND INFO SMEMBERS
OBJECT ENCODING shop:prod:rank:sale:202607
INFO commandstats
SLOWLOG GET 10判断:
O(n) 命令是否进入在线请求链路。集合元素数量是否超过缓存契约上限。cmdstat_* 调用次数是否和业务流量匹配。
慢命令是否集中在同一类 key 或同一个接口。
生产建议:禁止在线请求执行全量集合读取。排行榜、关系集合、购物车、权限集合都要分页、限长或拆 key;确实需要全量导出时,放到后台任务并限速。
TTL 治理
现象:大量 key 没有过期时间,或者所有 key 同一时间过期。
直接做法:
TTL shop:prod:order:detail:100001:v1判断:
-2 表示 key 不存在。-1 表示 key 存在但没有 TTL。正数表示剩余秒数。
生产建议:不同缓存类型使用不同 TTL 档位,统一加随机扰动。空值 TTL 必须短,热点缓存建议逻辑过期。
内存治理
现象:Redis 内存持续增长,淘汰次数上升,命中率下降,接口偶发慢。
直接做法:
INFO memory
INFO stats
MEMORY USAGE shop:prod:order:detail:100001:v1
redis-cli --bigkeys判断:
used_memory 是否持续上涨。evicted_keys 是否增加。是否存在大量无 TTL key。
是否存在 BigKey 或大集合。
生产建议:缓存也有成本。不要因为 Redis 快,就缓存低频、巨大、频繁变化的数据。
热点治理
现象:某个活动、配置、榜单或商品 key 被打爆,Redis CPU 和网络上升,数据库也可能被击穿。
直接做法:
热点 key 预热。本地短缓存。逻辑过期 + 异步刷新。
互斥重建。限流和降级。
验证:
SLOWLOG GET 10
INFO commandstats生产建议:热点治理要提前设计。等热点发生后再加锁,通常已经进入线程池和数据库联动事故。
Pipeline 和输出缓冲
现象:批量预热、批量查询或活动接口使用 pipeline 后,客户端偶发超时,Redis 内存上涨,连接被服务端断开。
直接做法:
CLIENT LIST
INFO clients
CONFIG GET client-output-buffer-limit
INFO commandstats判断:
单个连接的 omem、obl、tot-mem 是否持续增长。pipeline 响应是否包含大 value 或大集合结果。客户端是否只管写入 pipeline,不及时读取响应。
同一应用实例是否并发打开太多大 pipeline。
生产建议:pipeline 要按响应字节数、连接数和 Redis P99 做上限,而不是只写“每批 1000 条”。大 value 批量读和小 value 批量读不是同一种风险。
持久化抖动
现象:业务代码没有发布,Redis 慢命令也不多,但接口 P99 在某些时间点规律性升高。
直接做法:
INFO persistence
INFO memory
LATENCY LATEST
LATENCY DOCTOR判断:
抖动时间是否和 rdb_bgsave_in_progress 或 aof_rewrite_in_progress 重合。latest_fork_usec 是否异常升高。aof_delayed_fsync 是否增长。
抖动前后是否有批量写入、预热、大 key 删除或数据修复。
生产建议:开发侧要把 Redis 持久化窗口当成容量约束的一部分。批量任务要可暂停、可限速、可错峰,不能只在应用里追求最快写入。
降级
现象:Redis 抖动后,所有请求回源数据库,数据库也被拖垮。
直接做法:
核心数据允许有限回源。非核心数据返回默认值或空结果。后台任务暂停预热和批量刷新。
对回源数据库加限流。
生产建议:降级要有开关和指标。不要把降级写成隐藏分支,结果没人知道什么时候触发、返回了多少默认值。
缓存一致性
现象:数据库已更新,用户还看到旧数据;或者缓存删除失败,没有补偿。
直接做法:
写库成功后删除缓存。删除失败写入重试队列或补偿表。核心配置使用版本号或发布号。
定期抽样比对数据库和缓存。
生产建议:缓存一致性的关键是可观测和可补偿。只要不能证明缓存已失效,就不要认为更新链路完成。
成本
现象:Redis 容量越买越大,但命中率不高,很多缓存只是把数据库旧问题搬到内存里。
直接做法:
对每类缓存记录命中率、平均 value 大小、key 数、TTL、回源成本。删除低命中、大 value、频繁更新的缓存。对报表、导出、聚合类需求改用异步汇总,而不是塞 Redis。
生产建议:缓存不是免费午餐。内存、网络、序列化、运维、排障和一致性都是成本。
用状态模型验证缓存窗口
两个 Java 17 模型不连接真实 Redis,也不复刻 Caffeine 内部结构。它们把本篇必须守住的状态、版本和容量不变量变成确定输出;真实客户端与 Redis 集成测试继续验证协议、超时、拓扑和服务器行为。
javac --release 17 -Xlint:all -Werror examples/backend-development/cache/cache-degradation-governance/CacheDegradationDemo.java examples/backend-development/cache/cache-degradation-governance/RecoveryRampDemo.java
java -cp examples/backend-development/cache/cache-degradation-governance CacheDegradationDemo
java -cp examples/backend-development/cache/cache-degradation-governance RecoveryRampDemocacheAvailable=false incoming=900 dbAdmitted=120 shed=780
recoveryRamp=[5, 15, 30, 60, 100] prewarmBounded=true errorRateStable=true缓存正确性不是“命中返回值”,而是命中值仍在允许的陈旧窗口内、加载不会放大到权威存储、旧版本不能覆盖新版本、失败能够在预算内退化。模型输出任一项改变,都应触发对应集成用例,而不是只调整命中率告警。
命中率之外还要看放大率和陈旧年龄
把缓存当可丢失的派生状态验收
上线必须有旁路开关、按 cache name/tenant/keyspace 的隔离、容量硬上限、随机化过期、重建并发上限和回滚路径。缓存故障时不能把全部流量无条件打回数据库;先保护权威存储,再决定陈旧读、功能降级、排队或拒绝。只有缓存消失时系统仍以受控方式工作,缓存才真正是优化层。
