https://chat.deepseek.com/share/ce5pvlaaj32ar3gyxt
面试记录 2026-09-20
题目:什么情况下会出现数据库和缓存不一致的问题?
难度:medium
标签:db/redis/cache, general/knowledge
评分:⭐⭐⭐⭐☆(4/5)
我的回答
数据库缓存不一致,核心原因就是非原子性数据库跟缓存的更新。然后非原子性造成问题,衍生的也就是并发问题,在并发情况下,在并发状态下,数据的更新由于不同线程的不同操作,导致相互的不一致。另外就是单个线程内,数据库或者缓存的更新失败,另一者更新成功导致的数据库与缓存不一致,其实也是原子性的问题导致的。一般处理这种问题主要是原子性的操作或者是补偿性的操作,然后最终的话可能是选择强一致或者最终一致性来处理这个问题;正常是先更新数据库,数据库更新完后删除缓存,然后异步的或者是延迟的再去删除一次缓存,确保数据库数据读取到缓存中是一个新值的数据。当然这块可能通过一些异步的方案去数据库读数据库更新日志,读 binlog 日志,然后去通过买四,通过其他的工具去异步的执行缓存删除。然后缓存更新的话,基本上都是通过二次调用去读取的。这里边会有一些衍生问题,比如主从数据库。二者会有一定的时间延迟,主库更新后,从库没有读取到,但是缓存已经更新了,而读一般是从从库来的,这时候就导致主库的数据和缓存数据不一致,也会产生数据库缓存不一致问题。常规的最处理最常见的处理方式就是更新完数据库删缓存,然后另外给缓存设一个相对合理的过期时间,哪怕数据出现了问题不一致,也可以正常的在一定时间周期后恢复正常的数据。
点评
优点:
- 核心根因抓得准:非原子性 + 并发 + 单线程局部失败,三个层次清晰
- 主动提到延迟双删:先更新 DB → 删缓存 → 延迟再删一次
- 主动提到 binlog 异步消费(Canal 类工具):工程意识强
- 主动提到主从延迟:很多候选人会漏掉
- 给了兜底方案:TTL 作为最终一致性保底
不足:
- 没系统列举 6 大经典不一致场景(更新后删失败 / 删后更新失败 / 并发 race / 先删后更 / 先更后删 / 主从延迟)
- 没解释为什么"更新缓存"不推荐(并发写时先算旧值覆盖新值,删除更安全)
- 没提 Cache-Aside 模式名
- 强一致方案未展开:Binlog(Canal)+ MQ 重试 / CDC / 事务性消息 / Seata
- "延迟双删"的延迟值没给(经验 500ms-1s,覆盖主从同步周期)
- "二次调用去读取"表述模糊
补充:
- 6 大不一致场景结构化列举
- 推荐模式 Cache-Aside(读时回填 + 写时删缓存)
- 删除优于更新:并发写时更新缓存会 race,删除让下次读自然回填
- 延迟双删延迟 500ms-1s(覆盖主从同步周期)
- 强一致方案:Binlog 订阅(Canal)+ MQ 重试 / 事务性消息 / CDC / Seata
- 兜底:TTL 是最终一致性的安全网
完整讲解
第一部分:知识重点剖析
核心考点定位:考察对不一致根因的枚举能力 + 解决方案的层次选型能力,而非只答一个"延迟双删"。
知识点拆解:
- 不一致的 6 大场景:
- 更新 DB 后删除缓存失败:脏数据驻留到 TTL
- 删除缓存后更新 DB 失败:下次读回填旧值
- 并发 race:读线程读旧值 → 写线程更新 DB 删缓存 → 读线程把旧值写回缓存
- 先删缓存再更新 DB:删除后并发读回填旧值
- 先更新缓存再更新 DB:DB 更新失败,缓存是新值 DB 是旧值
- 主从延迟:主库写完从库未同步,读走从库拿旧值
- 三大经典模式对比:
- Cache-Aside(推荐):先读缓存 miss 读 DB 回填;更新 DB → 删缓存
- Read-Through:缓存 miss 由缓存层回填;更新缓存 → 缓存层写 DB
- Write-Through:同步写缓存 + DB
- Write-Behind:异步批量写 DB
- 为什么"删除缓存"优于"更新缓存":更新缓存在并发写时会有旧值覆盖新值的 race,删除让下次读自然回填最新值
- 延迟双删正确姿势:更新 DB → 删缓存 → 延迟 500ms-1s(覆盖主从同步周期)→ 再删一次;实现用线程池延迟任务或 MQ 延迟消息
- 强一致性方案(按侵入性从小到大):
- Binlog 订阅(Canal):消费 binlog → MQ → 删缓存,业务无侵入(最推荐)
- CDC 工具:Debezium / Flink CDC
- 事务性消息:RocketMQ 半消息
- 分布式事务:Seata AT/TCC,侵入大不推荐
- 锁住整个流程:性能差
- 兜底策略:TTL 是最终一致性的安全网;监控不一致率(定时抽样对比)
- 工程实践清单:
- 关键数据(订单、余额):Binlog + MQ 强一致
- 一般数据(商品、配置):Cache-Aside + 延迟双删 + TTL
- 只读数据:Read-Through + 长 TTL
- 高频写场景:Write-Through
第二部分:考察方向与延伸递进
考察方向:
- 深度:Canal 如何保证不丢消息?事务性消息的回查机制?
- 广度:与 Kafka Exactly-Once、Seata TCC、分布式锁对比
- 应用:订单、库存、账户余额等场景的方案选型
延伸递进面试题:
- Canal 如何保证不丢消息?
- 解答要点:offset 落盘 + 断点续传 + MQ 至少一次投递
- 延迟双删的 N 值怎么定?
- 解答要点:主从延迟监控 P99 + 业务读放大系数;经验 500ms-1s
- 读写分离下如何避免读到旧值?
- 解答要点:强制走主库读(有写会话);session sticky;读前等待 binlog 同步
- Seata AT 模式原理?为什么侵入小?
- 解答要点:undo_log + 全局锁 + 一阶段本地事务二阶段补偿
- 如何监控缓存不一致率?
- 解答要点:定时抽样对比 DB 与缓存;埋点上报不一致次数;阈值告警
- 库存扣减场景如何保证一致性?
- 解答要点:Redis Lua 原子扣减 + MQ 事务消息 + DB 最终落库 + 对账补偿
第三部分:面试者参考回答(300-600 字口语化)
面试官您好,DB 与缓存不一致的根因是非原子性 + 并发,具体有 6 类场景:
场景 1:更新 DB 后删缓存失败,脏数据驻留到 TTL。场景 2:删缓存后更新 DB 失败,下次读回填旧值。场景 3:并发 race——读线程读到旧值,写线程更新 DB 删缓存,读线程把旧值写回缓存,脏数据长期驻留。场景 4:先删缓存再更新 DB,删除后并发读回填旧值。场景 5:先更新缓存再更新 DB,DB 失败则缓存是新值 DB 是旧值。场景 6:主从延迟,主库写完从库未同步,读走从库拿旧值。
模式选型:我们通用场景用 Cache-Aside(读时回填、写时删缓存),删除比更新安全——并发写时更新缓存会有旧值覆盖新值的 race,删除让下次读自然回填最新值。
延迟双删:更新 DB → 删缓存 → 延迟 500ms-1s(覆盖一次主从同步周期)→ 再删一次,清掉并发读回填的脏值。实现用线程池延迟任务或 MQ 延迟消息。
强一致场景(订单、余额):用 Binlog 订阅(Canal)消费 binlog → MQ → 删缓存,业务无侵入;或者用 RocketMQ 事务性消息,DB 事务与消息发送原子;Seata AT/TCC 侵入大不推荐。
兜底:TTL 是最终一致性的安全网,业务可容忍的脏数据时间窗(订单 5min、商品详情 1h)。配合不一致率监控:定时抽样对比 DB 与缓存,超阈值告警。
实际项目我做过一个坑:一开始只用了"更新 DB + 删缓存",压测发现偶发脏数据。查下来是主从延迟导致读从库拿旧值。加了延迟双删(延迟 1s)后问题解决。后来核心订单场景改成了 Canal + MQ 强一致方案。