在 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 才真正减少压力,而不是给系统增加一份难以判断真假的状态。