首次来源:约牛科技 2026-01-12 一面
缓存与数据库数据一致性问题如何处理?你在实际项目中是怎么解决的?
最后更新:2026-10-29 · 基于第 1 次作答
结论:一致性本质是「读缓存 + 写库删缓存」的取舍,接受短暂不一致、消灭长期不一致;写后立即读强一致用「先写库再删缓存 + 延迟双删 + MQ 补偿」。 关键词:Cache Aside · 延迟双删 · 先更新再删 · 补偿 · 最终一致 边界:并发下「读旧值回填缓存」会覆盖新值,需延迟双删或版本号;删除失败必须补偿,否则长期不一致。
核心考点定位:面试官想看你对「缓存一致性」是否有工程化理解,而不是背"先删缓存再更新库"——要能说清并发竞态、删除失败、写后立即读的三种典型坑,并给出补偿闭环。
机制链(≥3 环):
SETNX 互斥或逻辑过期,避免热点 key 过期瞬间压垮数据库。证据锚点(去重 ≥12):
DEL → 更新库 → sleep(ms) → DEL,sleep > 读回填耗时。SET key val EX ttl:设 TTL 作为最终兜底,防缓存永不过期。CAS:UPDATE ... SET version=version+1 + 缓存带版本,旧版本回填被拒绝。retry + DLQ 兜底。SETNX 互斥)或布隆过滤器。@CacheEvict/@CachePut:Spring Cache 注解;@CachePut 更新缓存,@CacheEvict 删除。canal/CDC:订阅 binlog 异步失效缓存,解耦应用层。SETNX key 互斥 + 逻辑过期:防缓存击穿(热点 key 过期瞬时压库)。TTL 加随机抖动。对比与权衡:
| 方案 | 一致性 | 复杂度 | 适用 |
|---|---|---|---|
| 先更新库再删缓存 + TTL | 最终一致 | 低 | 默认、读多写少 |
| 延迟双删 | 较强 | 中 | 写后立即读、并发高 |
| 版本号 CAS | 强 | 中高 | 需严格不覆盖 |
| binlog CDC 失效 | 强 | 高 | 复杂缓存链路、多级缓存 |
| MQ 补偿删 | 最终一致 | 中 | 删除易失败场景 |
| 只设 TTL 不删 | 弱 | 低 | 可容忍长时间不一致 |
常见误区:
@CachePut 更新缓存就一致 → 正确:@CachePut 先写后读可能读到旧库值,更新链路以「删缓存」为准 → 为什么错:混淆「更新缓存」与「失效缓存」语义。适用边界:强一致需求(写后立即读自己刚写的)用「写库 + 删缓存 + 延迟双删」并配合 TTL;多级缓存(本地 Caffeine + Redis)或复杂联动失效,用 binlog CDC 或 MQ 广播统一失效;只读高频、写极少的热点,可退化到「TTL 过期」简单模型并接受秒级不一致。若删除缓存与更新库之间系统崩溃,靠 TTL 过期兜底;若读回填与删缓存竞态频繁,优先版本号 CAS 而非无限延迟双删。
sleep 取多少、依据是什么。SETNX 互斥回源防缓存击穿。publish/canal)。SETNX 互斥、逻辑过期、随机 TTL。【结论】 缓存一致性本质是取舍:用了缓存就要接受短暂不一致,但必须消灭长期不一致;标准做法是 Cache Aside——读缓存、未命中读库回填,写库后删缓存,配合延迟双删和补偿。
【三点】 第一,更新顺序:先更新数据库,再删缓存。先删后更新会在窗口内被读线程回填旧值,导致长期不一致。第二,处理并发竞态:单次删除不够,因为删完后可能立刻被并发读回填旧值,所以用延迟双删(删除 → 更新库 → 短暂延迟 → 再删一次),或用版本号 CAS(UPDATE version=version+1,缓存带版本,旧版本回填被拒绝)。第三,删除失败必须补偿:删除可能超时或失败,不能只记日志,要发 MQ 异步重试删除、兜底 DLQ,并给缓存设 TTL 作为最终收敛保证。
【边界】 我在实际项目里的取舍是:核心链路用「更新库 + 删缓存 + TTL」,有强一致要求(写后立即读)的加延迟双删;复杂多级缓存或联动失效(一个底层变动影响很多上层数据)用 MQ 广播统一删缓存,避免每层各自失效不一致。缓存本质是性能优化,别为追求强一致把成本做到失衡;只读高频写极少的场景,甚至可以退化到纯 TTL 过期模型接受秒级不一致。
题目:缓存与数据库数据一致性问题如何处理你在实际项目中是怎么解决的
难度:high
标签:db/redis/advanced, real-interview, difficulty/high
评分:⭐⭐⭐⭐☆(4/5)
练习次数:1
缓存和数据库一致性问题的核心是取舍;
都用缓存了,本身你就要接受短暂的不一致,主要不要是业务层级长期的不一致;
实际项目中主要还是ttl缓存过期,数据更新的时候,移除缓存等待下次拉取;
有强一直的要求都话,做过数据更新后,手动拉取写入缓存的,不过很少;
触发方面如果是涉及多缓存更新,比如之前做的一个需求是很多联动性的内容,当底层数据变动的时候会影响很多上层数据,当时用的是更新数据的时候发消息到mq,响应的要更新的服务去监听消息,做缓存删除;
优点:抓住本质(接受短暂不一致、消灭长期不一致)+ Cache Aside(TTL 过期 + 更新删缓存)+ 强一致手动回写 + MQ 联动多级失效,工程化取舍清晰。
不足:未提延迟双删、删除失败补偿(MQ 重试/DLQ)、版本号 CAS 防旧值回填。
补充:并发下"删完又回填旧值"需延迟双删或版本号;删除失败必须补偿否则长期不一致;先更新库再删缓存。
完整讲解见上方「完整讲解(权威版)」