首页 > 数据库 >如何保证缓存与数据库双写时的数据一致性

如何保证缓存与数据库双写时的数据一致性

时间:2023-10-12 12:04:52浏览次数:47  
标签:缓存 删除 数据库 Redis 更新 一致性 数据 双写


1、背景

在做系统优化时,想到了将数据进行分级存储的思路。因为在系统中会存在一些数据,有些数据的实时性要求不高,比如一些配置信息。

基本上配置了很久才会变一次。而有一些数据实时性要求非常高,比如订单和流水的数据。所以这里根据数据要求实时性不同将数据分为三级。

  • 第1级:订单数据和支付流水数据;这两块数据对实时性和精确性要求很高,所以不添加任何缓存,读写操作将直接操作数据库。
  • 第2级:用户相关数据;这些数据和用户相关,具有读多写少的特征,所以我们使用redis进行缓存。
  • 第3级:支付配置信息;这些数据和用户无关,具有数据量小,频繁读,几乎不修改的特征,所以我们使用本地内存进行缓存。

但是只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。

接下来就讨论一下关于保证缓存和数据库双写时的数据一致性。

2、解决方案

那么我们这里列出来所有策略,并且讨论他们优劣性。

  1. 先更新数据库,后更新缓存
  2. 先更新数据库,后删除缓存
  3. 先更新缓存,后更新数据库
  4. 先删除缓存,后更新数据库

3、先更新数据库,后更新缓存

这种场景一般是没有人使用的,主要原因是在更新缓存那一步,为什么呢?因为有的业务需求缓存中存在的值并不是直接从数据库中查出来的,有的是需要经过一系列计算来的缓存值,那么这时候后你要更新缓存的话其实代价是很高的。如果此时有大量的对数据库进行写数据的请求,但是读请求并不多,那么此时如果每次写请求都更新一下缓存,那么性能损耗是非常大的。

举个例子比如在数据库中有一个值为 1 的值,此时我们有 10 个请求对其每次加一的操作,但是这期间并没有读操作进来,如果用了先更新数据库的办法,那么此时就会有十个请求对缓存进行更新,会有大量的冷数据产生,如果我们不更新缓存而是删除缓存,那么在有读请求来的时候那么就会只更新缓存一次。

4、先更新缓存,后更新数据库

这一种情况应该不需要我们考虑了吧,和第一种情况是一样的。

5、先删除缓存,后更新数据库

该方案也会出问题,具体出现的原因如下。

如何保证缓存与数据库双写时的数据一致性_redis

先删除缓存,后更新数据库

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)。

  1. 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作。
  2. 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中。
  3. 但是此时请求 A 并没有更新成功,或者事务还未提交。

那么这时候就会产生数据库和 Redis 数据不一致的问题。如何解决呢?其实最简单的解决办法就是延时双删的策略。

如何保证缓存与数据库双写时的数据一致性_数据_02

延时双删

但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

如何保证缓存与数据库双写时的数据一致性_数据_03

主从同步时间差

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)。

  1. 请求 A 更新操作,删除了 Redis。
  2. 请求主库进行更新操作,主库与从库进行同步数据的操作。
  3. 请 B 查询操作,发现 Redis 中没有数据。
  4. 去从库中拿去数据。
  5. 此时同步数据还未完成,拿到的数据是旧数据。

此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

如何保证缓存与数据库双写时的数据一致性_缓存_04

从主库中拿数据

6、先更新数据库,后删除缓存

问题:这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

如何保证缓存与数据库双写时的数据一致性_缓存_05

先更新数据库,后删除缓存

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作。
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败。
  3. 此时将Redis 的 key 作为消息体发送到消息队列中。
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作。

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

如何保证缓存与数据库双写时的数据一致性_redis_06

利用订阅binlog 删除缓存

 

7、总结 

每种方案各有利弊,比如在第二种先删除缓存,后更新数据库这个方案我们最后讨论了要更新 Redis 的时候强制走主库查询就能解决问题,那么这样的操作会对业务代码进行大量的侵入,但是不需要增加的系统,不需要增加整体的服务的复杂度。

最后一种方案我们最后讨论了利用订阅 binlog 日志进行搭建独立系统操作 Redis,这样的缺点其实就是增加了系统复杂度。

其实每一次的选择都需要我们对于我们的业务进行评估来选择,没有一种技术是对于所有业务都通用的。没有最好的,只有最适合我们的。

PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。

标签:缓存,删除,数据库,Redis,更新,一致性,数据,双写
From: https://blog.51cto.com/zhongmayisheng/7825429

相关文章

  • 基于 MySQL 和 DynamoDB 的强一致性分布式事务实践
    在单体应用向微服务架构转型的过程中,本地事务已不再满足系统一致性需求,为了解决这一问题,前人在对性能和数据一致性反复权衡的过程中总结了许多典型的协议和算法,各有优劣。本文我们将深入探讨Freewheel如何实现无单点故障的可扩展分布式事务实现模型。1、为什么需要分布式事务?当应......
  • 第四节:Redis数据持久化机制(备份恢复)、缓存淘汰策略、主从同步原理、常见规范与优化
    一.数据持久化 1. 含义Redis提供了RDB和AOF两种持久化方式,默认开启的是RDB,如果需要AOF,需要手动修改配置文件进行开启。RDB:是一种对Redis存在内存中的数据周期性的持久化机制,将内存中的数据以快照的形式硬盘,实质上是fork了一个子进程在执行数据存储,采用的是二进制压......
  • 时间、顺序与一致性
    一、背景分布式架构下,需要协调不同节点之间的先来后到,但不同节点又没有统一承认的时间标准,于是创造了网络时间协议(NTP)试图来解决不同节点之间的时间标准,但是NTP本身表现并不如人意,所以又构造出了逻辑时钟,最后在逻辑时钟的基础上改进为了向量时钟二、时间标准分类1. 网络时......
  • 黑群晖配置缓存,为NAS加速
    不啰嗦直接上操作         ......
  • Spring 源码分析(五)——Spring三级缓存的作用分别是什么?
    Spring的三级缓存是经典面试题,也会看到一些文章讲三级缓存与循环依赖之的关系。那么,三级缓存分别存储的什么呢?他们的作用又分别是什么?一、一、二级缓存一级缓存是一个名为singletonObjects的ConcurrentHashMap,用于存储已经创建完成的Bean。其作用也是最明显的,获取Bean时最优......
  • 探索Redis与MySQL的双写问题
    本文已收录至GitHub,推荐阅读......
  • 探索Redis与MySQL的双写问题
    本文已收录至GitHub,推荐阅读......
  • 看完包你搞懂Redis缓存穿透、击穿和雪崩!!!说到做到
    缓存穿透缓存穿透是指当用户对Redis发出无效或者不存在的数据信息操作时,这条数据在Redis中不存在,Redis就会在MySQL数据库中查询,可时无效的信息在mysql数据库中也不存在,就会造成Redis一直查询MySQL,对MySQL造成极大压力解决方式方式一:返回缓存空值这种方式有点像“以牙还牙”,对......
  • .NET6 startup.cs 注入 本地缓存,AddTransient ,AddScoped ,AddSingleton生命周期
    .NET6startup.cs注入本地缓存//使用缓存usingMicrosoft.Extensions.Caching.Memory;services.AddMemoryCache();//自定义缓存类,类继承接口services.AddScoped<IMemoryCacheHelper,MemoryCacheHelper>();service.cs里使用构造函数注入生成对象方法里调用对象的写,获取......
  • Asp.Net Core webapi+net6 使用资源筛选器(过滤器) 做缓存
    写一个特性类,用来做标记[AttributeUsage(AttributeTargets.Method)]//只对方法有效publicclassResourceFilterAttribute:Attribute{}我这里使用了MemoryCache来做缓存,也可以使用字典来做,但一定要加上static,否则字典每一次请求都会new一个实例,缓存的东西就丢了private......