如何保证数据库、缓存的双写一致?

文章目录

  • 前言
  • 常见方案
  • 更新缓存的方案
    • 1. 先写缓存,再写数据库
    • 2. 先写数据库,再写缓存
      • 2.1. 写缓存失败
      • 2.2. 高并发问题
      • 2.3. 性能浪费
    • 3. 先删缓存,再写数据库
      • 3.1. 高并发问题
      • 3.2. 缓存双删
    • 4. 先写数据库,再删缓存
    • 5. 删除缓存失败怎么办?

前言

在我们日常研发过程中,由于数据库的一些限制,我们经常使用缓存(如:Redis)来提升访问速率。此时,数据库和缓存双写数据就存在一致性问题,这个问题跟开发语言无关,在高并发场景下,问题更加严重。
另外,在面试、工作中也会经常遇到这个问题。所以这里跟大家一起探讨下数据库和缓存双写一致性问题的解决文案。

常见方案

通常,我们使用缓存的主要目的就是为了提升查询性能。所以,我们一般这样使用缓存:

用户请求
查询缓存
是否存在?
返回
查询数据库
是否存在?
放入缓存
  1. 用户请求数据,先查询缓存中是否有相关数据,如果有则直接返回
  2. 如果缓存没数据,再继续查询数据库
  3. 如果数据库有相关数据,则将查询出来的数据放入缓存中,然后返回该数据
  4. 如果数据库也没数据,则直接返回空

这是缓存的常见用法,粗看之下,好像没啥问题。但这个方案忽略了一个非常重要的细节:如果数据库中的某条数据放入缓存后又立即更新了,那么如何更新缓存呢?
答案是:在很长的一段时间内(取决于缓存的过期时间),用户请求从缓存中取到的数据都可能是旧值,而非数据库的最新值。

更新缓存的方案

那该如何主动更新缓存呢?有以下四种方案:

  • 先写缓存,再写数据库
  • 先写数据库,再写缓存
  • 先删缓存,再写数据库
  • 先写数据库,再删缓存
    接下来,我们分别探讨下这四种方案

1. 先写缓存,再写数据库

很多人第一想法是在写操作中直接更新缓存(写缓存),直接明了。那么问题是:在写操作中,先写缓存,还是先写数据库呢?

用户写操作
写缓存
写数据库

如果用户刚写完缓存,突然网络异常导致写数据库失败了,那结果会怎么样呢?

X网络异常
用户写操作
写缓存
写数据库

结果是缓存更新为最新数据,但数据库没有更新,这样缓存中的数据就变成了脏数据。如果此时用户查询该数据,就会出现问题。因为数据库中根本不存在该数据。
原本缓存的主要目的是把数据库中的数据临时保存在内存,便于后续的查询,提升查询速度。但如果数据库中不存在某数据,缓存中存在该数据,那缓存中的这份“假数据”有啥意义?
因此,该方案不可取,实际工作中用得很少。

2. 先写数据库,再写缓存

既然上面的方案不行,那调整下顺序,先写数据库,再写缓存呢?

用户写操作
写数据库
写缓存

这样可以避免之前的“假数据”问题,但它又带来了新的问题。

2.1. 写缓存失败

X网络异常
用户写操作
写数据库
写缓存

如果把写数据库和写缓存的操作,放在同一个事务中,写缓存失败了,可以回滚写数据库的数据。
如果并发量比较少,对接口性能要求不高,系统中可以这样写。但如果是高并发的业务场景中,写数据库和写缓存,都属于远程操作。为了防止出现大事务造成的死锁问题,通常建议写数据库和写缓存不要放在同一个事务中。也就是说在该方案中,如果写数据库成功了,但是写缓存失败了,数据库中已经写入的数据不会回滚。这就会出现:数据库是新数据,缓存是旧数据,两个数据不一致的情况。

2.2. 高并发问题

在高并发场景中,针对同一个用户的同一条数据,有两个写数据请求:a和b,它们同时请求。其中请求a获取的是旧数据,请求b获取的是新数据。如图:

请求b
redis服务器
请求a
1
2网络卡顿
3
4
5写入新值
6写入旧值
用户写操作
写数据库
写缓存
缓存
用户写操作
写数据库
写缓存
  1. 请求a先到,先写数据库,然后由于网络原因卡顿,还没来得及写缓存
  2. 请求b到,先写数据库,然后写缓存
  3. 此时,请求a卡顿结束,继续写缓存

显然,在这个过程中,请求b在缓存中的新数据,被请求a的旧数据覆盖了。也就是说:在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。

2.3. 性能浪费

该方案还有一个比较大的问题:每个写操作,写完数据库,会马上写缓存,比较浪费系统资源,进而造成性能浪费。
例如:写的缓存并不是简单的数据内容,而是要经过非常复杂的计算得出的最终结果。这样每写一次缓存,都需要经过一次非常复杂的计算,这样非常浪费系统资源,尤其是CPU和内存资源。
还有一些业务场景比较特殊:写多读少。如果在这些业务场景中,每个写操作,都需要写一次缓存,也有点儿得不偿失。
所以,在高并发场景中,先写数据库,再写缓存,这套方案问题不少,也不太建议使用。

3. 先删缓存,再写数据库

如果换一种思路:不去直接更新缓存,而改为删除缓存呢?
那么同样有两种方式:

    1. 先删缓存,再写数据库
    1. 先写数据库,再删缓存

先看看第一种情况:

用户写操作
删除缓存
写数据库

这个方案在单机情况下还可以,但是在高并发场景下也会有一样的问题。

3.1. 高并发问题

假设在高并发场景中,同一个用户的同一条数据,有一个读数据请求r,还有另一个写数据请求w,同时请求。如图:

请求r
redis服务器
请求w
1
8网络卡顿
3
5
6
2清空
4
7写入旧值
用户查询操作
查询缓存
查询数据库
更新缓存
缓存
用户写操作
删除缓存
写数据库
  1. 请求w先到,把缓存删除。由于网络原因,卡顿一下,未及时写数据库
  2. 请求r到,先查缓存没数据,查询数据库,有数据且还是旧值
  3. 请求r将数据库中的旧值更新到缓存中
  4. 此时,请求w卡顿结束,把新值写入数据库

在这个过程中,请求w中的新值未被请求r写入缓存,同样会导致缓存和数据库的数据不一致。如何解决呢?

3.2. 缓存双删

在上面的场景中,一个读数据,一个写数据。当写数据请求把缓存删了之后,读数据请求可能把当时从数据库查询出来的旧值写入缓存中。可以采用重新删除缓存数据的方式解决。这就是缓存双删,即在写数据库之前删除一次,写完数据库之后再删除一次。

第二次删除缓存,不是立即删除,而是在一定的时间间隔之后(如500ms)。

为什么一定要间隔一段时间之后才能删除呢?因为请求w卡顿结束,把新值写入数据库后,请求r将数据库中的旧值更新到缓存中。如果请求w删除太快,请求r还未将旧值更新到缓存,这次删除将没有任何意义。必须要在请求r更新缓存后再及时删除缓存。所以需要在请求w中添加一个时间间隔。

4. 先写数据库,再删缓存

用户写操作
写数据库
删除缓存

在高并发场景中,有一个读数据请求,一个写数据请求,更新过程如下:

  1. 请求a先写数据库,由于网络原因卡顿一下,没来得及删除缓存
  2. 请求b查询缓存,发现缓存中有数据,直接返回该数据
  3. 请求a删除缓存

在这个过程中,只有请求b读了一次旧数据,后来旧数据就被请求a及时删除了,看起来问题不大。如果是读数据请求先来呢?

  1. 请求a查询缓存,发现缓存中有数据,直接返回该数据
  2. 请求b先写数据库
  3. 请求b删除缓存

这种情况看起来也是没有问题的。但有一种极限情况,缓存已经失效,如图:

请求b
redis服务器
请求a
5
6
2
4
8网络卡顿
7清空
3
9写入旧值
用户查询操作
查询缓存
查询数据库
更新缓存
缓存
用户写操作
写数据库
删除缓存
  1. 缓存过期时间到了,自动失效
  2. 请求b查询缓存,发现缓存中没有数据,查询数据库,发现旧值,但由于网络卡顿,没来得及更新缓存
  3. 请求a先写数据库,然后删除缓存
  4. 请求b更新旧值到缓存中

此时,缓存和数据库的数据同样出现不一致的情况。但这种情况比较少,需要同时满足以下条件才会发生:

  • 缓存刚好自动失效
  • 请求b从数据库查出旧值,更新缓存的耗时,比请求a写数据库并删除缓存的时间还长

一般情况下,查询数据库的速度比写数据库更快,何况写完数据库还要删除缓存。所以,系统同时满足上述两个条件的概率非常小。推荐大家使用该方案。

5. 删除缓存失败怎么办?

其实上面两个方案还有一个共同的风险点,即:如果缓存删除失败了,也会导致数据不一致。应该怎么解决这个问题呢?
答案是重试机制。
在接口中如果更新数据库成功了,但更新缓存失败了,可以立即重试三次。如果其中有任何一次成功,则直接返回成功。如果三次都失败了,则写入数据库,准备后续再处理。
当然,如果在接口中直接同步重试,该接口并发量比较高的时候,可能会影响接口性能。这时,可以修改为异步重试。
异步重试方式有多种:

  1. 每次单独起线程,该线程专门做重试工作。但在高并发场景下,可能会创建太多的线程,导致系统OOM问题,不建议使用
  2. 将重试的任务交给线程池处理,但如果服务器重启,部分数据可能丢失
  3. 将重试数据写表,然后用elastic-job等定时任务进行重试
  4. 将重试请求写入mq等消息中间件中,在mq的consumer中处理
  5. 订阅mysql的binlog,在订阅者中,如果发现了更新数据请求,则删除相应的缓存

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/809202.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

单例模式a

为什么需要单例模式? 保证类的实例在全局只有一个,避免无效对象创建和销毁时的资源消耗。 在Java中一切都是对象,实例方法的调用需要通过对象,为了调用类中的方法而创建对象,方法调用完成之后对象也需要被GC回收&…

Stable Diffusion之Ubuntu下部署

1、安装conda环境 conda create -n webui python3.10.6 2、激活环境 每次使用都要激活 conda activate webui 注意开始位置的变换 关闭环境 conda deactivate webui 3、离线下载SD 代码 https://github.com/AUTOMATIC1111/stable-diffusion-webui https://github.com/Stabilit…

CentOS7.9.2009安装Kibana7.11.1

本文章使用CentOS7.9.2009服务器安装kibana7.11.1软件 1.服务器信息 [root@elasticsearch ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [root@elasticsearch ~]# 2.kibana安装 2.1.创建kibana用户 创建kibana用户和组 命令: useradd kibana [r…

10个大型语言模型(LLM)常见面试问题和答案解析

今天我们来总结以下大型语言模型面试中常问的问题 1、哪种技术有助于减轻基于提示的学习中的偏见? A.微调 Fine-tuning B.数据增强 Data augmentation C.提示校准 Prompt calibration D.梯度裁剪 Gradient clipping 答案:C 提示校准包括调整提示,尽量减少产生…

SQL注入sqli_labs靶场第九、十题

第九题 ?id1 and 11 页面正常 ?id1 and 12 页面正常 ?id1 and 11 页面正常 ?id1 and 12 页面正常 输入任何信息,均显示相同页面,尝试延时注入判断 ?id1 and if(11,sleep(5),1)-- 页面迅速显示 ?id1 and if(11,sleep(5),1)-- 页面过了5秒显示 判断为…

http请求头导致了dial tcp:lookup xxxx on 10.43.0.10:53 no sunch host

事实证明人有的时候也不能太偷懒,太偷懒容易给自己埋坑。 问题的背景: web端调用服务A,服务A异步调用服务B。服务A有四个场景需要调用服务B,所以,服务A中封装了一个公用的方法,唯一的区别是,场…

IDEA 控制台中文乱码 4 种解决方案

前言 IntelliJ IDEA 如果不进行相关设置,可能会导致控制台中文乱码、配置文件中文乱码等问题,非常影响编码过程中进行问题追踪。本文总结了 IDEA 中常见的中文乱码解决方法,希望能够帮助到大家。 IDEA 中文乱码 解决方案 一、设置字体为支…

手机银行客户端框架之mPaaS介绍

移动开发平台(Mobile PaaS,简称 mPaaS)是源于支付宝 App 的移动开发平台,为移动开发、测试、运营及运维提供云到端的一站式解决方案,能有效降低技术门槛、减少研发成本、提升开发效率,协助企业快速搭建稳定…

H. GCD is Greater

H. GCD is Greater 题意 给定一个长度为 n n n的数组 a a a,先手选择 [ 2 , n − 2 ] [2,n-2] [2,n−2]个数并计算所选择数的gcd,后手选择剩下的数,并计算剩下所有的数按位与的结果,再加上给定的 x x x,如果先手的结果…

Docker Redis Debian服务器版

1.使用官方安装脚本自动安装docker 安装命令如下: curl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.sh 如果安装提示 -bash sudo command not found 则需要 #update sudo apt-get update sudo apt-get install sudo再执行安装脚本1 安装…

c++中常用库函数

大小写转换 islower/isupper函数 char ch1 A; char ch2 b;//使用islower函数判断字符是否为小写字母 if(islower(ch1)){cout << ch1 << "is a lowercase letter." << end1; } else{cout << ch1 << "is not a lowercase lette…

上采样-Lanczos插值

Lanczos插值是一种高级的上采样方法&#xff0c;它在计算目标图像中每个像素的值时&#xff0c;利用了周围像素的信息&#xff0c;并通过Lanczos滤波器进行插值计算&#xff0c;以产生较为清晰的结果。下面通过图文并茂的方式详细描述Lanczos插值方法的实现过程。 现在我们想将…

IntelliJ IDEA(WebStorm、PyCharm、DataGrip等)设置中英文等宽字体,英文为中文的一半(包括标点符号)

1.设置前&#xff08;idea默认字体为 JetBrains Mono&#xff09; 2.设置后&#xff08;楷体&#xff09;

计算机网络常见面试总结

文章目录 1. 计算机网络基础1.1 网络分层模型1. OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f;2.TCP/IP 四层模型是什么&#xff1f;每一层的作用是什么&#xff1f;3. 为什么网络要分层&#xff1f; 1.2 常见网络协议1. 应用层有哪些常见的协议&#xff1f;2…

三种语言实现spark createDataFrame

前言 我们经常需要在本地用数组写一些测试数据,进行spark逻辑测试,需要借助StructType和StructField以及数组生成DataFrame,最终进行测试,这里就简单的用Java、Scala、Python三种语言实现用数组的数据创建DataFrame。 了解StructType和StructField StructType spark stru…

systemctl start docker报错(code=exited, status=1/FAILURE)

运行systemctl start docker报错内容如下: 输入systemctl status docker.service显示以下内容&#xff1a; 本次启动不起来与docker服务无关 具体解决问题是修改 /etc/docker/daemon.json&#xff0c;vim /etc/docker/daemon.json # 添加如下内容 {"registry-mirrors&qu…

ccf201509-3模板生成系统(list,map,字符串综合运用)

问题描述 成成最近在搭建一个网站&#xff0c;其中一些页面的部分内容来自数据库中不同的数据记录&#xff0c;但是页面的基本结构是相同的。例如&#xff0c;对于展示用户信息的页面&#xff0c;当用户为 Tom 时&#xff0c;网页的源代码是&#xff1a; 而当用户为 Jerry 时…

华为OD-C卷-分披萨[100分]

题目描述 "吃货"和"馋嘴"两人到披萨店点了一份铁盘(圆形)披萨,并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。但是粗心的服务员将披萨切成了每块大小都完全不同奇数块,且肉眼能分辨出大小。 由于两人都想吃到最多的披萨,他们商量了一个他们认…

使用htmlentities()和nl2br()将文本数据正确显示到前台

问题&#xff1a; 在后台textarea里编辑了有一串字符串&#xff0c;虽然在textarea里编辑是有换行效果的&#xff0c;但是数据获取到就只是\n&#xff0c;前端是不认识这个的&#xff0c;正确输出到前台的换行只能是<br/>。 $str "ABCDEFGHIJKLMNOPQ"; echo…

网络安全之反弹Shell

网络安全之反弹Shell 在网络安全和渗透测试领域&#xff0c;“正向Shell”&#xff08;Forward Shell&#xff09;和"反向Shell"&#xff08;Reverse Shell&#xff09;是两种常用的技术手段&#xff0c;用于建立远程访问目标计算机的会话。这两种技术都可以让攻击者…