Redis 缓存设计:Key 命名、过期策略、击穿和一致性
本文目录
缓存不是数据库的第二份权威数据,而是带有明确陈旧窗口、可丢弃并能重新构造的副本。先测量数据库压力和接口延迟,再决定缓存哪些读路径。没有命中率、数据访问分布和一致性要求时,盲目加 Redis 只会多一个故障域。
先选模式:cache-aside
读请求先查缓存,未命中再读数据库并回填;写请求先提交数据库,再删除对应缓存键。这个模式简单,适合读取较多、允许短暂陈旧的详情或目录。缓存故障时应回退到数据库但限制并发和超时,避免 Redis 故障把主库流量瞬间放大。
async function getProduct(tenantId, productId, deps) {
const key = 'cache:product:v2:' + tenantId + ':' + productId;
const cached = await deps.redis.get(key);
if (cached !== null) {
try {
return JSON.parse(cached);
} catch {
await deps.redis.del(key);
}
}
const row = await deps.db.product.findUnique({
where: { tenantId_productId: { tenantId, productId } },
});
if (row !== null) {
const ttlSeconds = 300 + Math.floor(deps.random() * 60);
await deps.redis.setEx(key, ttlSeconds, JSON.stringify(row));
}
return row;
}
async function updateProduct(tenantId, productId, patch, deps) {
const row = await deps.db.product.update({
where: { tenantId_productId: { tenantId, productId } },
data: patch,
});
await deps.redis.del('cache:product:v2:' + tenantId + ':' + productId);
return row;
}
示例依赖数据库复合唯一键,并省略了错误策略。缓存写失败不应让已经成功的数据库写入被误报为失败;可以返回成功并记录失效任务,或将数据库变更与 outbox 事件放在同一事务,再由消费者删除缓存。选择哪种语义,要由业务的陈旧容忍度决定。
Key 设计与数据边界
Key 要能回答“这是哪个环境、实体、租户、版本和查询”。推荐稳定前缀,例如 app:env:entity:v2:tenant:id;版本段用于序列化结构变化时平滑迁移。分页列表的 Key 需要包括所有影响结果的筛选、排序、权限范围和分页游标,参数序列化应稳定且有长度上限。
多租户数据必须把 tenantId 纳入 Key,并在数据库查询中再次使用同一租户条件。只在缓存 Key 中隔离而数据库查询漏掉租户条件,仍然是越权;只在 SQL 中隔离但 Key 不含租户,也可能把别人的缓存结果返回。不要缓存访问令牌、密码、支付凭证或不可撤销的授权结论。
TTL、热点与失效竞态
- TTL 为缓存陈旧时间设上界,不代表到点时所有副本准时删除。给热点条目的 TTL 加随机抖动,避免一批 Key 同时过期。
- 热点失效时采用 single-flight、互斥锁或 stale-while-revalidate,降低并发回源。进程内 single-flight 只覆盖单进程;多实例需要共享协调,而且锁过期、持有者崩溃和续租都要考虑。
- 锁释放必须比较唯一 token 后再删除,不能对可能已过期并被别人取得的锁直接 DEL。锁不是数据正确性的替代品,写入仍需数据库约束或版本条件。
- 空结果可短暂缓存以抑制不存在 ID 的重复查询,但 TTL 更短并限制 Key 数量,防止攻击者制造大量随机 Key。
- 写后删除比双写缓存更容易恢复;并发读写仍可能出现旧读回填覆盖新值。需要更强一致性时可用版本号、写入栅栏、消息失效或绕过缓存的读模型。
缓存击穿、雪崩与穿透
击穿是单个热点 Key 过期造成数据库瞬时放大;雪崩是大量 Key 同时失效或缓存整体不可用;穿透是持续请求不存在的数据。分别使用热点合并回源、TTL 抖动/分批预热、短期空值缓存和入口限流。Bloom Filter 只能帮助快速过滤候选,不可代替数据库唯一约束或授权检查。
缓存不可用时设置回源并发上限、请求超时、熔断和降级响应;不要无限重试 Redis 或数据库。恢复期间逐步放量并观察数据库连接池、慢查询、延迟和错误率,避免所有进程同时重建缓存。
容量、淘汰和运维
Redis 是有限内存系统。估算序列化后值的平均大小、Key 开销、活跃条目数量和复制/持久化开销,再配置 maxmemory 与适合该实例用途的淘汰策略。缓存实例与会话、队列等关键数据共用时,淘汰策略和 maxmemory-policy 必须按最重要的数据语义共同决定;纯缓存与必须保留的数据最好使用隔离实例或明确的资源预算。
观测命中/未命中、回源 QPS、Key 数量、内存、淘汰数、过期数、慢命令、连接拒绝、延迟分位数和缓存失效失败。命中率要按端点/实体分组看;全局平均会掩盖某个热点接口的低命中。日志记录 Key 类型和请求关联 ID即可,不记录完整个人数据。
上线步骤
- 先建立无缓存基线:读 QPS、P50/P95/P99、数据库耗时、连接池和错误率。
- 选一个读多写少、允许短期陈旧的接口,小流量启用并保留关闭开关。
- 设计 Key 规范、TTL、最大响应体积、权限范围和失效路径;为更新、删除、租户切换和权限变更写测试。
- 注入 Redis 超时、断连、旧值回填、热点过期和数据库慢查询,验证降级是否有界。
- 比较缓存前后的真实用户延迟与数据库负载;没有测得收益就删掉这层复杂度。
Redis 官方 cache-aside 指南 讲解 TTL、有界陈旧与失效;客户端缓存追踪的失效消息语义见 Redis client-side caching。实际命令、协议和客户端能力要核对部署版本。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。