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

2023-05-16

关系型数据库系统给我们带来许多惊艳的特性,例如:ACID。但为了维护这些特性,数据库的性能在高负载下也会下降。为了提高性能,通常会在项目的应用层(处理业务逻辑)和存储层(持久化到数据库)之间添加一个缓存层。因为数据库的性能瓶颈通常是在硬盘(或二级存储)的读写(I/O)。所以缓存层通常使用内存来实现。当然,我们只会将一些“热数据”存储在缓存中。为识别“热数据”,通常会指定一个过期策略,如LFU(最少使用)和LRU(最近最少使用)。

将数据库中部分数据存储在内存缓存中,性能是有所提高,数据库的服务器压力也相应的减小,但一份数据分别存储在两个地方,那我们如何来保证两边的数据是一致?

接下来,我们来看下一些解决方案。

设置缓存过期

这是最简单的一种解决方案。这种方案的做法:将数据直接写入数据库,读取数据时先从缓存中读取,如果缓存数据不存在,再从数据库中读取并写回缓存。在写回缓存的时候为每个数据都添加一个过期时间。

假设缓存过期时间设置为30分钟,在这时候段内,A君更新了数据,而缓存中的数据还未过期,B君读取的数据为脏数据。那如果设置成一分钟或者更短,在大流量和高并发的情况下,将会有很多缓存未能命中,且系统的性能也会大大降低,那么缓存的数据将毫无价值。这就违背了使用缓存的最初目标。

缓存的过期时间设置过长,会导致脏读,会增加数据不一致的时间;设置过短,则缓存无效。所以,很难为过期时间设置一个合适的值。

显然,这种方案不合适高并发,且更改较频繁的数据。当然,我们可以在更新数据时候增加一个更新缓存的机制。

Cache Aside

Cache Aside 就翻译成缓存备用模式。它有三种实现方式,三种方式都是围绕数据读取和写入(新增、修改、删除)。

第一种方式:

读取

  • 缓存命中:直接从缓存返回数据,不查询数据库

  • 缓存没有命中:从只读数据库获取数据,再将数据保存到缓存中

写入

  • 新增、修改、删除数据库中的数据

  • 删除缓存(始终删除而不是更新,下次读取缓存未命中时再插入到缓存)

这种方式在理论情况下,基本上可以保证一致性。当然,也有例外的情况:

  1. 假设:A君已成功更新了数据库,在删除缓存前,B君获得了缓存命中的数据,因为该缓存还未删除。这里B君读取到为脏数据,但缓存还是会被删除。后面C君会得到更新后的值。

  2. 假设:A君已成功更新数据,但是在删除缓存时进程突然中止了,则该缓存将永远不会被删除,后续 C 君将继续读取旧数据。(进程中止的原因有:更新版本,老版本的程序被中止;中止多余的服务;应用程序崩溃。)

  3. 假设:A君未能缓存命中,从数据库中获取了数据。突然,A君出现了未知的故障卡了一下,这时,B君更新数据且删除了条目。之后,A君恢复了且将旧值保存到缓存。后续C君读取的都是脏数据。

当应用程序正确操作数据时,可以尽量减少情况1和情况3的发生。比如说,情况1在更新数据库,马上删除缓存,不要做其它任何事情。而情况2避免人为发生的可能性,但程序崩溃就没办法避免了。最后情况3在从数据库中读取数据后,尽快将结果写入缓存,不要做额外的格式转换。这样可以减少不一致性发生的概率。当然也有一些无法避免的情况,如垃圾回收产生的stop-the-word。

第二种方式

第二种方式是在第一种方式在写入时颠倒下顺序。先删除缓存,再新增、更新、删除数据到数据库。

这种方式虽然解决了第一种方式带来情况1和情况2的问题,但也会出现新的问题。假设A君在更新现有值,已成功清理了缓存,在更新数据库之前,B君来获取数据,此时缓存没能命中,然后就跑去数据库获取值并写入到缓存。但数据库里的值还未更新,这时不会再删除缓存,因此C君获取仍是旧的值。

若要说这种方式与第一种方式中情况1和情况2发生的概率,这种方式会小很多。但也没能有效的改善。因此,也不是一个很好的方案。

第三种方式

第三种方式也是在第一种方式可变操作中更改了缓存写入方式。具体方式:先新增、更新、删除数据库中的数据,再对应的创建、更新、删除缓存。

这种方式也存在一定的问题。假设B君在A君之前更新了数据库,而A君在B君之前更新了缓存。最终还是会导致数据库和缓存不一致。

在多服务部署的情况这种方式发生的概率极大。因此,这也是一种糟糕的方式。

总得来说,缓存备用模式也是一种简单的实现方式,但相比设置缓存过期时间还是比较可靠。如果想进一步提高一致性,这种方案也是不够看的。

Read Through

Read Through 就翻译成只读模式。只读模式不做写入缓存,客户端始终简单从缓存中读取。缓存命中与否对客户端是透明的。如果未命中,缓存会自动从数据库中获取。

只读模式一个致命的缺点是许多缓存可能不支持。比如,Redis 就无法自动从 MySql 获取,除非自己写插件。还好 NCache 是支持,但支持的客户端还是有限。另外还有一些开源或付费版本,如果开源使用的人少,一旦出现问题就杯具。对于付费的企业版本,对于中小企业来说是一个不小的开销。因此可选择就很少了。

对此,我们还剩下自己造车轮的路子了。比如:我们可以把缓存打包为数据访问层,并通过内部API来协调缓存和数据库。这样我们不用关心缓存的类型,只要它足够快地为我们提供数据。当然这个对缓存内部API要很熟悉。

Write Through

Write Through 就翻译成只写模式。只写模式不做读取,而且由 Read Through 来完成。只写模式仅为缓存写入数据,然后以原子方式将数据同步到数据库。

很明显这是把缓存当作了关系型数据库,但是这有个问题,许多数据库具有缓存所不具备的功能,比如:缓存有ACID的保证?再者缓存也不适合数据持久性,那怕 Redis 支持 RDB 和 AOF 的持久化,但也不建议这样用。因为缓存会由于“某种原因”而丢失数据,而这数据丢失了也就没法找回了。所以也就有了 Write Behind 模式了。

Write Behind

Write Behind 模式同样没有读取数据,只管写入,但它不是以原子方式同步数据库。而是通过内部消息队列将数据异步复制到数据库。这样做不仅提高了吞吐量,还不必等复制结果。

消息队列不仅有效的保证数据持久性,也保证了一定程度的原子性和隔离性。虽然没有像关系型数据库那样完整,但基本上是可靠的。

当然,消息队列的加入会使结构变得复杂,使用消息队列也需要相应的领域知识和详细的设计及实现。

此外,消息队列可以将零碎的更新合并到批处理中,例如:在 Redis 5.0 以后的版本中提供了 Redis Steam 的功能,可以合并更改并批量更新到数据库,从而进一步提高了性能。

使用消息队列还有一个值得注意的点,一定要确保消息队列中的执行顺序,比如:数据先更新再删除与先删除再更新是具有不同的含义。显然 Write Behind 的复杂性就高很多。

另外,这里做一个延伸。可以通过解析 MySql 的 binlog,将数据库中的数据同步到 Redis 。这个主要是利用 MySql 的复制原理,用一句话来说就是:从服务器读取主服务器 binlog 中的数据,从而同步到缓存中。当MySQL中有数据写入时,我们就解析MySQL的 binlog,然后将解析出来的数据写入到Redis中,从而达到同步的效果。这种方式实质上一个复制队列,它按顺序记录所有事务,不用担心顺序一致性的问题。

Double Delete

Double Delete 是在 Cache Aside 中第二种方式的伸延,在更新完数据库后让线程稍等片刻再次清除缓存。具体如下:

读取

  • 缓存命中:直接从缓存返回数据,不查询数据库

  • 缓存没有命中:从只读数据库获取数据,再将数据保存到缓存中

写入

  • 首先删除缓存

  • 新增、修改、删除数据库中的数据

  • 等待片刻(如:500ms)

  • 再次删除缓存

Double Delete 最大目的是减少读取旧值保存到缓存中,因此在第二次清除缓存有效清除脏数据,但在等待的过程难免也会有意外的情况发生,即在这等待的时间内可能存在不一致的情况。因此如何设置等待时间也是一个难题。

有人可能会提出,可以通过异步的方式执行,这无异于给自己增加难度。

虽然不完美,但这种发生不一致的可能性比较小吧。

最后

以上这些都不能保证强一致性的要求。如果是为了强一致性,那我们必须在所有操作上实现 ACID。但这做会降低缓存的性能。这就违背我们使用缓存最初目标。当然我们可以根据自己业务情况选择合适的方案。

当一致性不是那么重要时,使用缓存过期就足够了,而且这个实现工作量比较低。比如:CDN使用的就是使用缓存过期之一。

随着一致性的需求越来越高,我们可以使用 Cache Aside 和 Double Delete。这两种基本上能满足大多数方案了。

但是,随着一致性要求的不断增加,就需要 Read Through、Write Through 和 Write Behind 这些模式来实现。虽然提高了一致性,但也要付出相应的代价。比如:需要人力去学习相应的知识来实施,且实施时间成本和之后的维护成本也会相应的增加。除些之外,还需要额外付出基础设施的费用。

当然,如果需要保证强一致,也只能使用更先进的技术了,比如:共识算法。至少目前我不会去掌握。因为我们要的是在100%正确和性能之间做权衡。也许99.9%正确就足够了,我们使用缓存的目的就是为了提高性能。

根据自己的情况选择可以实现的方案,即使是简单的方案,如果能正确实现它们,也能提高一致性。

好了,今天就到这。

祝大家学习愉快!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何保证缓存与数据库的一致性 的相关文章

  • java.lang.IllegalStateException android.media.MediaPlayer._prepare(Native Method)

    目录 错误log xff1a 原因 xff1a 错误log xff1a 1566550586 419 28478 28478 com stone stonemusic W System err java lang IllegalStateE
  • Android自定义view刷新方法

    目录 描述 描述 Android view的刷新有三个方式 xff1a span class token comment 只会触发执行onDraw方法 xff0c 只会改变绘制里面的内容 条目的绘制 span span class toke
  • Ubuntu升级后VMware缺少vmmon、vnnet

    Ubuntu升级系统后启动时 xff0c 缺少vmmon vnnet 解决方式运行以下脚本 xff0c 可以修改 span class token shebang important bin bash span VMWARE VERSION
  • XMMS乱码的解决办法

    一 X org 下 XMMS aMule 等 Gtk1 程序的中文解决 这样做以后如果还不行 那么 二 1 安装 xmms mpg123 ja 代码 sudo apt get install xmms mpg123 ja xmms mpg1
  • ConnectionRefusedError: [Errno 111] 拒绝连接

    pip3 install upgrade pip 结果 Exception Traceback most recent call last File 34 usr share python wheels urllib3 1 19 1 py2
  • 设计模式-多例模式

    参考 xff1a 设计模式之禅 目录 多例模式类图实现1 皇帝2 大臣 运行结果补充 多例模式 这种情况有没有 有 大点声 有没有 有 是 确实有 就出现在明朝 那三国期间的算不算 不算 各自称帝 各有各的地盘 国号不同 大家还记得那首诗
  • python3-socket-Demo

    python3 socket Demo 1 背景2 Demo实现2 1 查看端口2 2 服务端2 3 客户端 1 背景 想了解一下python socket编程 xff0c 学习一下 34 白月黑羽 34 的Demo xff0c 做一下记录
  • 【数据库-MySQL-从入门到精通】【学习笔记】

    数据库 01 1 MySQL安装1 1 官网下载1 2 安装1 2 1 安装失败解决方案 xff1a 2 MySQL初学2 1 数据库基础 命令行形式2 2 MySQL操作数据库和数据表2 3 MySQL数据表基本数据类型 鸣谢 xff1a
  • Android7~8.1源码编译失败(Communication error with Jack server (35), try ‘jack-diagnose‘ or see Jack serve)

    目录 1 背景1 1 报错信息 2 原因2 1 分析 3 解决方案3 1 杀掉服务3 2 修改JDK配置文件 xff0c 移除可能导致端口占用的配置3 3 重启服务 1 背景 Android7 0 8 1编译过程中可能会出现异常报错 xff
  • android mediaplay 出现IllegalStateException的几种可能性及解决办法

    1 错误log java lang IllegalStateException at android media MediaPlayer setDataSource Native Method at android media MediaP
  • 创建.xml的矢量图片;使用Android studio 和 SVG图 生成.xml矢量图标

    Android开发中 xff0c 为什么要使用矢量图标 xff1f 使用矢量图标有什么好处 xff1f 如果使用 png xff1b jpg 这样的图片 xff0c 一般在资源文件中 xff0c 都需要准备不同分辨率的图 这样既让apk臃肿
  • 设计模式-单例模式

    本文章参考慕课DocMike老师的讲解 xff0c 作为个人笔记 xff0c 也希望能帮到需要的人 1 单例模式 单例模式 xff08 Singleton Pattern xff09 是 Java 中最简单的设计模式之一 这种类型的设计模式
  • Android studio 3 gradle配置问题

    目录 问题描述原因解决方法1 xff09 使用低版本的三方依赖库2 xff09 手动声明 xff0c 排除高版本的依赖参考文章 问题描述 Duplicate class android support design widget Coord
  • 51单片机定时器中断按键消抖(无延时)

    单片机入门学习记录 xff08 二 xff09 在机械按键的触点闭合和断开时 xff0c 都会产生抖动 xff0c 为了保证系统能正确识别按键的开关 xff0c 就必须对按键的抖动进行处理 按键的抖动对于人类来说是感觉不到的 xff0c 但
  • Ubuntu常用命令

    目录 更新仓库命令查看软件依赖包安装软件定时查看某个命令查找文件查找文件中的内容 grep 将命令行中输出内容保存文档scp通过ssh连接复制文件修改环境变量删除指定路径下包含某个关键字的文件与文件夹压缩解压查看运行信息远程桌面连接Wind
  • C#: WMI 获取远程 Windows 主机信息

    起步文档 xff1a WMI 基本介绍 WMI调用基本步骤 一个简单的远程访问例子 xff1a xff08 参考自MSDN How To Connect to a Remote Computer xff09 span class hljs
  • 端到端是什么意思?

    不久前 xff0c 燕姐 表扬了我 原话是 xff1a 像你这样端到端负责的人现在越来越少了 哈哈 xff0c 听到这话 xff0c 还是有点高兴的 xff0c 今天我来闲扯一下端到端 客户需要一个求立方差的系统 假设是fun系统 xff1
  • 电磁波和声波对比实验

    如图 xff0c 电话拨通 xff0c 能听到两个手机的声音 不断对右边的罩子进行抽气 xff0c 右边手机的声音越来越小 抽成真空的时候 xff0c 右边手机的声音消失 xff0c 但左边手机仍然如初 此时 xff0c 右边手机发送的信号
  • eclipse用MVC模式编写简单的JSP登录界面(一)

    刚开始接触JSP xff0c 打算写写博客记录记录 xff0c 大佬可以不用看了 1 JSP 在编写登录界面之前需要安装服务器 xff08 这里使用的是Tomcat xff09 并且安装IDE以及进行相关的部署 这里就不进行赘述了 xff0
  • seata

    Seata 1 seata概述 1 1 Seata简介 Seata 是一款开源的分布式事务解决方案 xff0c 致力于提供高性能和简单易用的分布式事务服务 Seata 将为用户提供了 AT TCC SAGA 和 XA 事务模式 xff0c

随机推荐

  • git clone出现fatal: HTTP request failed --git版本问题

    当git版本低于2 0版本时 xff0c 在push或clone代码时容易出现 fatal HTTP request failed 的问题 当前 xff0c git的最新版本是2 33 1 但是 xff0c 当我按官网提示 xff0c 用
  • 层次狄利克雷过程HDP(Hierarchical Dirichlet Processes)

    HDP本质是一个聚类算法 xff0c 自动决定聚类的个数 HDP HMM也是一个聚类算法 xff0c 自动决定HMM的隐状态的个数 xff0c 以每个隐状态作为一个聚类 LDA是主题模型 xff0c 可以被用作聚类算法 HDP也是个主题模型
  • vscode离线安装插件方法

    在实际工作中 xff0c 由于大多开发环境为内网开发 xff0c 无法连接外网 xff0c 需要进行离线安装相应插件 xff0c 此文用于记录vscode离线安装插件方法 1 方法一 xff1a 到vscode官网 https market
  • AD--------简单规则的设定

    这学期打了好多块板子 xff0c 都是在大佬的帮助下弄得 xff0c 嘿嘿嘿 xff0c 以后得多多练习 AD的规则设定 xff0c 反正对于英文不好的我来说还是比较难得 xff0c 但是现在画的板子规则设定都比较简单 rules 最小间距
  • linux系统编程中的信号量--模拟生产者与消费者

    FileName producer and customer c description This app demonstrates how to use the semaphore solve the problem about the
  • MySQL数据库索引相关知识

    目录 定义重点 存储原理B TreeB 43 TreeMyISAMInnoDB主键使用自增整形主键联合索引 原则那些情况应当创建索引不适合见索引 定义 索引时帮助MySQL高效获取数据的数据结构 简单说 xff1a 排好序的快速查找数据结构
  • 解决Mac M1环境下使用Goland debug失败的问题

    问题描述 xff1a 在m1环境下 xff0c 使用GoLand工具 xff0c 项目可以正常Run xff0c 但无法Debug运行 error could not launch process can not run under Ros
  • 解决“java.sql.SQLException: Expression #1 of ORDER BY clause is not in SELECT list,references column”

    在一次跑项目的时候 xff0c 报了这个错 分析原因 xff1a 百度发现是Mysql5 7及以上版本默认将 sql mode 的 ONLY FULL GROUP BY 模式设置为打开状态 解决办法 xff1a 1 将数据库换回5 6及以下
  • Lottie动画使用及原理分析

    1 Lottie是什么 xff1f Lottie是Airbnb开源的一个动画渲染库 xff0c 支持多平台 xff0c 包括iOS Android React Native以及Flutter xff08 https github com a
  • Windows Vista 下载

    Windows Vista 下载 简介 xff1a 2005年7月22日 xff0c 微软宣布 Windows Vista 为这款新操作系统的名字 微软于2006 年11月2日完成GA版本 xff0c 向OEM和企业用户发布 2007年1月
  • ubuntu 安装VS 转

    以下文字转 在ubuntu 安装VS 人生不过一闭一睁的博客 CSDN博客 ubuntu安装vs2017 able of Contents 一 前言 二 安装过程 1 下载VS Code 2 安装过程 3 下载C 43 43 模块 4 汉化
  • SQL2000 好书 《SQL Server 2000数据库管理与开发技术大全》----求是科技 人民邮电出版社

    SQL2000 好书 SQL Server 2000数据库管理与开发技术大全 求是科技 人民邮电出版社
  • 小米 pro 笔记本拆机-加固态

    前言 小米 pro 笔记本 256G 的固态 xff0c 有点不够用 xff0c 因此想加装固态 网上一打听 xff0c 拆机加固态售后要 100 元人民币 这哪行呀 不能这么便宜小米了 xff0c 100块我都不给你 xff01 准备工作
  • android四大组件之Activity - (2)onNewIntent()的作用

    要说onNewIntent 就不得不提到Activity的四种启动模式 分别是 1 standard 标准模式 也是系统默认的模式 每次都会新建Activity放置任务栈中 2 singleTop 模式 这个模式能够确保每次使用的Activ
  • 解决谷歌无法加载扩展程序

    方法一 1 先将下载的文件 crx格式修改为 zip 2 然后解压zip格式文件 3 选择加载解压过的zip文件 即可 方法二 1 在Google Chrome浏览器的桌面快捷方式上鼠标右键 xff0c 选择属性 R xff0c 进入如下界
  • 好玩的CMD几个命令

    1 msg命令 如果是在局域网中使用msg命令可以达到恶作剧的效果 msg server 192 168 1 26 东东是个人物 xff01 server 这里输入要发送人的IP地址 后面是输出的文字 2 Nslookup 检查网站的ip地
  • MySQL数据库使用相关语句

    目录 MySQL数据库的安装位置创建命令建库查看插入 编码格式配置文件修改数据库外网权限索引 MySQL数据库的安装位置 etc my cnf mysql配置文件 usr bin 客户端程序和脚本 usr sbin mysqld 服务器 v
  • C++筛法求素数

    假定题目为输出n以内的所有素数 一般方法 最容易理解的一个方法 xff0c 从0遍历到根号n判断n是否能被整除 使用时只需要记住判断到根号n就可以了 但是时间复杂度是o xff08 n sqrt xff08 n xff09 xff09 xf
  • 七 对话框

    1 模态与非模态对话框 模态对话框创建 CTestDlg dlg dlg DoModal 非模态对话框的创建 CTestDlg pDlg 61 new CTestDlg pDlg gt Create IDD DIALOG1 this pDl
  • 如何保证缓存与数据库的一致性

    关系型数据库系统给我们带来许多惊艳的特性 xff0c 例如 xff1a ACID 但为了维护这些特性 xff0c 数据库的性能在高负载下也会下降 为了提高性能 xff0c 通常会在项目的应用层 xff08 处理业务逻辑 xff09 和存储层