tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 各种MQ

  • Nginx

  • IdentityServer

  • Redis

    • 初始Redis与安装
    • Redis常用的一些场景
    • Redis中的缓存失效
    • 关于Redis的一些问题
    • Redis的持久化策略
    • Redis从单机到集群等模式
    • Redis和MySql实现数据一致性
      • 问题的背景
      • 更新策略
        • 先更新缓存
        • 先删缓存再更新数据库
        • 延迟双删
        • 先更新数据库再删除
        • 总结
    • 通过Redis实现分布式锁
    • Redis的客户端介绍
  • Linux

  • 中间件
  • Redis
EffectTang
2024-12-02
目录

Redis和MySql实现数据一致性

# Redis和MySql实现数据一致性

首先明确一个观点:缓存的写入通常要远远快于数据库的写入

# 问题的背景

在我们使用Redis来提高查询效率后,我们不可避免的会遇上这么一个问题————如何更新数据,才能更好的保证数据一致性。

Redis中的数据来自Mysql,一旦MySql中的数据变更后,那么Redis中的数据也应该要发生相应变化才对,或者说将变化的这部分数据同步到Redis中。这是问题就来了,先更新谁:

  • 先更新数据库,再更新缓存;
  • 先更新缓存,再更新数据库;

这两种操作,不考虑并发情况,正常情况下,不论先后,都可以让两者保持一致,但现在我们需要重点考虑【异常】情况。以下我们讨论的都是异常情况,或者少数情况可能会发生的。

这个两个操作不可能同时发生,必然存在先后顺序,这就导致了数据不一致的情况发生。因为我们获取数据是先从Redis中获取,如果先更新数据库如MySql等,那这时用户请求可能会获得旧数据。

所以答案是————先更新缓存吗?

先更新数据库还可能存在以下问题:

比如「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:

A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。

此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象。

# 更新策略

上文说了,先更新数据库好像存在一些明显的问题,特别是在更新数据库特别耗时的情况。那我们先更新缓存呢?

# 先更新缓存

如果缓存先更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。那么此时就会发生请求可以命中缓存,拿到正确的值。但是,一旦缓存【失效】,就会从数据库中读取到【旧值】,然后重建缓存也是这个旧值。这时用户会发现自己之前修改的数据又变回去了,对业务造成影响。

或者因为其他因素————比如求一个计算后的值,需要在数据库获取一堆值,其中就包括这个【旧值】,那么这样得出的最终值就是一个错误的。

这2种方案都不行。似乎到这里,解决方案就是:加分布式锁。但这种方式对性能影响有点大。

还有其他方案吗?

# 先删缓存再更新数据库

我们在访问数据的时候,其实本质都是访问的数据库的数据,缓存的数据其实都是复制的数据库的。那我们在更新之前,先把缓存中的数据给删掉,再更新数据库的数据。这样能解决问题吗?

先删除缓存,后更新数据库,假设第二步操作失败,数据库没有更新成功,那下次读缓存发现不存在,则从数据库中读取,并重建缓存,此时数据库和缓存依旧保持一致。

似乎一切大功告成。不要急,我们再来看看在【并发】的情况,是怎样的?

假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21。

请求B可以读到用户年龄的原因是,此时请求A在删除缓存后,可能因为网络延迟了。如果请求B是一个写操作,那这种情况也会发生。

在这种并发操作下,缓存的数据仍然是旧的,出现业务不一致。那给数据库加锁吧?但加缓存不就是为了效率,而加锁又会降低效率,违背了初衷。再想想其他方案呢?

# 延迟双删

那我们请求B在更新完数据库后,再删除一次缓存,然后存储对应的值,这样不就行了吗?

是的,这样实现了数据一致性,虽然在中途出现了一些————请求到旧数据,但最终的情况是相同的。这种就叫做实现了最终一致性。

但请注意在第二次删除的时候,请延迟删除。这是为了避免发生————在你删除后,请求B再写入缓存。这样删除就没效果了。当然具体的时间根据你的业务而定。

其实就"更新缓存"来说,直接删除缓存比更新缓存还有一个优势:

删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小。

在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。比如上面提到的商品详情信息,在底层可能会关联商品表、价格表、库存表等,如果更新了一个价格字段,那么就要更新整个数据库,还要关联的去查询和汇总各个周边业务系统的数据,这个操作会非常耗时。

从另外一个角度,不是所有的缓存数据都是频繁访问的,更新后的缓存可能会长时间不被访问,所以说,从计算资源和整体性能的考虑,更新的时候删除缓存,等到下次查询命中再填充缓存,是一个更好的方案。

# 先更新数据库再删除

过程:

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

但这种情况,可能会发生以下情况:

  • 短暂的数据不一致:在数据库更新成功后,缓存被删除,但在新的数据项被重新加载到缓存之前,其他请求可能会读取到旧的或不存在的数据。

它也实现了最终一致性。

似乎一切大功告成。不要急,我们再来看看在【异常、并发】的情况,是怎样的?

假如在删除缓存的时候,失败了?我们有哪些解决方案:

  • 在删除缓存失败时,可以尝试多次删除,直到删除成功或达到最大重试次数。
  • 在删除缓存失败时,将删除缓存的操作放入消息队列,由消息队列消费者异步处理。

那并发情况下呢?会发生什么?

1.缓存中x 不存在(数据库x=1)

2.线程 A 读取数据库,得到旧值(X=1)

3.线程 B 更新数据库(X=2)

4.线程 B 删除缓存

5.线程 A 将旧值写入缓存(X=1)

最终 X的值在缓存中是1(旧值),在数据库中是2(新值),也发生不一致。这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?其实概率「很低」,这是因为它必须满足 3个条件:

1.缓存刚好已失效

2.读请求 +写请求并发

3.更新数据库 +删除缓存的时间(步骤 3-4),要比读数据库+写缓存时间短(步骤 2和 5)

仔细想一下,条件3发生的概率其实是非常低的。因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的,「先更新数据库 +再删除缓存」的方案,是可以保证数据一致性的。

# 总结

那么我们使用哪个方案呢?

推荐(供参考)——采用「先更新数据库,再删除缓存」方案。主要是因为,它在并发情况下,效果好于前者(先删缓存再更新)。为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据最终一致

其实使用延迟双删时,第二次操作,不就是一个【先更新数据库再删除】吗?

上次更新: 2025/04/23, 16:23:16
Redis从单机到集群等模式
通过Redis实现分布式锁

← Redis从单机到集群等模式 通过Redis实现分布式锁→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式