登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

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配置、缓存键和权威数据库的静态关系说明图
图1:Redis 缓存旁路的 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 防止过期后误删别人的锁。

Redis热点键、单飞锁、权威查询、正值TTL和空值TTL的静态关系说明图
图2:热点键与空值缓存边界说明图,展示单飞锁和两种 TTL 的职责,不是运行截图。
# 查看缓存内容与剩余 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 观察剩余时间,结合业务的陈旧投诉和数据库压力调整。最后用这张速查表收口:

场景建议边界
普通读多写少正值缓存 + TTLTTL 不超过业务可接受陈旧窗口
写入必须尽快可见权威写成功后 DEL删除失败要记录并处理重试
不存在的 ID 被反复查询空值短缓存空值 TTL 要短于正值 TTL
热点键同时失效单飞锁或提前刷新锁 TTL 覆盖最坏回源耗时

相关问题

TTL 到期后一定会立刻删除键吗?不要把它当作精确的业务事件;TTL 的职责是限定可服务窗口,关键写入仍应主动失效。

为什么不直接把数据库更新后的对象 SET 回 Redis?删除后按需回填的状态更少,能减少部分更新、顺序竞争和缓存结构漂移。

空值要不要缓存?只有当不存在查询确实会重复打到权威源时才缓存,并给它更短的 TTL。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>