宁语之溪
众里寻他千百度,蓦然回首,那人却在灯火阑珊处。 (宋·辛弃疾·青玉案)
自豪地使用 Typecho 建站搭配使用 🌻Sunny 主题博主 昨天 13:54 在线 · 当前 44 人活跃
文章

Redis 缓存一致性:从热点数据到失效策略

语之溪

·

数据库

·

⚠️ 本文最后更新于2026年03月28日,已经过了149天没有更新,若内容或图片失效,请留言反馈

在 HAOVP 中接入 Redis 之后,我越来越觉得“用了缓存”和“正确使用缓存”是两件事。把数据写进 Redis 并不难,真正需要判断的是哪些数据值得缓存、缓存多久、数据变化时怎样失效,以及 Redis 不可用时业务应该怎样处理。

目前我先把 Redis 用在认证状态上:验证码、刷新令牌、角色和权限等数据会进入 Redis,并设置过期时间或在特定操作后删除。视频详情、热门列表等热点数据缓存还处在策略梳理阶段,接下来需要按真实查询和更新链路逐步接入。

先区分三类数据

不同数据不能共用同一套缓存策略。

临时状态

验证码、一次性校验信息和短期会话状态有明确生命周期:

写入 Redis
    -> 设置过期时间
    -> 使用成功后主动删除
    -> 超时后自动失效

这类数据通常不需要长期保留。验证码即使仍在有效期内,验证成功后也应删除,避免重复使用。

可从数据库重建的数据

角色、权限、用户基础信息或视频详情可以从数据库重新读取。Redis 在这里用于减少重复查询,数据库仍是事实来源。

读取缓存
    -> 命中:返回
    -> 未命中:查询数据库
    -> 写回缓存
    -> 返回

这就是常见的旁路缓存。缓存丢失不会让原始数据消失,但会增加数据库访问。

累计状态

播放量、点赞数和其他高频计数如果先在 Redis 中累积,再异步写回数据库,就不再只是可随时丢弃的副本。此时要处理重复累计、落库失败、任务重试和 Redis 故障等问题。

HAOVP 的视频统计任务还没有接通这条累计与落库链路,这部分需要先定义计数口径、幂等方式和失败补偿,再继续实现。

什么才算热点数据

热点数据不是“看起来重要的数据”,而是在一段时间内被集中访问、直接回源会造成明显压力的数据。

在线视频平台中可能出现的热点包括:

  • 首页推荐或热门列表;
  • 某个突然被大量访问的视频详情;
  • 高频读取的标签和分类;
  • 登录用户的角色与权限;
  • 短时间内频繁校验的会话状态。

“可能出现”不等于已经通过监控确认。是否缓存、缓存多长时间,需要结合访问频率、数据库耗时、数据变化速度和一致性要求判断。

我会先问四个问题:

这个数据读得是否足够频繁?
缓存未命中时,回源成本有多高?
数据更新后允许旧值存在多久?
缓存丢失后能否从事实来源重建?

如果这四个问题没有答案,直接增加缓存可能只是在系统中多放一份需要维护的数据。

旁路缓存怎样处理读取

一个通用的查询结构可以写成:

public VideoView queryVideo(Long id) {
    String key = "video:detail:" + id;
    VideoView cached = cache.get(key);
    if (cached != null) {
        return cached;
    }

    VideoView value = repository.findView(id);
    if (value != null) {
        cache.set(key, value, ttlWithJitter());
    }
    return value;
}

这是我用来梳理旁路缓存流程的简化示例,实际接入视频详情时还要按当前对象结构和键空间重新设计。

读取流程看似简单,但还需要决定:

  • 查询不到数据时是否短暂缓存空值;
  • 多个请求同时未命中时怎样限制回源;
  • 缓存序列化格式变更后怎样兼容;
  • Redis 超时后是降级到数据库,还是直接失败;
  • 回源失败时能否返回旧值。

缓存逻辑如果把数据库异常也当成“数据不存在”,就可能写入错误空值。

为什么更新后通常删除缓存

对于数据库是事实来源的旁路缓存,常用流程是:

更新数据库
    -> 数据库事务成功
    -> 删除相关缓存
    -> 下一次查询重新回填

相比同时更新数据库和缓存,删除缓存可以减少在两个存储中拼装同一份复杂对象的机会。下一次读取会从数据库获得新值。

一个简化示例:

@Transactional
public void updateProfile(ProfileUpdate command) {
    repository.update(command);
    afterCommit(() -> cache.delete(profileKey(command.userId())));
}

示例强调“数据库提交成功后再失效缓存”。如果事务最终回滚,却提前删除了缓存,数据不会错误,但会产生一次不必要的回源;如果数据库更新失败却错误地写入新缓存,则可能直接产生脏数据。

先更新数据库再删除缓存也不是绝对一致

即使采用“更新数据库,再删除缓存”,并发下仍可能出现竞态:

请求 A:缓存未命中,读取旧数据库值
请求 B:更新数据库为新值,删除缓存
请求 A:把刚才读到的旧值写回缓存

最终缓存里仍然可能是旧值。因此,这个策略的目标通常是降低不一致概率和持续时间,而不是提供严格的线性一致性。

可以根据业务要求继续增加保护:

  • 给缓存设置合理 TTL,让旧值最终失效;
  • 更新后延迟再次删除,缩短并发回填造成的旧值窗口;
  • 使用版本号,避免旧版本覆盖新版本;
  • 通过消息或变更事件通知其他实例失效;
  • 对强一致场景直接读取数据库;
  • 对关键写操作使用串行化或其他并发控制。

这些办法各有成本,不能为了追求形式上的完整,把所有机制一次性堆进每个接口。

TTL 不是随便填一个数字

过期时间需要与数据语义匹配。

验证码          -> 较短且明确的有效期
会话或刷新令牌   -> 与认证生命周期一致
角色权限         -> 允许短期缓存,变更时主动删除
视频详情         -> 根据更新频率和旧值容忍度决定
热门列表         -> 根据计算周期和实时性决定

TTL 太长,旧数据持续时间会增加;TTL 太短,大量请求频繁回源,缓存收益下降。

多个热点键如果在同一时刻过期,可能瞬间把请求全部放回数据库。可以在基础 TTL 上增加小范围随机抖动:

实际 TTL = 基础 TTL + 随机抖动

这里只给出策略结构,不给出固定秒数,因为不同接口的访问量和更新周期不同。

缓存穿透、击穿和雪崩分别是什么

这三个问题容易混在一起。

缓存穿透

请求的数据本来就不存在,每次缓存未命中后都访问数据库。可以使用参数校验、短期空值缓存或布隆过滤器等方式减少无效回源,但必须避免把数据库暂时异常误判为永久不存在。

缓存击穿

一个高频热点键失效时,大量并发请求同时回源。可以使用互斥重建、逻辑过期、提前刷新或单飞请求合并等方式,让少量请求负责重建。

缓存雪崩

大量键集中失效,或者 Redis 整体不可用,导致请求大范围回源。TTL 随机化、分批预热、限流、降级和数据库容量保护可以减少影响。

缓存保护措施不能假设数据库可以无限承接回源流量。Redis 故障时是否允许所有请求访问数据库,需要经过容量评估。

多实例下本地缓存更难保持一致

我也在方案中考虑过“本地缓存 + Redis + 数据库”的多级缓存。多一级本地缓存可以减少网络访问,但也多出一层失效问题。

实例 A 本地缓存
实例 B 本地缓存
实例 C 本地缓存
        ↓
       Redis
        ↓
      数据库

数据更新后,不仅要删除 Redis,还要通知所有服务实例清理本地副本。实例重启、消息丢失和网络分区都会影响失效传播。

这套多级缓存目前还停留在方案层,接下来只有把本地副本、Redis 和数据库之间的读取与失效链路真正接通,才适合用于业务查询。

Redis 高可用不等于缓存一致性

Redis 主从和 Sentinel 解决的是实例故障后的服务连续性。缓存一致性关注的是 Redis 中的数据与数据库事实是否匹配。

两者可以同时存在,但解决的问题不同:

  • Sentinel 能切换 Redis 主节点,不会自动删除业务脏缓存;
  • 正确的失效策略不能替代 Redis 的持久化、主从和故障恢复;
  • Redis 切换期间仍要处理连接失败、命令重试和数据复制延迟;
  • 重试写命令时要考虑操作是否幂等。

看到 Sentinel 主节点正常,只能说明 Redis 拓扑的一部分状态,不能证明每个业务缓存都正确。

怎样验证当前缓存策略

我会按数据生命周期检查,而不是只看 Redis 中有没有键。

临时状态

  • 写入时是否带正确 TTL;
  • 使用成功后是否立即删除;
  • 超时后是否被拒绝;
  • 重复使用是否会成功;
  • Redis 不可用时认证流程怎样响应。

旁路缓存

  • 首次查询是否回源并写入;
  • 再次查询是否命中;
  • 数据更新后缓存是否失效;
  • 并发未命中是否造成集中回源;
  • 数据不存在和数据库异常是否被正确区分。

热点与故障

  • 单个热点键失效时数据库压力如何变化;
  • 大量键同时过期时是否触发保护;
  • Redis 短暂不可用时是否发生重试放大;
  • 恢复后旧值是否继续存在;
  • 指标能否观察命中率、延迟、错误和回源量。

没有这些验证,仅仅加入 Redis 还不能说明性能已经提升,也得不出命中率、响应时间或承载能力。

接下来还没有接通的部分

目前 Redis 已用于验证码、刷新令牌、角色权限等认证状态,相关逻辑包含读取、写入、TTL 和主动删除;公共模块也提供了常用键值与集合操作。

接下来还需要逐项实现和验证:

  • 为视频详情接入旁路缓存;
  • 为首页热门列表设计并实现 Redis 预热;
  • 接通本地缓存、Redis 与数据库之间的多级读取和失效;
  • 实现数据库更新后的缓存补偿;
  • 接通视频互动计数从 Redis 到数据库的可靠同步;
  • 完成缓存命中率和性能变化测试。

因此,缓存一致性的起点不是选择一个漂亮的架构名,而是先确定事实来源和数据生命周期:哪些数据可以丢、哪些可以重建、哪些更新后必须失效、哪些允许短暂旧值。只有代码路径、并发竞态和故障场景都经过验证,Redis 才真正减少压力,而不是给系统增加一份难以判断真假的状态。

现在已有 14 次阅读,0 条评论,0 人点赞
作者:语之溪
作者
Redis 缓存一致性:从热点数据到失效策略
当前文章累计共 4503 字,阅读大概需要 8 分钟。
Docker 容器开机自动启动
2023年7月3日 - 0评论
我如何梳理一个微服务系统
2026年3月21日 - 0评论
从竞赛业务转向教育数据中台
2024年9月21日 - 0评论
评论:共0条
发表
搜索 消息 足迹 排行
你还不曾留言过..
你还不曾留下足迹..
博主 不再显示
博主
未知作品 歌曲封面
立即安装