缓存故障复盘应沉淀什么
缓存故障恢复后,最容易留下的是一句“Redis 出现 BigKey”。这句话不能解释请求为何被放大、数据库为何同时变慢,也不能阻止另一个业务换个 Key 再犯同样的问题。复盘要把假设与证据分开,再把确认有效的改动放进代码、监控和发布验收。
Redis 故障也不应自动等同于“单线程 CPU 满”。慢命令、网络、内存回收、故障转移、客户端连接池和集群热点都可能造成相似现象。先还原时间线和访问模式,再讨论扩容、分片或替换中间件。
第一份资产是完整时间线
记录缓存延迟何时上升、命中率何时变化、应用重试与回源何时增加、数据库连接和慢查询何时受到影响。每个时间点关联集群节点、应用版本和配置变更。若执行过故障转移、限流、禁用功能或扩容,也写清对象和结果。
用户影响按接口、租户或功能统计,并说明数据来源。没有可靠数字时直接标注未知,不用“全链路雪崩”替代范围。恢复时间也要以关键请求重新正常完成为准,不只看 Redis 节点显示在线。
时间线能帮助区分因果。数据库压力先上升,可能是缓存失效导致回源;Redis 延迟先上升,也可能让客户端超时重试;两者同时变化时,要继续看请求标识和连接池,不能只凭先入为主选择一个根因。
第二份资产是可复查证据
缓存侧保留命令延迟分布、Slow Log 摘要、内存、驱逐、连接、网络和集群状态。应用侧保留缓存命中、超时、重试、回源、连接池等待和降级。数据库侧查看相同时间窗口的请求量、连接与慢查询。三方指标使用一致时间范围。
Slow Log 的时间单位、阈值和保留长度应按当前 Redis 配置解读。某条HGETALL较慢只能说明它值得调查,还要确认返回字段数、调用频率和客户端读取方式。日志中的 Key 可能包含用户或业务信息,对外分享时使用脱敏标识。
BigKey 也要分类型。大 String 的字节数、大集合的元素数、单个元素本身很大,风险并不相同。HLEN只能看到 Hash 字段数量,不能直接代表内存和响应大小。真正影响请求的是具体命令:读取一个字段与读取整份 Hash 的开销完全不同。
巡检应增量、限速并默认只读
SCAN每次迭代比KEYS更容易控制影响,但全量遍历仍会消耗 CPU、网络和客户端资源。生产扫描应选择低峰或只读副本,限制批次与速度,并在延迟上升时停止。Redis Cluster 需要按节点处理,不能只连一个地址就认为覆盖全部槽位。
下面的 Python 示例只按元素数量生成候选项,输出 Key 的带密钥摘要,不打印原文。阈值、扫描目标和摘要密钥都由受控配置提供。它不会计算真实内存,也不会自动拆分或删除 Key。
import hashlib import time from dataclasses import dataclass import redis @dataclass(frozen=True) class Candidate: key_id: str kind: str elements: int def key_id(key: bytes, digest_key: bytes) -> str: return hashlib.blake2b(key, key=digest_key, digest_size=12).hexdigest() def cardinality(client: redis.Redis, key: bytes, kind: bytes) -> int | None: if kind == b"hash": return client.hlen(key) if kind == b"list": return client.llen(key) if kind == b"set": return client.scard(key) if kind == b"zset": return client.zcard(key) return None def scan_candidates( client: redis.Redis, *, digest_key: bytes, element_limit: int, batch_size: int = 100, pause_seconds: float = 0.02, ) -> list[Candidate]: cursor = 0 found: list[Candidate] = [] while True: cursor, keys = client.scan(cursor=cursor, count=batch_size) for key in keys: kind = client.type(key) size = cardinality(client, key, kind) if size is not None and size > element_limit: found.append( Candidate(key_id(key, digest_key), kind.decode(), size) ) if cursor == 0: break time.sleep(pause_seconds) return found真实脚本还要处理认证、TLS、超时、取消、集群节点和错误报告。扫描期间 Key 可能变化,结果是观察样本,不是事务快照。MEMORY USAGE等命令也有自身开销,需要单独评估和抽样,不应对每个 Key 无限制调用。
先修访问模式,再决定是否分桶
若业务每次只需要一个用户字段,改用HGET或HMGET比全量HGETALL更直接。若需要分页,数据结构应支持范围读取,而不是把整份结果取回应用后再切片。大对象的序列化、网络传输和客户端内存也要一起测量。
分桶可以降低单 Key 元素数量,但会引入路由、迁移和跨桶聚合。使用CRC32 % N后改变 N 会让大量数据重新映射;若业务仍要读取所有桶,总工作量并未消失。设计前先明确访问方式、扩容策略和一致性要求,必要时采用稳定的分片映射并提供双写、回填与校验流程。
Redis Cluster 能分散不同槽位的负载,却不能把一个 BigKey 自动拆到多节点。Hash Tag 还会让相关 Key 固定在同一槽位。是否使用 Sentinel、Cluster 或其他兼容产品,要根据容量、故障模型、命令兼容和运维能力测试,不用固定数据量或 QPS 阈值替代评审。
替换缓存实现也不会消除大响应和回源问题。新系统需验证协议差异、持久化、复制、故障转移、监控和客户端行为。先修访问模式,通常比把同一数据结构搬到另一个产品更可控。
缓存失效时保护数据库
真正的高可用边界在缓存之外。应用需要为回源设置并发上限,热点 Key 使用单飞或请求合并,缓存不可用时优先保护数据库。降级可以返回稍旧数据、减少非核心字段或暂时拒绝,但要符合业务一致性要求,并向用户说明状态。
超时值根据服务预算确定,不能统一写成一个固定毫秒数。超时后是否重试取决于命令是否送达、操作是否幂等和剩余时间。重试必须有次数、退避和抖动,避免所有实例同时再次打向缓存。
对危险命令,优先使用 Redis ACL 按应用身份限制管理能力。不要为了防一次误用就全局改名或禁用正常业务依赖的命令。HGETALL本身有合法场景,问题是它是否用于不受控的大集合;治理应落到数据模型、客户端接口和评审门禁。
把结论变成可验收改动
复盘后至少沉淀四类资产:一组能复现访问模式的测试数据;一个监控候选 Key、回源和连接池的看板;一份缓存降级与恢复说明;一条针对数据结构变更的发布验收。每个动作有负责人、完成条件和回滚方式。
修复后在隔离环境回放相同命令分布,观察缓存延迟、响应大小、应用等待和数据库回源。再模拟缓存超时与节点切换,确认请求会收敛,恢复时不会同时预热压垮系统。
最后保留尚未确认的部分。某个 BigKey 与故障同时出现,不一定是唯一原因;若网络或客户端重试也有影响,就分别记录。缓存故障复盘的价值,不是为事故找到一个醒目的标签,而是让团队下次能更早发现异常访问,并在缓存失效时保住后面的数据库。