缓存与数据库的双写一致性

news2024/11/20 15:20:27

背景

在高并发的业务场景下,系统的性能瓶颈往往是出现在数据库上,用户并发访问过大,压力都打到数据库上。所以一般都会用redis做缓存层,起到一个缓冲作用,让请求先访问到缓存层,而不是直接去访问数据库,减轻数据库压力,从而减少网络请求的延迟响应,提高系统性能。一般在使用缓存和数据库结合的时候就会面临数据一致性问题,可能会经常遇到“明明数据已经更新了,怎么还是显示旧的”,下面就来分析下产生的原因及其对应处理方案。

数据更新的常见操作

注意:我们讲的数据一致性的前提是数据库更新和缓存删除不把它当成一个原子性操作。因为高并发场景下,我们不可能引入分布式锁将这两者操作绑定为一个原子性操作,如果绑定的话就会很大程度上影响系统并发性能,所以一般只追求最终一致性,本文也是针对非追求强一致性要求的场景,金融或银行业务的小伙伴请自行判断。

本文涉及到一种常用的缓存模式:Cache-Aside Pattern,即旁路缓存模式,这种模式就是为了尽可能地解决缓存和数据库的数据一致性问题。这种模式分为读请求和写请求两种。
读请求流程:
读的时候,先读取缓存,若缓存命中的话,直接返回数据。
若缓存没有命中,就去读数据库,从数据库取出数据,放入缓存后,返回响应。

写请求流程:
更新数据的时候,先更新数据库,然后再删除缓存。

下面罗列出常见的几种数据更新的方式及其对应的问题:

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

流程如下图:

在这里插入图片描述

1.请求A先发起一个写操作,先更新数据库
2.请求B再发起一个写操作,更新了数据库
3.由于网络等原因,请求B先更新了缓存
4.请求A更新缓存

问题如上所示,缓存保存的是A的数据(旧数据),数据库保存的是B的数据(新数据),数据不一致,脏数据出现。

这种场景一般不推荐使用。因为有的业务需求中缓存里的值并不是直接从数据库中查出的,有的是需要经过一系列操作计算出缓存的值,那么这时候你要更新缓存的代价是很高的。如果这时有大量请求需要对数据库进行写操作,但是读的请求并不多,那么每次写操作都更新一次缓存,性能损耗是非常大的。

举个最简单的例子:数据库中有一个num字段的值为1,这时有10个请求对其进行递增加一的操作,但是这期间读请求很少,如果是先更新数据库,后更新缓存的话,那么就会有十个请求对缓存进行更新,这样会有大量的冷数据产生。如果选择删除缓存而不是更新缓存,那么在读请求进来的时候就只会更新一次缓存。这样的话哪种操作消耗的资源更多是不是就很明显了。

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

这一种情况和上一种是类似的,这里就不再赘述。

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

流程如下图:

在这里插入图片描述

1.请求A先发起一个写操作,先删除缓存,此时会还没更新数据库完成(可能在还没更新,或者正在更新,但事务还未提交)
2.此时请求B发起一个读操作,读取到缓存数据为空
3.请求B在缓存中读不到数据,就去读取数据库并将旧数据写入缓存(脏数据)
4.请求A更新DB完成

问题如上所示,缓存保存的是旧数据(请求B将脏数据写入了缓存),如果是一个读多写少的数据,可能脏数据会存在比较长的时间(要么后续有更新,要么等待缓存过期),这在业务上是不能接受的。

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

流程如下图:

在这里插入图片描述

1.请求A先发起一个写操作,先更新数据库,此时还没删除缓存完成
2.此时请求B发起一个读操作,读取到的缓存数据为旧数据
3.请求A删除缓存

问题如上所示,在请求A更新数据库和删除缓存之间请求B会读取到旧数据,因为此时整个请求A的操作还没有完成,并且读到旧数据的时间是非常短的,而后请求后会删除缓存,所以可以满足数据最终一致性要求。但是不排除请求A删除缓存失败的可能。

分析

先来对以上几种方式进行概括分析,可以分为这两种选择,1、是更新缓存还是删除缓存,2、是先更新数据库还是先操作缓存

是更新缓存还是删除缓存,结合上面的第1、2点的例子可以得出是选择删除缓存

更新缓存相对于删除缓存两点劣势:

  • 如果你写入的缓存,是经过复杂计算才得到的话。更新缓存的频率高,性能损耗大。
  • 在写操作多,读数据少的场景下,缓存数据很多时候还没被读取到,又被更新了,浪费了资源(写多读少的场景用缓存不是很划算)

根据上面分析的结论是删除缓存,那么是先更新数据库还是先删除缓存?
由上面第3点的例子,可以看出如果是先删除缓存再更新DB,会有较大可能导致缓存保存的是旧数据,数据库保存的是新数据。

有的人会说,那第4点的先更新数据库再删除缓存,不也可能导致缓存中是旧数据?

其实只要是非原子性操作就都可能出现数据不一致的情况,但是第四点这种方式,一般是因为删除缓存失败等原因,才会导致缓存了脏数据,这个概率会低很多。

解决方案

其实上面第3点和第4点的问题,删除缓存失败的情况,我们只要保证他删除成功就可以。

一、延时双删缓存

以上面第3点例,先删除缓存再更新数据库,最直观能想到的最简单的办法就是延时双删。什么是延时双删,看完如下图流程就明白了。

在这里插入图片描述

在原来第3点,先删除缓存再更新数据库的基础上,在请求A更新完数据库后,休眠一下(比如1秒),然后再次删除缓存。

这种方案只有休眠那一下,可能有脏数据被读取,一般业务也可以接受的。这个休眠延迟时间一般要根据读业务逻辑的耗时去估算(比较难)然后增加相应几百毫秒的延迟。

但是如果第二次删除缓存又失败了?给key设置一个过期时间?业务上能否接受在key过期之前的这段时间内的数据不一致?

二、重试删除缓存

根据业务预估缓存过期时间很麻烦,而且你预估的也不一定准,可能还有其他什么原因造成过期时间过短或过长而影响了正常业务。
既然如此,前两次都删除失败了,那我多删他几次保证他删除成功就可以了。

利用消息队列进行重试删除缓存的补偿机制,流程如下图:

在这里插入图片描述

1.写请求进来,先更新数据库
2.由于某些原因,删除缓存失败
3.把删除失败的key推送到消息队列
4.消费队列消息,获取要删除的key
5.重试删除缓存

上面第3点和第四点的例子,均可以使用此方案。但这个方案有一个缺点:会对业务代码造成大量侵入,耦合在一起。

三、异步淘汰缓存

重试删除缓存机制已经满足保持数据一致性的要求,但是会造成好多业务代码入侵。所以可以优化下:开一个订阅服务(独立的中间系统),负责通过订阅数据库(如mysql)的binlog来异步淘汰缓存。

mysql更新数据后在binlog日志中都有相应的记录,我们可以订阅mysql的binlog对缓存进行操作。流程如下图:

在这里插入图片描述

异步淘汰机制可以达到想要的双写一致性效果,但是对应的也有他的缺点:增加了整个系统的复杂度。

踩坑

注意⚠️:上面讲的数据库和缓存都是普通的单机情况下的。

这里讲下我以前踩过的坑,上面第3点的例子,先删除缓存,后更新数据库,然后异步淘汰。

这里还隐藏着一个问题:如果你使用的是mysql读写分离架构的话,主从同步之间会有时延问题,这就有可能产生脏数据。

看如下图流程你就明白了,先不复杂化,搞个大家都能看懂的。我假设缓存删除成功,更新数据库也成功,这两者之间没有其他读请求插入:

在这里插入图片描述

如上所示,请求A和请求B操作时序没问题,是主从同步的时延问题(假设1s),导致读请求读取到从库中的脏数据

1.请求A先发起一个写请求,先删除了缓存
2.请求A请求主库进行更新数据
3.主库与从库进行数据同步
4.请求B发起一个读请求,读取缓存中的数据为空
5.请求B去DB从库中取数据,由于主库压力大/处理数据量多/网络原因等,此时主从同步还没完成。请求B读取到DB从库中的旧数据并写入缓存中
6.最后主从同步完成

我之前的解决方案:如果缓存数据为空,需要查询DB再设置到缓存的操作,就强制将查询的DB指向master进行查询。

总结

每种方式和方案都各有利弊。

比如先删除缓存,后更新数据库这个方式,我们最终选择了重试删除缓存+更新Redis的时候强制走主库查询就能解决问题,但是这操作会对业务代码进行大量的侵入,但是不需要增加新的中间系统去处理,不需要增加整体的服务的复杂度。

如果我们选择异步淘汰缓存的方案,利用订阅binlog日志进行搭建独立的中间系统来操作缓存,但就样就增加了系统复杂度,复杂度增加带来的风险往往是后知后觉的。

其实每种方案的选择都需要我们对本身的业务进行评估,没有一种技术是对所有业务都通用的。我觉得最难的是寻找最佳效益的平衡点的取舍问题,就像常说的:没有最好的,只有最适合你的。

我是六涛sheliutao,文章编写总结不易,转载注明出处,喜欢本篇文章的小伙伴欢迎点赞、关注,有问题可以评论区留言或者私信我,相互交流!!!

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

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

相关文章

Web3D发展趋势以及Web3D应用场景

1,Web3D发展趋势随着互联网的快速发展,Web3D技术也日渐成熟,未来发展趋势也值得关注。以下是Web3D未来发展趋势的七个方面:可视化和可交互性的增强:Web3D可以为三维数据提供可视化和可交互性的增强,将极大地…

CCNP350-401学习笔记(补充题目101-186)

101、 enable the https service 102、fabric edge node 103、 104、 105、 16113 106、filter access-group 10 107、 108、VETP 109、 110、 111、the server and the database can inititate communication 112、 113、 114、 115、 116、WAN edge&#xf…

【教程】去水印开源工具Lama Cleaner在Windows的安装和使用

一、Lama Cleaner是什么? Lama Cleaner是一款开源且免费的人工学习图片去水印程序(个人主要学习用途),没有图片分辨率限制(个人使用暂未发现),并且保存的图片质量很高(个人觉得跟原…

MSTP多进程讲解与实验配置

目录 MSTP多进程 专业术语 MSTP多进程配置 在MSTP域配置 MSTP多进程 多进程的作用 将设备上的端口绑定到不同的进程中,以进程为单位进行MSTP计算,不在同一进程内的端口不参与此进程中的MSTP协议计算,实现各个进程之间的生成树计算相互独立…

【算法】DFS与BFS

作者:指针不指南吗 专栏:算法篇 🐾题目的模拟很重要!!🐾 文章目录1.区别2.DFS2.1 排列数字2.2 n-皇后问题3.BFS3.1走迷宫1.区别 搜索类型数据结构空间用途过程DFSstackO( n )不能用于最短路搜索到最深处&a…

leetcode打卡-深度优先遍历和广度优先遍历

200.岛屿数量 leetcode题目链接:https://leetcode.cn/problems/number-of-islands leetcode AC记录: 思路:深度优先遍历,从0,0开始遍历数组,使用boolean类型数组used记录是否被访问过,进行一…

28个案例问题分析---014课程推送页面逻辑整理--vue

一&#xff1a;背景介绍 项目开发过程中&#xff0c;前端出现以下几类问题&#xff1a; 代码结构混乱代码逻辑不清晰页面细节问题 二&#xff1a;问题分析 代码结构混乱问题 <template><top/><div style"position: absolute;top: 10px"><C…

SpringBoot监听机制-以及使用

11-SpringBoot事件监听 Java中的事件监听机制定义了以下几个角色&#xff1a; ①事件&#xff1a;Event&#xff0c;继承 java.util.EventObject 类的对象 ②事件源&#xff1a;Source &#xff0c;任意对象Object ③监听器&#xff1a;Listener&#xff0c;实现 java.util…

奇思妙想:超链接唤起本地应用

文章目录分析实现参考很多人的博客都有这样的小玩意&#xff0c;点击之后就可以直接与博主进行对话&#xff0c;而且无需添加好友。 先研究一下网页源代码&#xff1a; <a href"tencent://message/?uin88888888&Siteqq&Menuyes">联系我</a>很明…

Decoupled Knowledge Distillation(CVPR 2022)原理与代码解析

paper&#xff1a;Decoupled Knowledge Distillationcode&#xff1a;https://github.com/megvii-research/mdistiller/blob/master/mdistiller/distillers/DKD.py背景与基于响应logits-based的蒸馏方法相比&#xff0c;基于特征feature-based的蒸馏方法在各种任务上的表现更好…

【教学典型案例】14.课程推送页面整理-增加定时功能

目录一&#xff1a;背景介绍1、代码可读性差&#xff0c;结构混乱2、逻辑边界不清晰&#xff0c;封装意识缺乏![在这里插入图片描述](https://img-blog.csdnimg.cn/bbfc5f04902541db993944ced6b62793.png)3、展示效果不美观二&#xff1a;案例问题分析以及解决过程1、代码可读性…

现代操作系统——Linux架构与学习

小白的疑惑 在我决定从事嵌入式&#xff08;应用层&#xff09;方面的工作时&#xff0c;我查询了大量资料该如何学习&#xff0c;几乎所有观点不约而同的都指向了学习好Linux&#xff0c;大部分工作都是在Linux环境下来进行工作的。于是我雄心勃勃的去下载Linux&#xff0c;可…

GEE开发之降雨(CHIRPS)数据获取和分析

GEE开发之降雨CHIRPS数据获取和分析1.数据介绍2.初识CHIRPS2.1 代码一2.2 代码二3.逐日数据分析和获取4.逐月数据分析和获取4.1 代码一4.2 代码二(简洁)5.逐年数据分析和获取5.1 代码一5.2 代码二(简洁)前言&#xff1a;主要获取和分析UCSB-CHG/CHIRPS/DAILY的日数据、月数据和…

一文带你入门,领略angular风采(上)!!!

话不多说&#xff0c;上代码&#xff01;&#xff01;&#xff01; 一、脚手架创建项目 1.安装脚手架指令 npm install -g angular/cli 2.创建项目 ng new my-app(ng new 项目名) 3.功能选择 4.切换到创建好的项目上 cd my-app 5.安装依赖 npm install 6.运行项目 npm start或…

32 openEuler使用LVM管理硬盘-管理卷组

文章目录32 openEuler使用LVM管理硬盘-管理卷组32.1 创建卷组32.2 查看卷组32.3 修改卷组属性32.4 扩展卷组32.5 收缩卷组32.6 删除卷组32 openEuler使用LVM管理硬盘-管理卷组 32.1 创建卷组 可在root权限下通过vgcreate命令创建卷组。 vgcreate [option] vgname pvname ...…

曹云金郭德纲关系迎曙光,新剧《猎黑行动》被德云社弟子齐点赞

话说天下大势&#xff0c;分久必合&#xff0c;合久必分。这句话经过了历史的证明&#xff0c;如今依然感觉非常实用。 就拿郭德纲和曹云金来说&#xff0c;曾经后者是前者的得门生&#xff0c;两个人不但情同父子&#xff0c;曹云金还是郭德纲默认接班人。然而随着时间的流逝&…

数据库基本概念及常见的数据库简介

数据库基本概念 【1】数据库基本概念 &#xff08;1&#xff09;数据 所谓数据&#xff08;Data&#xff09;是指对客观事物进行描述并可以鉴别的符号&#xff0c;这些符号是可识别的、抽象的。它不仅仅指狭义上的数字&#xff0c;而是有多种表现形式&#xff1a;字母、文字…

设计模式-策略模式

前言 作为一名合格的前端开发工程师&#xff0c;全面的掌握面向对象的设计思想非常重要&#xff0c;而“设计模式”是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的&#xff0c;代表了面向对象设计思想的最佳实践。正如《HeadFirst设计模式》中说的一句话&…

【Verilog】——模块,常量,变量

目录 1.模块 1.描述电路的逻辑功能 2. 门级描述 3.模块的模板​编辑 2.关键字 3.标识符 4.Verilog源代码的编写标准 5.数据类型 1.整数常量​ 2.参数传递的两种方法 3.变量 4.reg和wire的区别 5.沿触发和电平触发的区别​ 6.memory型变脸和reg型变量的区别​ 1.模块 1.描…

Mybatis一级缓存与二级缓存

一、MyBatis 缓存缓存就是内存中的数据&#xff0c;常常来自对数据库查询结果的保存。使用缓存&#xff0c;我们可以避免频繁与数据库进行交互&#xff0c;从而提高响应速度。MyBatis 也提供了对缓存的支持&#xff0c;分为一级缓存和二级缓存&#xff0c;来看下下面这张图&…