一、缓存不是把查询结果塞进去Redis 在国内后端项目里几乎是标配但缓存事故也集中缓存击穿打满数据库热 key 打爆单核大 key 阻塞主线程过期策略随意导致旧数据。缓存设计的核心不是“加一层 Redis”而是明确一致性、失效方式和降级路径。常见缓存对象可分三类低频变更的配置数据、高频读取的详情页数据、需要快速计数或排序的临时数据。不同对象不能用同一套 TTL。配置数据可长 TTL 加主动刷新商品详情适合短 TTL 加随机抖动排行榜、计数器要考虑落库和重放。场景推荐策略风险点商品详情Cache Aside TTL 抖动更新后短时间旧值计数器Redis 增量 异步落库失败重试要幂等热门榜ZSET 定时重算大 key 和全量读取二、Cache Aside 要写完整最常用的是 Cache Aside读时先查缓存未命中再查数据库并回填写时先更新数据库再删除缓存。这里容易犯两个错误把删除缓存写成更新缓存或删除失败不处理导致旧值长期存在。下面是 Go 写法示例重点是空值缓存、TTL 抖动和删除失败记录。示例省略了数据库实现但保留了关键分支。funcGetProduct(ctx context.Context,idint64)(*Product,error){key:fmt.Sprintf(product:%d,id)val,err:rdb.Get(ctx,key).Result()iferrnil{ifval__nil__{returnnil,sql.ErrNoRows}varp Productreturnp,json.Unmarshal([]byte(val),p)}iferr!redis.Nil{log.Printf(redis get failed: %v,err)}p,err:repo.FindProduct(ctx,id)iferrors.Is(err,sql.ErrNoRows){_rdb.Set(ctx,key,__nil__,time.Minute).Err()returnnil,err}iferr!nil{returnnil,err}bs,_:json.Marshal(p)ttl:10*time.Minutetime.Duration(rand.Intn(120))*time.Second_rdb.Set(ctx,key,bs,ttl).Err()returnp,nil}funcUpdateProduct(ctx context.Context,p*Product)error{iferr:repo.UpdateProduct(ctx,p);err!nil{returnerr}key:fmt.Sprintf(product:%d,p.ID)returnrdb.Del(ctx,key).Err()}空值缓存能缓解穿透但 TTL 必须短否则新数据可能被旧空值挡住。TTL 抖动能避免同批 key 同时过期。写后删缓存仍有短暂不一致窗口核心链路要结合版本号、消息补偿或直接绕过缓存。三、热 key 和大 key 要提前治理Redis 单个命令很快但主线程模型决定了大 key 和慢命令会影响其他请求。几 MB 的 String、几万个元素的 Hash、一次KEYS *都可能让延迟突然上升。生产业务禁止使用KEYS排查也应使用SCAN。# 查慢日志redis-cli slowlog get10# 观察命令统计redis-cli info commandstats redis-cli--scan--patternproduct:*|head热 key 通常用本地缓存、读副本、拆 key 处理。首页配置可放进进程内缓存设置 5 到 30 秒 TTL计数类可按用户或时间片分桶再汇总。不要把所有压力都交给 Redis Cluster集群不能消除单个热 key 的集中访问。四、故障恢复要能演练Redis 宕机时最怕业务把所有请求原样打到数据库。连接池要设置超时缓存访问要有熔断关键接口要有降级值。对缓存来说恢复也不是简单重启实例如果大量 key 同时失效恢复后会出现冷缓存数据库仍然可能被打满。redis:addr:redis.internal:6379pool_size:100min_idle_conns:10dial_timeout:200msread_timeout:100mswrite_timeout:100msmax_retries:1实践建议先列出业务里最重要的 20 个缓存 key逐个确认 TTL、最大体积、回源成本、删除策略和 Redis 不可用时的行为。命中率只是结果指标真正要长期维护的是 key 设计、过期策略、慢命令治理和故障演练。
Redis 缓存设计的三条底线:命中率、穿透保护和可恢复性
发布时间:2026/7/9 8:25:55
一、缓存不是把查询结果塞进去Redis 在国内后端项目里几乎是标配但缓存事故也集中缓存击穿打满数据库热 key 打爆单核大 key 阻塞主线程过期策略随意导致旧数据。缓存设计的核心不是“加一层 Redis”而是明确一致性、失效方式和降级路径。常见缓存对象可分三类低频变更的配置数据、高频读取的详情页数据、需要快速计数或排序的临时数据。不同对象不能用同一套 TTL。配置数据可长 TTL 加主动刷新商品详情适合短 TTL 加随机抖动排行榜、计数器要考虑落库和重放。场景推荐策略风险点商品详情Cache Aside TTL 抖动更新后短时间旧值计数器Redis 增量 异步落库失败重试要幂等热门榜ZSET 定时重算大 key 和全量读取二、Cache Aside 要写完整最常用的是 Cache Aside读时先查缓存未命中再查数据库并回填写时先更新数据库再删除缓存。这里容易犯两个错误把删除缓存写成更新缓存或删除失败不处理导致旧值长期存在。下面是 Go 写法示例重点是空值缓存、TTL 抖动和删除失败记录。示例省略了数据库实现但保留了关键分支。funcGetProduct(ctx context.Context,idint64)(*Product,error){key:fmt.Sprintf(product:%d,id)val,err:rdb.Get(ctx,key).Result()iferrnil{ifval__nil__{returnnil,sql.ErrNoRows}varp Productreturnp,json.Unmarshal([]byte(val),p)}iferr!redis.Nil{log.Printf(redis get failed: %v,err)}p,err:repo.FindProduct(ctx,id)iferrors.Is(err,sql.ErrNoRows){_rdb.Set(ctx,key,__nil__,time.Minute).Err()returnnil,err}iferr!nil{returnnil,err}bs,_:json.Marshal(p)ttl:10*time.Minutetime.Duration(rand.Intn(120))*time.Second_rdb.Set(ctx,key,bs,ttl).Err()returnp,nil}funcUpdateProduct(ctx context.Context,p*Product)error{iferr:repo.UpdateProduct(ctx,p);err!nil{returnerr}key:fmt.Sprintf(product:%d,p.ID)returnrdb.Del(ctx,key).Err()}空值缓存能缓解穿透但 TTL 必须短否则新数据可能被旧空值挡住。TTL 抖动能避免同批 key 同时过期。写后删缓存仍有短暂不一致窗口核心链路要结合版本号、消息补偿或直接绕过缓存。三、热 key 和大 key 要提前治理Redis 单个命令很快但主线程模型决定了大 key 和慢命令会影响其他请求。几 MB 的 String、几万个元素的 Hash、一次KEYS *都可能让延迟突然上升。生产业务禁止使用KEYS排查也应使用SCAN。# 查慢日志redis-cli slowlog get10# 观察命令统计redis-cli info commandstats redis-cli--scan--patternproduct:*|head热 key 通常用本地缓存、读副本、拆 key 处理。首页配置可放进进程内缓存设置 5 到 30 秒 TTL计数类可按用户或时间片分桶再汇总。不要把所有压力都交给 Redis Cluster集群不能消除单个热 key 的集中访问。四、故障恢复要能演练Redis 宕机时最怕业务把所有请求原样打到数据库。连接池要设置超时缓存访问要有熔断关键接口要有降级值。对缓存来说恢复也不是简单重启实例如果大量 key 同时失效恢复后会出现冷缓存数据库仍然可能被打满。redis:addr:redis.internal:6379pool_size:100min_idle_conns:10dial_timeout:200msread_timeout:100mswrite_timeout:100msmax_retries:1实践建议先列出业务里最重要的 20 个缓存 key逐个确认 TTL、最大体积、回源成本、删除策略和 Redis 不可用时的行为。命中率只是结果指标真正要长期维护的是 key 设计、过期策略、慢命令治理和故障演练。