Redis缓存旁路控制数据陈旧时间
来源:17golang原创
时间:2026-11-15 11:31:38 172浏览 收藏
Redis 缓存旁路最容易被忽略的不是“要不要加缓存”,而是数据最多允许旧多久。把这个时间直接换成缓存键的 TTL,再配合写入后的主动失效,通常比试图让 Redis 和数据库时时同步更稳。本文用一个商品详情读取场景,把正值、空值和热点键分别放进可解释的边界里。
官方地址:https://redis.io/docs/latest/develop/use-cases/cache-aside/
TTL 是允许缓存继续服务旧值的上限,不是数据一定会在这个时刻被刷新。允许短暂陈旧的读场景可以依赖 TTL;不能接受旧值的写场景,应在权威数据更新成功后删除对应缓存键。
先把陈旧时间换算成 Redis TTL
假设商品名称允许最多旧 60 秒,价格只允许旧 5 秒。那就不要给所有实体统一设置 30 分钟 TTL,而应让业务字段的陈旧容忍度决定缓存策略。TTL 越短,读命中率可能越低、回源压力越高;TTL 越长,命中率更好,但旧值窗口也更大。
Redis 的过期时间可以用秒或毫秒精度设置,常用的 SET ... EX、SET ... PX 或单独的 EXPIRE 都是在给键设置时间边界。对缓存旁路来说,关键不是记住某个命令,而是让“业务容忍度”和“缓存键 TTL”能够一一对应。

读路径先查缓存,未命中再回源
读旁路的最小实现是:先读 Redis,命中就返回;未命中才查询数据库或其他权威服务,然后把结果带 TTL 写回。下面的 Go 片段刻意把 TTL 作为参数传入,避免在业务方法里藏一个无法解释的常量。
// GetProduct 展示缓存旁路:Redis 命中直接返回,未命中才访问权威源。
func (c *Cache) GetProduct(ctx context.Context, id string, ttl time.Duration) (Product, error) {
key := "cache:product:" + id
cached, err := c.rdb.Get(ctx, key).Result()
if err == nil {
// 命中后只做反序列化,不再访问数据库。
var product Product
if json.Unmarshal([]byte(cached), &product) == nil {
return product, nil
}
}
if err != nil && !errors.Is(err, redis.Nil) {
return Product{}, err
}
// 数据库是权威源;真实项目中这里还应记录回源次数。
product, err := c.repo.FindProduct(ctx, id)
if err != nil {
return Product{}, err
}
payload, err := json.Marshal(product)
if err != nil {
return Product{}, err
}
// EX 由客户端用 ttl 表达,保证每个缓存键都有明确过期边界。
if err := c.rdb.Set(ctx, key, payload, ttl).Err(); err != nil {
return Product{}, err
}
return product, nil
}
这里的 redis.Nil 代表键不存在,不应和 Redis 连接错误混为一谈。缓存写入失败时是否继续返回数据库结果,要由接口的可用性目标决定;如果缓存只是加速层,通常可以记录错误后仍返回权威源结果。
写入成功后删除键,不要手工“同步”旧对象
更新商品时,先提交数据库,再删除 cache:product:{id}。下一次读取自然会重新加载最新值。这样做的好处是逻辑简单:缓存不承担权威性,也不会因为更新顺序或部分字段遗漏而长期保存一份看似完整、实际过期的对象。
// UpdateProduct 只在权威写入成功后删除缓存,避免提前清除旧值。
func (c *Cache) UpdateProduct(ctx context.Context, product Product) error {
if err := c.repo.UpdateProduct(ctx, product); err != nil {
return err
}
// DEL 让下一次读回到权威源,避免继续服务旧对象。
return c.rdb.Del(ctx, "cache:product:"+product.ID).Err()
}
如果业务允许极短的旧值窗口,也可以使用 TTL 作为兜底;但对于价格、库存、权限这类写入后必须尽快可见的数据,主动删除比等待 TTL 到期更直接。
空值和热点键要单独设边界
查询不存在的商品时,如果每次请求都直达数据库,攻击性探测或输入错误会反复击穿同一个 ID。可以缓存一个带短 TTL 的“确实不存在”标记,但空值 TTL 应明显短于正值 TTL,以便新建记录尽快可见。
另一类问题是热点键同时过期。大量请求会在同一时刻发现缓存为空,如果每个请求都回源,就会把数据库瞬间推高。可用单飞锁让一个请求负责回源,其余请求短暂等待缓存重新出现;锁的 TTL 要长于最坏的权威查询耗时,并用唯一 token 防止过期后误删别人的锁。

# 查看缓存内容与剩余 TTL;这里只做排查,不把命令输出当成业务真相。
redis-cli HGETALL cache:product:p-001
redis-cli TTL cache:product:p-001
# 写入一个带 30 秒过期时间的字符串缓存,EX 的单位是秒。
redis-cli SET cache:product:p-001 '{"id":"p-001"}' EX 30
用 TTL、命中率和回源次数复查策略
上线后至少区分三类指标:缓存命中率、权威源回读次数、热点键失效后的并发等待次数。单看命中率容易误判:命中率高但 TTL 过长,可能只是把旧值服务得更久;TTL 很短却没有回源告警,也可能是流量很低。
可以抽样读取 TTL 或 PTTL 观察剩余时间,结合业务的陈旧投诉和数据库压力调整。最后用这张速查表收口:
| 场景 | 建议 | 边界 |
|---|---|---|
| 普通读多写少 | 正值缓存 + TTL | TTL 不超过业务可接受陈旧窗口 |
| 写入必须尽快可见 | 权威写成功后 DEL | 删除失败要记录并处理重试 |
| 不存在的 ID 被反复查询 | 空值短缓存 | 空值 TTL 要短于正值 TTL |
| 热点键同时失效 | 单飞锁或提前刷新 | 锁 TTL 覆盖最坏回源耗时 |
相关问题
TTL 到期后一定会立刻删除键吗?不要把它当作精确的业务事件;TTL 的职责是限定可服务窗口,关键写入仍应主动失效。
为什么不直接把数据库更新后的对象 SET 回 Redis?删除后按需回填的状态更少,能减少部分更新、顺序竞争和缓存结构漂移。
空值要不要缓存?只有当不存在查询确实会重复打到权威源时才缓存,并给它更短的 TTL。
-
117 收藏
-
426 收藏
-
171 收藏
-
113 收藏
-
195 收藏
-
311 收藏
-
483 收藏
-
379 收藏
-
464 收藏
-
239 收藏
-
331 收藏
-
421 收藏
-
184 收藏
-
209 收藏
-
406 收藏
-
135 收藏
-
401 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习