每日Java面试场景题知识点之-数据库与缓存的一致性

每日Java面试场景题知识点之-数据库与缓存的一致性

一、为什么需要数据库与缓存的一致性?

在现代Java企业级应用中,为了提升系统性能和吞吐量,几乎都会引入缓存层(如Redis)。缓存的核心思路是将热点数据存储在内存中,减少对数据库的直接访问。然而,缓存和数据库是两个独立的数据存储,如何保证它们之间的数据一致性,成为了分布式系统中的经典难题。

面试中,考官往往会从以下几个维度来考察:

  • 你是否理解缓存与数据库不一致的根本原因?
  • 你能否设计出合理的缓存更新策略?
  • 你对最终一致性有没有深刻的理解?

二、经典的缓存更新策略

2.1 Cache Aside Pattern(旁路缓存模式)

这是业界最常用的缓存更新策略,核心思想是:应用程序同时与缓存和数据库交互,缓存不主动与数据库通信。

读流程:

  1. 应用程序先读缓存;
  2. 如果缓存命中,直接返回数据;
  3. 如果缓存未命中,读取数据库,将数据写入缓存,再返回。

写流程:

  1. 先更新数据库;
  2. 再删除缓存。

为什么是删除缓存而不是更新缓存?因为更新缓存可能引发并发写导致的脏数据问题,而删除缓存是幂等操作,更加安全。

2.2 Read/Write Through Pattern(读写穿透模式)

应用程序只与缓存交互,由缓存层负责与数据库的读写同步。这种方式对业务代码侵入性低,但实现复杂度较高。

2.3 Write Behind Pattern(异步回写模式)

应用程序只更新缓存,由缓存层异步批量地将数据写入数据库。这种方式写入性能极高,但存在数据丢失的风险。


三、先更新数据库还是先删缓存?

这是面试中最高频的问题之一,我们需要分析两种方案的并发问题:

3.1 先删缓存,再更新数据库

并发问题场景:

  1. 线程A删除缓存;
  2. 线程B读缓存未命中,读数据库拿到旧值;
  3. 线程B将旧值写入缓存;
  4. 线程A更新数据库为新值。

结果: 缓存中是旧值,数据库中是新值,数据不一致!

解决方案:延迟双删

java 复制代码
public void updateData(String key, Object value) {
    // 第一步:先删除缓存
    cache.delete(key);
    // 第二步:更新数据库
    db.update(key, value);
    // 第三步:延迟一段时间后再次删除缓存
    Thread.sleep(500); // 延迟时间需大于一次读操作的耗时
    cache.delete(key);
}

延迟双删的核心思想是:在第二次删除缓存之前,等待足够长的时间,让其他线程将旧值写入缓存的操作完成,然后再将脏数据清除。

3.2 先更新数据库,再删缓存(推荐)

并发问题场景:

  1. 缓存刚好失效;
  2. 线程A读数据库拿到旧值;
  3. 线程B更新数据库为新值;
  4. 线程B删除缓存;
  5. 线程A将旧值写入缓存。

分析: 这种场景发生的概率极低,因为步骤3和4的时间消耗远大于步骤2和5,线程B通常会在线程A写缓存之前完成删除操作。所以这种策略在实际生产中被广泛推荐使用。


四、缓存删除失败怎么办?

不管采用哪种策略,如果最后一步操作(删除缓存或更新数据库)失败,就会导致数据不一致。以下是常见的补偿机制:

4.1 消息队列重试

将删除缓存的操作放入消息队列,如果删除失败,消费者会自动重试,直到成功为止。

java 复制代码
public void updateData(String key, Object value) {
    db.update(key, value);
    // 发送消息到MQ,异步删除缓存
    mq.send("cache-delete-topic", key);
}

4.2 订阅Binlog异步删除

通过Canal等中间件监听MySQL的Binlog日志,一旦检测到数据变更,异步删除对应的缓存。这种方式对业务代码零侵入,是目前大型互联网公司的主流方案。

架构示意:

MySQL -> Canal监听Binlog -> 发送至MQ -> 消费者删除Redis缓存

4.3 设置缓存过期时间

为缓存数据设置合理的TTL(Time To Live),即使出现不一致的情况,缓存过期后也会自动从数据库加载最新数据。这是一种兜底策略,保证最终一致性。


五、强一致性 vs 最终一致性

5.1 强一致性

如果要保证缓存和数据库的强一致性,通常需要引入分布式锁或分布式事务,例如:

  • 使用Redisson的读写锁;
  • 使用Seata等分布式事务框架。

但强一致性方案会显著降低系统性能,在绝大多数互联网场景中并不必要。

5.2 最终一致性(推荐)

对于绝大多数业务场景,最终一致性才是合理的选择。核心思路是:

  1. 采用先更新数据库、再删除缓存的策略;
  2. 通过消息队列或Binlog订阅保证删除操作的可靠性;
  3. 设置缓存过期时间作为兜底;
  4. 接受短暂的不一致窗口期。

六、面试高频追问

Q1:缓存雪崩、缓存穿透、缓存击穿分别是什么?如何解决?

  • 缓存雪崩:大量缓存同时失效,请求全部落到数据库。解决方案:缓存过期时间加随机值、缓存预热、限流降级。
  • 缓存穿透:查询不存在的数据,缓存永远不命中。解决方案:布隆过滤器、缓存空值。
  • 缓存击穿:某个热点key过期瞬间,大量并发请求打到数据库。解决方案:互斥锁、热点数据永不过期。

Q2:如何保证缓存的高可用?

  • Redis集群(主从+哨兵或Cluster模式);
  • 本地缓存+分布式缓存多级架构;
  • 缓存降级策略。

Q3:在Spring Boot中如何实现缓存与数据库的一致性?

  • 使用Spring Cache注解(@Cacheable、@CacheEvict、@CachePut);
  • 结合消息队列实现异步缓存失效;
  • 整合Canal监听Binlog自动更新缓存。

七、总结

数据库与缓存的一致性是分布式系统设计的核心问题之一。在实际生产中,推荐的做法是:先更新数据库、再删除缓存 ,并结合消息队列重试Binlog异步订阅 来保证删除操作的可靠性,同时设置缓存过期时间作为兜底策略,实现最终一致性。对于特殊场景(如金融交易),才需要考虑强一致性方案。

掌握这些知识,不仅能帮助你通过面试,更能在实际项目设计中做出合理的技术决策。

感谢读者观看

相关推荐
utf8mb4安全女神1 小时前
⽇志管理与深层防⽕墙
java·开发语言·spring boot
减瓦1 小时前
Jackson 自定义反序列化器的类型不匹配陷阱
java·后端
light blue bird1 小时前
工序路径主子表单工序组装图表组件
前端·数据库·信息可视化·.net·web端·razor page
我叫张小白。1 小时前
基于Redis与FastAPI的分布式共享会话体系
数据库·redis·分布式·缓存·中间件·fastapi·依赖注入
qq_452396231 小时前
第九篇:《Dockerfile 指令精讲(二):WORKDIR、ENV、ARG、EXPOSE》
java·开发语言·docker
代码旅人ing1 小时前
Redis+Spring+MyBatis + 微服务 + 消息队列核心知识点(面试高频题目合集)
redis·spring·mybatis·java-rabbitmq
JAVA社区1 小时前
Java高级全套教程(九)—— SpringCloud超详细实战详解
java·开发语言·后端·spring cloud·面试·职场和发展
java_cj1 小时前
MySQL 8.0新特性详解:从隐藏索引到窗口函数全面解析
数据库·mysql·架构·开源
数据库安全1 小时前
业务可用、数据可控:美创“动态脱敏+数据库透明加密“合规方案
数据库