Redis缓存一致性:从面试“翻车”到系统设计高手

4 阅读5分钟

缓存一致性:从面试“翻车”到系统设计高手

🎭 面试场景还原

候选人:“缓存一致性?这个我研究过,应该是先删缓存,再更新数据库”

面试官:“如果删缓存成功,但更新数据库失败了呢?”

候选人:“呃...那换成先更新数据库,再删缓存?”

面试官:“如果数据库更新成功,但删缓存失败了呢?”

候选人:“这...”(开始冒汗)

面试官:“我们聊聊缓存一致性的几种方案吧...”


📊 4种核心方案对比

方案一:先更新DB,后删缓存(Cache-Aside)

┌─────────┐    1.更新DB    ┌─────────┐    2.删除缓存    ┌─────────┐
│  客户端  │ ────────────> │ 数据库  │ ────────────> │  缓存   │
└─────────┘               └─────────┘               └─────────┘

优点

  • 简单直接,业界常用
  • 保证数据最终一致性

问题

  • 删缓存失败 → 缓存里是旧数据
  • 并发读可能导致脏数据:
    线程A: 更新DB (value=2)
    线程B:           读缓存(没命中) → 读DB(读到旧值1) → 写缓存(1)
    线程A:               删除缓存
    → 结果:缓存=1 (脏数据)
    

方案二:先删缓存,后更新DB

┌─────────┐    1.删除缓存    ┌─────────┐    2.更新DB    ┌─────────┐
│  客户端  │ ────────────> │  缓存   │ ────────────> │ 数据库  │
└─────────┘               └─────────┘               └─────────┘

优点

  • 确保后续读请求一定从DB读最新数据

问题

  • 更新DB失败 → 缓存已删,下次读会穿透到DB
  • 并发问题更严重:
    线程A: 删除缓存
    线程B:           读缓存(没命中) → 读DB(旧值) → 写缓存(旧值)
    线程A:               更新DB(新值)
    → 结果:缓存=旧值,DB=新值
    

方案三:延时双删

┌─────────┐    1.删除缓存    2.更新DB    3.等待    4.再删缓存
│  客户端  │ ────────────> ──────────> [延时] ──────────>
└─────────┘

执行步骤

  1. 先删除缓存
  2. 更新数据库
  3. 等待一段时间(比如500ms)
  4. 再次删除缓存

为什么等待?

  • 给并发读请求时间写完旧的脏数据
  • 等待时间 = 一次读耗时 + 一次写耗时

优点

  • 显著降低脏数据概率

缺点

  • 延迟高,性能影响
  • 等待时间不好确定
  • 第二次删除可能失败

方案四:监听binlog(推荐方案)

┌─────────┐    写DB    ┌─────────┐    binlog    ┌─────────┐    删缓存
│  客户端  │ ────────> │ MySQL  │ ──────────> │ Canal   │ ────────>
└─────────┘           └─────────┘              └─────────┘
                             异步消息队列 ──────────> 删除缓存

工作原理

  1. 正常更新数据库
  2. 数据库的binlog(变更日志)被监听
  3. 通过消息队列异步删除缓存

优点

  • 解耦业务和缓存操作
  • 保证最终一致性
  • 可重试机制

缺点

  • 架构复杂
  • 有一定延迟

🏆 面试最佳回答模板

“缓存一致性没有银弹,需要根据业务场景选择”

第一步:明确需求

  • 需要强一致性还是最终一致性?
  • 读多写少还是写多读少?
  • 数据敏感性如何?

第二步:分层回答

基础方案(中小系统)

“对于读写比不高、一致性要求不强的场景,我推荐 先更新数据库,再删除缓存,配合以下几点保证:

  1. 重试机制:删除失败时重试3次
  2. 设置缓存过期时间:兜底方案,比如30分钟
  3. 记录操作日志:用于问题排查和修复”
进阶方案(大型系统)

“对于高并发、一致性要求高的场景,我建议:

  1. 监听binlog + 消息队列 作为主方案
  2. 延时双删 作为补充
  3. 多级缓存策略:本地缓存 + Redis,设置不同过期时间
  4. 补偿任务:定时扫描数据库和缓存差异,修复不一致”

第三步:展示设计思维

“我还会考虑:

  • 降级方案:缓存大面积失效时如何保护DB
  • 监控告警:缓存不一致率超过阈值时告警
  • 灰度发布:新策略先在小流量验证”

🎯 不同业务场景建议

场景一:用户个人信息(弱一致性可接受)

  • 方案:先更新DB,后删缓存 + 缓存过期时间(30分钟)
  • 理由:用户自己修改,看到旧信息一会儿影响不大

场景二:商品库存(强一致性要求)

  • 方案:监听binlog + 同步更新缓存
  • 理由:超卖会直接造成经济损失,必须强一致

场景三:文章阅读量(最终一致性可接受)

  • 方案:先更新DB,异步更新缓存
  • 理由:阅读量有少量误差可以接受,性能优先

场景四:秒杀库存(超高并发)

  • 方案
    1. Redis原子操作扣减库存
    2. 异步同步到数据库
    3. 库存变化时通过消息队列更新缓存
  • 理由:性能压倒一切,允许短暂不一致

💡 面试加分项

1. 主动提出边缘情况

“除了刚才讨论的,还有一些特殊情况需要考虑:

  • 分布式锁的使用:防止并发更新
  • 缓存穿透/雪崩的防护:空值缓存、热点Key分散
  • 多级缓存同步:本地缓存如何更新”

2. 展示实战经验

“我在上个项目中遇到过缓存不一致问题,我们的解决方案是:

  1. 先采用延时双删,但发现延迟太高
  2. 改为binlog方案,不一致率从0.1%降到0.001%
  3. 增加了缓存版本号,支持灰度回滚”

3. 提出未来优化

“如果进一步优化,我会考虑:

  • 引入缓存数据中心(CDC)统一管理
  • 使用一致性哈希减少缓存抖动
  • 实现缓存预热和热点探测”

📝 一句话总结

“缓存一致性是在性能、复杂度、一致性之间的权衡,没有完美方案,只有适合场景的方案”


记住:面试官不只是想听正确答案,更想看到你的思考过程和设计能力。从简单方案说起,逐步深入,展示你处理复杂问题的能力,这样即使最初回答不完美,也能最终赢得认可!