如何解决缓存数据不一致性问题

 更新时间:2025年01月10日 09:29:28   作者:Wchisper  
文章主要讨论了缓存和数据库数据一致性的问题,包括数据不一致的原因、查询和更新数据的逻辑、缓存双删方案以及优化建议

1. 数据不一致的原因

1.1 双写导致数据不一致

由于分布性系统,不能保证每个节点都可用,所有可能引起 Redis 在极限情况下数据没有写入成功,那么此时缓存中的数据和数据库数据不一致。

数据的更新为什么会成功:因为事务保证数据不管是成功还是失败,都不会有脏数据。

1.2 高并发导致数据不一致

数据在修改的过程中必定会存在网络延时,因为分布式系统节点相互独立部署,那么在并发读的情况之下,还没来得及修改完,那么对于读操作,读到的数据都是老的数据

如果是某些不严谨的情况,无所谓,如果是极致的严谨,那么就不能这么做了。比如我们的环境监测,大气的一些数据会每隔1-2分钟更新,甚至有的是5分钟更新,所以如果读到的是一些老的数据,是没有关系的,因为最终几秒或者几十秒以后会更新,这些数据的来去不会很大,而且我们能容忍一定的误差,所以也就无所谓了。

2. 查询数据的逻辑

先请求先到 Redis,如果命中则返回结果。如果 Redis 中没有数据,则从数据库查询,再写入到缓存中,再返回结果。

3. 更新数据的逻辑

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

3.1.1 方案一

在并发不高的情况下:先删除 Redis 中的旧数据。更新数据库中的数据。再将数据库中的数据同步到 Redis 中。

3.1.2 方案二

在高并发的情况下,假设有请求 A 进行更新操作,另一个请求 B 进行查询操作,那么有可能会出现:

  1. A 进行更新操作前,先删除了缓存
  2. B 查询发现缓存不存在
  3. B 查询数据库的旧值
  4. B 将旧值写入到缓存
  5. A 执行更新,将新值写入到数据库
  6. 后续的请求因为发现缓存中有数据,导致 A 更新的数据一直无法更新到缓存中,这样便出现了数据库与缓存不一致的情况。

3.2 先更新数据库,再删除缓存

3.2.1 方案一

该方案虽然存在并发问题,但是出现上述情况的概率是极低的,也有一些企业在使用这种方案。

在超高并发下,请求 A 执行更新操作,请求 B 进行查询操作:

  1. B 将新值写入到数据库
  2. A 查询 Redis 得到旧数据
  3. 线程 B 删除缓存
  4. 这样就会导致 A 修改数据 —> A 删除 Redis 之间出现脏数据。

3.2.2 方案二

在超高并发下,请求 A 执行更新操作,请求 B 进行查询操作:

  1. 缓存刚好失效
  2. B 查询数据库,得到一个旧值
  3. A 将新值写入到数据库
  4. 线程 A 删除缓存
  5. B 将旧值写入到缓存
  6. 这样就会导致后续的请求之间出现脏数据。

3.3 缓存双删方案

它的流程为:

  1. 先删除缓存
  2. 再写数据库
  3. 休眠一段时间,再删除缓存

回顾一下方案“先删除缓存,再更新数据库”可能造成数据库与缓存不一致的情况。

假设有请求 A 进行更新操作,另一个请求 B 进行查询操作,如果使用缓存双删策略:

  1. A 进行更新操作前,先删除了缓存
  2. B 查询发现缓存不存在
  3. B 查询数据库的旧值
  4. B 将旧值写入到缓存
  5. A 执行更新,将新值写入到数据库,执行休眠 Thread.sleep(t)
  6. A 苏醒,再次将缓存中的值删除

缓存双删的优点是大大降低了数据库与缓存不一致的概率的发生,注意这里只是降低,并不是说完全的避免,途中红框的地方就是缓存脏数据的时间,缺点为一定程度上降低了吞吐量,因为系统进行了休眠

这里为什么要采用休眠,对数据进行延迟缓存,原因是

  1. 例如:如果在 A 删除缓存之后,数据库修改之间 C 再次请求数据库,将老的信息存储进缓存,那么后续所有的请求打在缓存中,还是获取到老的数据
  2. 在分库分表的情况下,延迟一定的时间,也保证了,修改后的数据全部同步到所有的数据库中。

4. 扩展:其它的解决双写一直问题

通过监听数据库日志,来修改 Redis 的数据,使数据的修改达到准实时的级别,例如:canal。但是这种情况下会有一些时间的延迟,也会短暂的产生脏数据。这种情况适用于写多读少的场景

完全使用缓存作为数据库,后面在定时任务修改数据库数据。这种情况下,没要求对 Redis 的三高要求非常高,可以采用云厂商的 Redis 服务。

读取的时候只提供 Redis,也就是说,当更新操作一开始从 Redis 中删除数据了,用户去读 Redis,如果没有是不会从数据库中读的,因为只提供 Redis 的读取,写入的时候只在数据新增以及更新后才会放入到 Redis,那么如此一来,并发读的时候就不会从数据库读取老的数据并且放入 Redis 中了。没有读到也没关系,做一些空数据的处理,可能会有个几百毫秒或者 1-2s 的延迟,但是可以忍受。但是要注意做好缓存穿透的校验处理。

5. 缓存数据的思考

我们能放入缓存的数据本就不应该是实时性、一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。

我们不应该过度设计,增加系统的复杂性,遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

超高并发场景的一致性,都是最终一致性,也就是弱一致性,所以要考虑每一个环节可能失败的情况,补偿 job 也是常有的。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Redis的Spring客户端使用小结

    Redis的Spring客户端使用小结

    在Spring中使用Redis,可以极大地提升应用程序的性能和响应速度,本文主要介绍了Redis的Spring客户端使用小结,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04
  • redis在spring boot中异常退出的问题解决方案

    redis在spring boot中异常退出的问题解决方案

    这篇文章主要介绍了redis在spring boot中异常退出的问题解决方案,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-05-05
  • Redis权限和访问控制的实现示例

    Redis权限和访问控制的实现示例

    Redis提供了一些机制来保护敏感数据和限制对Redis服务器的访问,本文主要介绍了Redis权限和访问控制的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12
  • Redis IP地址的绑定的实现

    Redis IP地址的绑定的实现

    这篇文章主要介绍了Redis IP地址的绑定的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • 基于Redis实现API接口访问次数限制

    基于Redis实现API接口访问次数限制

    日常开发中会有一个常见的需求,需要限制接口在单位时间内的访问次数,比如说某个免费的接口限制单个IP一分钟内只能访问5次,该怎么实现呢,本文小编给大家介绍了如何基于Redis实现API接口访问次数限制,需要的朋友可以参考下
    2024-11-11
  • Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了?

    Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了?

    这篇文章主要介绍了Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了的相关资料,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • python脚本实现Redis未授权批量提权

    python脚本实现Redis未授权批量提权

    这篇文章主要给大家介绍了关于利用python脚本实现redis未授权批量提权的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-09-09
  • Redis实现延迟任务的常见方案详解

    Redis实现延迟任务的常见方案详解

    延迟任务(Delayed Task)是指在未来的某个时间点,执行相应的任务,本文为大家整理了Redis实现延迟任务的几个常见方案,希望对大家有所帮助
    2024-04-04
  • Redis事务处理的使用操作方法

    Redis事务处理的使用操作方法

    Redis保证一个事务中的所有命令要么都执行,要么都不执行(原子性),如果客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令,下面通过本文给大家介绍Redis事务处理的使用操作,感兴趣的朋友一起看看吧
    2021-10-10
  • redis慢查询日志的访问和管理方式

    redis慢查询日志的访问和管理方式

    这篇文章主要介绍了redis慢查询日志的访问和管理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12

最新评论