一站式解决缓存与数据库数据一致性
2025-05-24 09:07 阅读(41)

引言

后端面试的时候,面试官常问如何保证缓存与数据库数据的一致性,很多同学都是东答一句西答一句,很少有回答较完整的,那么今天我们就一起总结一下,如何全面地回答这个问题。

Cache Aside Pattern

即旁路缓存模式


当读取数据时,首先检查缓存中是否存在所需的数据。如果存在,则直接返回;如果不存在,则从数据库中读取数据,更新到缓存后再返回给客户端。

当写入或更新数据时,先更新数据库,然后使缓存中的旧数据失效(即删除缓存中的对应数据)。这种方式避免了直接更新缓存可能带来的复杂性和潜在问题。


简单来说,就是大家常听到的先更新数据库,后删除缓存,但是这种方案仍有较小概率出现脏缓存问题(并发前提下)


延迟双删优化

即在旁路缓存的基础上,优化成,先删除缓存,再更新数据库,最后延迟合适的时间再删除一遍缓存,防止脏缓存出现。延迟时间需要根据业务逻辑的执行时间和读取数据库及写入缓存的时间来评估,以确保在延迟时间内,其他请求可以完成数据库读取和缓存写入操作。

但延迟双删也有其缺点:


并非强一致性:即使使用延迟双删,仍然不能完全避免Redis脏数据的问题,只能缓解。要完全解决,可能需要使用同步锁或在业务逻辑层进行处理 

操作失败处理:删除缓存操作有可能会失败,如果第二次删除缓存失败,可以考虑重试机制或使用消息队列进行异步同步 


分布式锁更新缓存

谈到同步问题必定想到锁。但是有些同学会想到用synchronized,Reentrantlock等Java提供的锁机制来实现更新缓存,这就是某些同学的知识盲区了,对分布式锁理解不够到位。像 synchronized 和ReentrantLock 这样的锁机制是Java语言层面提供的同步工具,主要用于在同一JVM实例内的线程间进行同步控制,在分布式系统中,服务通常部署在多台机器上,每台机器都有自己的JVM实例。因此,需要一种能够在不同JVM之间进行协调的锁机制,分布式锁便是为此而生的。

“没有什么是加一层中间层解决不了的,如果有就多加一层”,多个客户端通过网络向这个中间层发起请求。我们可以使用Redis或者ZooKeeper来实现分布式锁这一功能。以Redis为例子,用Redis的SetNx功能来做分布式锁(最好带过期时间,防止系统无响应问题),当一个写线程来更新数据库,便尝试获得该独占锁,获得锁成功之后,其他线程其他试图更新相同数据的服务实例将被阻塞,直到锁被释放。更新数据库和更新缓存是由同一个线程来进行的,这样就保证了强一致性,但是也带来了性能较差的问题。

为什么是更新数据库,也一起更新缓存,而不是由其他线程来读来重建缓存?即使采用“删除缓存 + 等待其他线程读取重建缓存”的策略,仍然存在产生脏数据的可能性。



MQ逐步更新数据库

我们可以借助MQ来帮我们实现数据库与缓存之间最终一致性。引入一个秒杀场景来方便讲解,当大量请求打到Redis缓存时,我们先用decr + 滑动窗口实现库存在Redis的快速扣减,一旦扣减成功就将相应信息存进MQ,让MQ削峰减流,从而让订阅该消息的消费者通过监听器逐步更新数据库。如果Redis库存已经扣减到0,可以借助MQ发送直接让数据库存更新为0的情况,进一步减少数据库的压力。

可以借助MQ的事务消息,分布式事务或者本地消息表等方案进一步保证消费者一定会消费到消息,解决消息丢失,消费幂等等问题。

这种方案性能非常高,而且可以保证最终一致性,但这种方案不适应强一致性的金融等场景,而且也增加了实现难度和维护难度。


binlog + Canal

使用 binlog + Canal 实现 MySQL 和 Redis 数据一致性是一种常见的、相对可靠的方案。其核心思想是:Canal 模拟 MySQL Slave,伪装成 MySQL 的从节点,从 MySQL Master 接收 binlog 日志,然后解析 binlog,将数据变更同步到 Redis。

工作原理



MySQL 开启 binlog:


确保 MySQL 开启了 binlog 功能,并且 binlog 的格式设置为 ROW 模式。这是 Canal 能够正常工作的前提。




Canal 模拟 Slave:


Canal 伪装成 MySQL 的 Slave 节点,向 MySQL Master 发起连接请求。




MySQL Master 推送 binlog:


MySQL Master 接收到 Canal 的请求后,会将 binlog 日志推送给 Canal。




Canal 解析 binlog:



Canal 接收到 binlog 日志后,会按照配置规则解析 binlog,提取出数据变更的详细信息,例如:


哪个数据库的哪个表发生了变更

变更的类型(INSERT、UPDATE、DELETE)

变更前后的数据






Canal 同步数据到 Redis:



Canal 根据解析出的数据变更信息,将数据同步到 Redis。同步的方式可以根据实际需求进行选择,例如:


更新缓存:将变更后的数据更新到 Redis 中。

删除缓存:删除 Redis 中对应的数据。






应用程序读取 Redis:


应用程序从 Redis 中读取数据,如果 Redis 中没有数据,则从 MySQL 中读取,并将数据写入 Redis。




这种方案也有其优缺点:

优点


实时性高:Canal 能够实时地将 MySQL 的数据变更同步到 Redis,保证了数据的一致性。

可靠性高:Canal 模拟 MySQL Slave,能够可靠地接收 binlog 日志,避免了数据丢失。

可扩展性强:Canal 支持多种数据同步方式,可以根据实际需求进行选择。

解耦:将数据同步的逻辑从应用程序中分离出来,降低了应用程序的复杂度。


缺点


实现复杂:需要安装、配置和维护 Canal 服务。

存在延迟:虽然 Canal 能够实时地同步数据,但仍然存在一定的延迟。

需要考虑 Canal 的高可用:为了保证 Canal 服务的可用性,需要考虑 Canal 的高可用方案。

需要监控Canal:需要对 Canal 服务进行监控,及时发现和解决问题


总结

这是后端面试常问的问题,建议各位结合自己的项目进行回答,面试官问你的优化方法也可以有更多的思路。如果你看了这篇文章有收获可以点赞+关注+收藏,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!


作者:想用offer打牌

链接:https://juejin.cn

https://www.zuocode.com