程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

深入Redis系列十一如何解决缓存和数据库的数据不一致问题缓存与数据库的双写策略

深入Redis系列(十一)如何解决缓存和数据库的数据不一致问题?缓存与数据库的双写策略

只要使用 Redis 缓存,就必然会面对缓存和数据库间的一致性保证问题,如果数据不一致,那么业务应用从缓存中读取的数据就不是最新数据,这会导致严重的错误。

这篇文章主要聊聊数据库和缓存的数据一致性问题,包括:

1. 缓存和数据库为什么会发生数据不一致的情况;

2. 缓存和数据库的数据同步如何实现(同步直写和异步回写);

3. 保证缓存和数据库一致性的解决方案有哪些;

01 缓存和数据库为什么会数据不一致

缓存和数据库的数据不一致是如何发生的?

首先这里的“数据的一致性”包含了两层含义:

1. 缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;

2. 缓存中本身没有数据,那么,数据库中的值必须是最新值。

不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。

为了研究数据库和缓存数据不一致是如何发生的,我们首先要知道数据库和缓存的数据写入策略。

在数据库和缓存写入场景中,同步直写策略和异步写回策略是两种常见的策略,它们各自具有不同的特点和适用场景。

同步直写策略

同步直写策略指的是在更新数据库的同时,也同步更新缓存中的数据。这种策略能够确保缓存中的数据与数据库中的数据始终保持一致。

优点:

数据一致性高:由于缓存和数据库同时更新,因此能够确保数据的一致性。

实时性强:对于需要实时反映数据变化的场景,同步直写策略能够迅速更新缓存中的数据。

缺点:

性能开销大:同步更新缓存和数据库会增加系统的性能开销,特别是在高并发场景下,可能会导致系统性能下降。

复杂性高:实现同步直写策略需要确保缓存和数据库之间的数据同步机制,增加了系统的复杂性。

异步写回策略

异步写回策略则指的是在更新数据库后,并不立即更新缓存,而是读取缓存时发现缓存中不存在这个数据则先读数据库再写入缓存。

下图是异步回写策略的过程。

这种策略通常用于对数据一致性要求不是特别高的场景。

优点:

性能开销小:由于不需要立即更新缓存,因此能够减少系统的性能开销。

灵活性高:异步写回策略允许系统在高并发场景下更加灵活地处理数据更新。

缺点:

数据一致性较低:由于缓存中的数据不是实时更新的,因此可能会存在一段时间的数据不一致性。

实现复杂度较高:需要确保数据在适当的时间点写回到缓存中,同时还需要处理缓存失效和数据回写失败等异常情况。

适用场景

  • 同步直写策略:适用于对数据一致性要求非常高的场景,如金融交易系统、实时数据分析系统等。在这些场景中,数据的准确性至关重要,因此需要确保缓存和数据库中的数据始终保持一致。

  • 异步写回策略:适用于对数据一致性要求不是特别高的场景,如社交媒体、电商网站等。在这些场景中,数据的实时性不是最重要的,更重要的是系统的性能和可扩展性。因此,可以采用异步写回策略来减少系统的性能开销。

要想保证缓存和数据库中的数据绝对一致,就要采用同步直写策略。不过,需要注意的是,如果采用这种策略,就需要同时更新缓存和数据库。所以,我们要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性,也就是说,两者要不一起更新,要不都不更新,返回错误信息,进行重试。否则,我们就无法实现同步直写。

对于异步写回策略,如果有数据新增,会直接写入数据库;而有数据删改时,就需要把只读缓存中的数据标记为无效。这样一来,应用后续再访问这些增删改的数据时,因为缓存中没有相应的数据,就会发生缓存缺失。此时,应用再从数据库中把数据读入缓存,这样后续再访问数据时,就能够直接从缓存中读取了。

异步回写策略在删改数据的时候可能会存在数据不一致的情况。这里需要分2种情况,那就是业务场景到底是需要缓存与数据库之间的数据的强一致性 还是 最终一致性。

对于强一致性而言,由于读写线程是并发的,不管是先写数据库再删除缓存,还是先删除缓存再写库,都无法保证强一致性,具体情况如下。

1.如果先删除了缓存Redis,但还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时写入缓存中的是脏数据。

2.如果先写了库,在删除缓存前,另一个线程就来读取,就会读取到缓存中的脏数据。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

对于最终一致性而言,由于异步回写策略无法保证删缓存操作和写库操作是原子性的,因此无论先删缓存再写库还是先写库再删缓存,依旧无法保证最终一致性,具体情况如下

1.如果先删除缓存,但写入数据库失败,此时数据库中的数据是脏数据,下次读取数据时优先从缓存读取,但由于缓存为空因此会读取数据库的脏数据。

2.如果先写了库,但删除缓存失败了,此时数据库中的数据是正确的,而缓存中的数据是脏数据,下次读取数据时优先从缓存读取,就会读取到缓存中的脏数据。

综上所述,异步回写策略由于读写并发和缓存与数据库操作无法保证原子性,而导致缓存和数据库数据可能不一致;

上面的同步直写和异步回写都是建立在更新数据库且删除缓存的基础上,实际上我们还可以更新数据库并且将数据原模原样的更新到缓存中,而非采用删除缓存再刷新缓存的这种迂回方式。

下面是几种数据库与缓存的双写策略:

  1. 先更新缓存,再更新数据库;

  2. 先更新数据库,再更新缓存;

  3. 先删除缓存,再更新数据库;

  4. 先更新数据库,再删除缓存;

那么我们到底该采用删除缓存还是更新缓存来实现双写呢?以下是一些建议和参考点。

选择直接更新缓存的 好处

  1. 减少缓存删除而读取数据库的频率:
* 直接更新缓存可以确保缓存中的数据是最新的,避免了因缓存删除而频繁访问数据库,从而提高系统的整体性能。
  1. 避免缓存穿透:
* 缓存穿透是指查询一个不存在的数据,导致缓存未命中而直接访问数据库,且数据库也未命中,导致频繁访问数据库的情况。更新缓存可以确保即使数据不存在(假设在缓存中标记为“不存在”),也能减少此类穿透的发生。
  1. 减少数据访问延迟:
* 相比删除缓存后再等待下一次请求来重新填充缓存,直接更新缓存可以减少数据的访问延迟,因为数据始终在缓存中保持最新状态。

选择直接更新缓存的 ** ** 不足

  1. 复杂性和一致性问题:
* 直接更新缓存可能引入更多的复杂性,特别是在并发写入场景下。如果多个进程或线程同时更新缓存,可能会导致数据不一致的问题。需要实现复杂的同步机制来确保一致性。
  1. 额外的维护成本:
* 直接更新缓存需要额外的逻辑来处理缓存的失效、过期和同步问题。这增加了系统的复杂性和维护成本。

综合考虑

  • 对于读多写少的场景:直接更新缓存是一个不错的选择,因为它可以避免缓存删除而请求数据库的行为,提高系统性能。

  • 对于写多读少的场景:删除缓存可能更为合适,因为读操作较少, 缓存删除而请求数据库的行为 可以保持在可控的范围之内。

  • 并发控制:无论选择哪种策略,都需要考虑并发控制的问题。直接更新缓存需要确保并发更新的一致性,而删除缓存则需要处理并发写入和读取时的数据一致性问题。

总的来说,直接更新缓存和删除缓存各有利弊,选择哪种策略取决于具体的应用场景、性能需求和一致性要求。在实际应用中,可以根据业务需求和系统特点进行权衡和选择。

02 如何解决缓存和数据库不一致问题

假如业务中采用的是同步直写策略,并且通过事务机制来保证缓存和数据库写操作的原子性,那么是不会出现数据不一致的问题的。那么具体该如何保证两者间的写操作是原子性的呢?比较通用的方案是采用分布式锁。

而对于异步回写策略,我们需要根据“实现最终一致性”和“实现强一致性”这两种不同的需求来设计解决方案。

队列 + 重试机制

采用队列 和 重试机制实现数据库和缓存写操作同步可以保证二者间数据的最终一致性。

基本思路

1. 更新数据库:当数据需要更新时,首先确保数据库中的数据被正确更新。

2.记录操作到消息队列:将数据库的操作信息和操作后的值(如更新、删除等)写入到消息队列中。

3. 异步处理:异步消费者从消息队列中读取操作信息,并尝试对缓存进行相应的操作(如删除缓存等)。

4. 重试机制:如果缓存操作失败(如删除缓存失败),则将该操作重新放入消息队列中重试。

具体实现步骤

1. 更新数据库数据

当应用程序需要对数据进行更新时,首先更新数据库中的数据。

数据库更新操作完成后,记录该操作的信息,包括操作类型、操作的数据ID等。

2. 将操作信息放入消息队列

将数据库操作信息封装成消息,并放入消息队列中。

消息队列可以选择高可靠性的消息中间件,如RocketMQ、Kafka等。

3. 异步消费消息并处理缓存

创建一个异步的消费者服务,从消息队列中读取消息。

根据消息中的操作类型和数据ID,对缓存进行相应的操作。例如,如果消息是删除操作,则尝试删除缓存中对应的数据。

4. 实现重试机制

如果缓存操作失败(如删除缓存失败),则将该消息重新放入消息队列中,并设置一个重试次数或重试间隔。

消费者服务在读取消息时,会检查消息的重试次数或重试间隔,如果未达到限制,则重新尝试对缓存进行操作。

如果重试多次后仍然失败,则可以将该消息记录到日志中,并通知相关开发人员进行处理。

方案的优点

解耦:通过消息队列将数据库操作和缓存操作解耦,降低了业务侧代码的复杂度。

可靠性:重试机制能够确保即使缓存操作失败,也能够最终保证数据的一致性。

可扩展性:消息队列和异步消费者服务可以水平扩展,以适应高并发场景下的数据处理需求。

综上所述,通过队列+重试机制可以有效地保证数据库和缓存的数据一致性,即使其他读线程在消费者写缓存的过程中读到了缓存中的脏数据,但是后续其他读线程最终还是能读取到正确的数据。

基于订阅binlog的异步更新缓存

技术整体思路

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1.读Redis:读取数据都从Redis缓存中读;

2.写MySQL: 增删改在 MySQL 操作;

3.更新Redis数据:通过监听MySQL的binlog日志,异步消费者消费日志并更新到Redis;

Redis更新的具体操作

1. 数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)

  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2. 读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

-- 往期精彩回顾 –

深入Redis系列(十)Redis高可用进阶——分片技术(Redis Cluster)详解

深入Redis系列(九)Redis高可用之哨兵Sentinel大揭秘

深入Redis系列(八)Redis高可用之主从复制详解

深入Redis系列(七)Redis事务原理详解

深入Redis系列(六)redis客户端和服务端围绕一条redis命令的打工人一生(一条redis命令的生命周期)

深入Redis系列(五)Redis事件机制详解 IO多路复用、文件事件、时间事件、reactor模式

深入Redis系列(四)Redis Stream轻量级消息队列详解

深入Redis系列(三)redis持久化之RDB快照持久化和AOF日志持久化

深入Redis系列(二)Redis底层数据结构:动态字符串、链表、字典、跳跃表和压缩列表

深入Redis系列(一) Redis的五种基本数据类型和使用场景

吊打面试官系列——Redis篇面试题总结


评论