数据库与缓存一致性的解决方案

news2024/9/25 23:24:59

数据库和缓存的数据一致性问题一直是老生常谈的话题了,它不仅在面试中十分常见,而且在实际开发中也是需要加以考量的因素。借着难得的空暇时光(其实是晚上不太想写代码),笔者今天想和大家简单讨论一下,数据库和缓存的数据一致性问题,以及如何避免or解决数据库和缓存的数据不一致的问题。

为什么要引入缓存?

在我们的后端系统没有引入缓存之前,我们的后端系统大致来讲应该是类似于这样的模型。

在这里插入图片描述

应用服务器中的数据库驱动通过网络I/O向数据库发增删改查操作的请求,数据库通过硬盘I/O在硬盘读/写相应数据之后,再通过网络I/O返回给应用服务器的数据库驱动,然后完成后续的业务操作。

这个模型在请求量较小的时候,是行得通的。但是当请求量变大,就会出现性能问题,而这个性能问题的瓶颈就出在硬盘I/O上。

我们都知道,内存的读写速度大于磁盘的读写速度,那么我们可以考虑将一些数据放到内存中,这样就能有效加快整体的响应速度。此时的模型应当是这样的。

对于缓存,我们有以下几个疑问:

  1. 对于缓存中存放的数据,是存放全部数据,还是存放热点数据?
  2. 假如数据库中的数据发生了更新/删除,缓存中对应的数据要怎么处理?

最简单的思路是,将数据库中的所有数据全部刷到缓存,然后定时将数据库的数据再次刷到缓存里面

但是这又带来了两个问题:

  1. 缓存的利用率不高。因为有些非热点数据也被放进了缓存,但是这些数据正如其名,很少被访问,所以缓存的利用率并不高
  2. 定时刷新会带来数据库和缓存的不一致问题。因为这里用了定时刷新缓存的方案,假如数据库中的数据发生了更新/删除,那么需要等到下一次刷新缓存才能把缓存中的数据更新为新的数据。在更新数据库到刷新缓存之间的窗口期就是数据不一致的窗口期,窗口期越长,数据不一致带来的负面影响就越大。

因此这个方案实际上无人采用,我们更倾向于下面这个方案:将数据库中的热点数据刷新到缓存中,并设置一个过期时间。对于热点数据的请求,先查看缓存中是否有对应的数据,如果没有再去查数据库,查询到对应的数据后返回并同时写回缓存中。当数据库的数据发生更新/删除时,缓存中对应的数据也需要做对应操作。(如果这个数据存在的话)

在这里插入图片描述

这样,缓存中不常访问的数据,都会随着时间的推移而过期,这样缓存中保存的数据就都是热点数据了。缓存的利用率问题成功解决。

那么,数据不一致的问题呢?我们刚刚提到,当数据库的数据发生更新/删除时,缓存中对应的数据也需要做对应操作,这个操作是更新还是删除?这个操作应该发生在数据库更新/删除之前,还是发生在数据库更新/删除之后?

多级存储结构带来的数据不一致问题

对于这个问题,我们有四个选择(假设此时数据库中的数据发生了更新):

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

哪种最可靠呢?实际上四种都不大可靠。对于这四种完全不同的解决方案,我们都可以构造出可以让它们发生数据不一致问题的情形——第二步操作失败。

假设某热点数据存在于数据库和缓存中,初始的数据为10,我们需要将其更新为20。

对于方案1,第一步操作之后数据库的数据更新为20,但是如果缓存中的数据更新失败了,仍然为10,那么就发生了数据不一致。

对于方案4,第一步操作之后缓存中的数据被删除了,但是数据库中的数据更新失败,仍然为10,那么就发生了数据不一致。

方案2和方案3同样很容易构造出数据不一致的情形,以下不再赘述。

因为更新数据库和更新/删除缓存并不是一个原子操作,所以一旦第二步操作失败了,就会发生数据不一致的问题。尽管操作失败的概率非常非常低,但如同墨菲定律——“If it can go wrong, it will.",最恶劣的情况往往不会发生,但不代表一定不会发生。因此我们需要考虑第二步操作失败而带来数据不一致的问题。

在讨论如何解决第二步操作失败而带来的数据不一致之前,让我们先转移一下视线,先看向导致数据不一致的另外一个”罪魁祸首“,高并发环境

高并发环境带来的数据不一致问题

高并发环境为什么会导致数据不一致?

对于上面提到的四种解决方案,我们假设两步操作都能操作成功,假设现在是在一个多线程的环境下,情况又会如何?

如果我们采用了”先更新数据库,再更新缓存“,假设某热点数据存在于数据库和缓存中,初始的数据为10。

有线程A和线程B两个线程,都需要更新这条数据,那么可能出现如下情况:

  1. 线程A更新数据为20
  2. 线程B更新数据为30
  3. 线程B更新缓存为30
  4. 线程A更新缓存为20

两步操作都是成功的,但是还是发生了数据不一致!这是为什么?我们发现线程A的两步操作之间被插入了线程B的操作,而线程B的操作直接导致了数据库与缓存的数据不一致。

同样的,”先更新缓存,再更新数据库“的方案也会带来数据不一致,这里不再赘述。

实际上,我们没必要在更新数据库的时候同时更新缓存!从缓存利用率的角度来考虑,假设数据更新完之后,读的次数相对较少,那么缓存利用率就提不起来;而且,如果写入缓存的值并非是数据库中原始的值,而是经过了其他的计算再把值写入缓存,那么“更新缓存”的方案就会带来性能问题

因此我们考虑另外一类方案,“删除缓存”

删除缓存就一定能保证数据一致性吗?

对于“删除缓存”的方案,我们同样有两种选择:

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

我们一个一个来分析。

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

假设某热点数据存在于数据库中,初始的数据为10。有线程A和线程B两个线程,线程A需要写数据,线程B需要读数据,那么可能会出现如下情况:

​ 1)线程B读数据,由于缓存中不存在这条数据,因此读数据库中的数据

​ 2)线程B读到数据为10

​ 3)线程A更新数据为20

​ 4)线程A删除缓存

​ 5)线程B将数据10写入缓存

数据不一致由此发生。

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

假设某热点数据存在于数据库中,初始的数据为10。有线程A和线程B两个线程,线程A需要写数据,线程B需要读数据,那么可能会出现如下情况:

​ 1)线程A删除缓存

​ 2)线程B读数据,由于缓存中不存在这条数据,因此读数据库中的数据

​ 3)线程B读到数据为10

​ 4)线程A更新数据为20

​ 5)线程B将数据10写入缓存

数据不一致由此发生。

这样看来,无论哪个都没法解决数据不一致的问题,那该如何是好?那么我们只能“矮个子挑高个”,选择一个相对来讲较优的方案。我们都知道,写内存的时间短于写硬盘的时间,那么“先更新数据库,再删除缓存”显然是比“先删除缓存,再更新数据库”更优的,因为数据不一致主要发生在写线程的两步操作之间,而删除缓存的时间显然比更新数据库的时间要短得多,留给读线程的“机会”自然也就更少。

这样看来,我们似乎已经解决了高并发环境下带来的数据不一致问题。但是别忘了我们遗留在前面的一个小tip——如果第二步操作失败了(也就是删除失败),也会导致数据不一致。如何解决?

无脑重试,能解决数据不一致问题?

删除失败了,那就重试呗!

在这里插入图片描述

删除失败了,就不断尝试,直到缓存被删除。但是如果一直删除失败呢?是不是要一直尝试?重试需不需要有时间间隔?如果一直失败会阻塞这个线程,那还能接收其他客户端请求吗?这个线程资源不就被浪费了?

由此看来,无脑删除并不能解决数据不一致的问题,尤其是这种“同步”删除。基于这个方案,我们提出了一个更好的解决方案,这就是**“异步”删除**。具体来讲就是把缓存删除的任务放进消息队列,让专门的消费者来删除缓存。

引入消息队列是否会带来额外的维护成本?在我看来是不会的,因为消息队列是非常常见的中间件,不会增加维护成本;而消息队列本身可以做到持久化,在消息被消费之前一般来讲都不会丢失消息,这和“同步”删除简直是大相径庭(“同步”删除会不断地尝试删除缓存,假如此时项目重启,或服务器宕机,那么删除请求就会永久丢失,数据就永远不一致了)

引入消息队列之后的模型应该是这样的。

在这里插入图片描述

如果不想写消息队列,有无其他解决方案?

目前比较流行的解决方案就是,通过中间件来监听数据库的变更日志(如MySQL的Binlog),根据变更日志中提到的操作的数据,去缓存中删除对应的缓存。常用的中间件有阿里巴巴开源的canal,通过监听MySQL的Binlog,自动向消息队列投递变更的数据,消费者再去缓存删除对应的数据。

在这里插入图片描述

但是这个方案就需要额外维护canal,保证canal的可用性。

延迟双删策略

对于解决数据不一致问题,业界还有另外一个知名的解决方案,那就是延迟双删策略

延迟双删策略就是在原来“先删缓存,再更新数据库”的基础上,让线程休眠一段时间,再去删除对应的缓存,从而避免缓存中出现脏数据,因而保证数据一致性

那么又引出一个新的问题,“延迟双删策略”的“延迟”,具体要延迟多久?

一般来讲,延迟的时间应当大于读线程读取数据库+写入缓存的时间,但是在高并发环境下,这个时间非常难衡量,而且仍然会有概率导致数据不一致,所以一般推荐“先更新数据库,再删除缓存”,并在这个方案的基础上采用消息队列或canal来实现异步删除。

强一致还是弱一致?

什么是强一致性和弱一致性?

Bing Copilot给了我这样的答案:

强一致性和弱一致性是分布式系统中的两种数据一致性模型。让我为您解释一下它们的区别:

  1. 强一致性(也称为线性一致性):
    • 当一个系统中的数据被修改时,其他所有系统的数据都能实时地反映出这个修改。
    • 也就是说,如果在一个系统中修改了某个数据项,那么这个修改将会立即反映到其他所有的系统中。
    • 强一致性可以保证从库有与主库一致的数据,即使主库宕机,数据仍然完整。
  2. 弱一致性
    • 在系统中,数据的修改可能不会立即反映到所有系统中。
    • 也就是说,在某个系统中修改了数据项后,可能需要经过一段时间,这个修改才会在其他系统中体现出来。
    • 弱一致性允许一些暂时的不一致状态,但最终会达到一致的状态。

在一个引入缓存的系统中,我们能做到强一致性吗?其实几乎是不可能的,或者说可以做到,但是需要一点性能作为代价——引入分布式锁,但是引入分布式锁也就违背了我们引入缓存的初衷——加快系统的响应速度。所以当选择了缓存,也就默认了放弃强一致性,性能和一致性往往是不可兼得的。根据本文上面所提到的内容,我们更多的是追求“弱一致性”,也就是追求最终达到数据一致的状态。即使本文上面所提到的方法都失效了,因为缓存有过期时间可以作为一致性的兜底,即使存在数据不一致的情况,当缓存过期,也就达到了最终一致的状态,尽管这个“最终一致”看起来不太体面罢了。

总结

  1. 引入缓存可以有效加快系统响应速度。
  2. 多级缓存结构会带来数据不一致。而解决数据不一致问题我们有四种解决方案。
  3. 考虑到高并发带来的数据不一致问题,我们推荐使用“删除缓存”的策略。
  4. 对于“删除缓存”策略而言,更加推荐使用“先更新数据库再删缓存”,“延迟双删策略”看似好用,实则难以估计延迟时间。
  5. 建议采用消息队列+canal中间件监听MySQL的Binlog的方式实现异步删除缓存

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

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

相关文章

Vue实现步骤条(el-step)+Popover弹出框

1、实现效果 hover到每一个步骤条上时,如果当前有未完成情况(unFinishedMe不为空),就使用popover显示出来,如果没有hover时就不显示 2、实现思路 循环app信息列表显示多个进度条 使用el-steps 循环步骤列表&#xf…

LeetCode的高频SQL50题(基础版)学习笔记

题目在此网站 https://leetcode.cn/ 查询 # Write your MySQL query statement below select product_id from products where low_fats like Y and recyclable like Y;# Write your MySQL query statement below select name from customer where referee_id !2 or referee_i…

Java笔试面试题AI答之面向对象(7)

文章目录 37. Java成员变量与局部变量的区别有哪些?38. Java 创建一个对象用什么运算符? 对象实体与对象引用有何不同?对象实体与对象引用的不同示例 39. 类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?40. Java构造方法有…

服了!DELETE 同一行记录也会造成死锁---图文解析

服了!DELETE 同一行记录也会造成死锁!! 作者:转转技术团队 链接:https://juejin.cn/post/7387227689319563290 来源:稀土掘金 MySQL 锁回顾 共享锁 使用共享锁(Shared Lock)时&am…

模糊视频一键变清晰,从此告别模糊不清的画质

话不多说,咱们直入主题。你是不是有比较模糊的视频,比如老视频,老电影和监控视频,对了,还有日本土特产(懂的都懂),模糊的视频看起是不是很不舒服,长期久了还会影响视力影…

这些可视化Python库非常强!

介绍的大体流程是:库名、类型、github star、功能、使用方法、案例、学习资料。 第一部分:数据可视化 pyecharts 类型:可视化图表设计 GitHub Star :5985 功能: 简洁的 API 设计,使用如丝滑般流畅&am…

LLM(二):Prompt

一,什么是Prompt 在人工智能领域,Prompt指的是用户给大型语言模型发出的指令。作用是引导模型生成符合预期主题或内容的文本,从而控制生成结果的方向和内容。 大模型是根据用户提出的问题来输出下文,所以用户提出的问题的质量也…

三种智能指针

智能指针 new和delete 1:new初始化 new未初始化值 int *p new int;//p值未定义string *str new string;//为空串&#xff0c;调用string默认构造函数new 初始化值 int *p new int(100);string *str new string(6,a);//aaaaaavector类型指针 vector<int> *p new v…

pikepdf:一个实用的PDF文件处理Python库

我是东哥&#xff0c;今天给大家介绍一个实用的Python库——pikepdf&#xff0c;它能让你像操作文本文件一样轻松地处理PDF&#xff0c;无论是读取、修改还是保存&#xff0c;都能迎刃而解。 基本介绍 pikepdf是一个基于Python的库&#xff0c;它允许开发者轻松地读取、写入和…

第四课,接收键盘输入

一&#xff0c;关于基本框架中头文件的作用 头文件就是一个工具箱&#xff0c;C中有很多工具&#xff0c;我们最熟悉的cout就是其中之一 引入头文件&#xff1a;如果你想在你的代码中使用工具箱里的工具&#xff0c;C会很大方的让你用&#xff0c;但前提是你必须在本页代码的最…

为什么 CNC 加工会产生毛刺?

在现代机械加工领域&#xff0c;CNC(计算机数控)加工以其高精度、高效率的特点被广泛应用。然而&#xff0c;在 CNC 加工过程中&#xff0c;毛刺的产生常常是一个令人困扰的问题。时利和将解析为什么 CNC 加工会产生毛刺呢? 一、刀具磨损 刀具在长时间的使用过程中会逐渐磨损。…

如何一步快速去除黑神话悟空图片上的文字?一招教会你

设计师朋友们&#xff0c;如果老板让你用去除海报上的文字&#xff0c;你会怎么做&#xff1f; 用PS的内容识别填充&#xff0c;图片就会变模糊再精修简直太麻烦啦&#xff01; 还好我最近找到一个图片处理神器&#xff0c;一键就能P去图片的文字&#xff01;简单又高效&…

day04-面向对象-常用API时间Arrays

一、常用API 1.1 StringBuilder类 StringBuilder类代表可变的字符串对象&#xff0c;理解为一个操作字符串的容器相对于String来说,比本来的操作效率更高 ​ StringBuilder常用方法public StringBuilder(): 构建一个空的可变字符串对象public StringBuilder(String str): 构建…

vue3中vite基于vite-plugin-html的多入口打包

先看打包效果 1、安装vite-plugin-html 2、配置多个入口 每个入口都要有模板(index.html、App.vue、main.js复制一份&#xff0c;根据实际需求调整三个文件) 3、配置vite.config.js 4、代码片段 import { createHtmlPlugin } from vite-plugin-htmlconst htmlParams {minif…

关于springboot的Rest请求映射处理的源码分析(一)

我们在开发中很常见的一种方式是通过请求的类型&#xff0c;也就是restful风格来区别我们的请求接口。 通过请求类型的不同(GET POST PUT DELETE) 来区分即便是请求路径相同的请求。 但是他的底层是如何支持的呢&#xff0c;明明我请求路径都是/user。就因为类型不同就能区分到…

网络层 III(划分子网和构造超网)【★★★★★★】

&#xff08;★★&#xff09;代表非常重要的知识点&#xff0c;&#xff08;★&#xff09;代表重要的知识点。 一、网络层转发分组的过程 分组转发都是基于目的主机所在网络的&#xff0c;这是因为互联网上的网络数远小于主机数&#xff0c;这样可以极大地压缩转发表的大小。…

【Python报错】AttributeError`:`‘NoneType‘ object has no attribute ‘XXXX‘`

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引言 在Python编程中&#xff0c;AttributeError是一个常见的错误类型&#xff0c;它表示尝试访问的对象没有该属性。本文将探讨…

深度强化学习算法(六)(附带MATLAB程序)

深度强化学习&#xff08;Deep Reinforcement Learning, DRL&#xff09;结合了深度学习和强化学习的优点&#xff0c;能够处理具有高维状态和动作空间的复杂任务。它的核心思想是利用深度神经网络来逼近强化学习中的策略函数和价值函数&#xff0c;从而提高学习能力和决策效率…

CNN网络的一些基本知识

CNN 网络的layer介绍 在卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;中&#xff0c;往往包含许多种不同的网络层交替组成&#xff0c;主要有卷积层&#xff08;Convolutional Layer&#xff09;、池化层&#xff08;Pooling Layer&#…

《黑神话:悟空》爆火,对程序员的 5 点启示(2)

前言 继续上篇未完章节…… 4. 需求捕捉 需求有真需求和伪需求的区别&#xff0c;捕捉和理解用户的真需求对于产品非常重要。 在《黑神话&#xff1a;悟空》面世以后&#xff0c;很多玩家都不吝称赞&#xff0c;有玩家这么评论&#xff1a; 不吹牛逼&#xff0c;这一段我眼…