当前位置: 首页 > news >正文

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

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

相关文章:

  • django(一):项目搭建
  • python-pptx - Python 操作 PPT 幻灯片
  • Node.js中离线集成ip2region:实现高效的IP地址定位
  • 【网络安全】服务基础第一阶段——第六节:Windows系统管理基础---- DNS部署与安全
  • 单片机的主流编程语言是什么
  • Vue的计算属性:methods方法、computed计算属性、watch监听属性
  • 拿到一个新项目,如何开展测试?
  • 应对Nginx负载均衡中的请求超时:策略与配置
  • Mysql在服务器中的源码部署
  • sqli-labs靶场通关攻略(41-50)
  • 如何通过日志或gv$sql_audit,分析OceanBase运行时的异常SQL
  • Linux: SPI应用编程
  • OpenCV 100道面试题及参考答案(7万字长文)
  • 错误: 找不到或无法加载主类 App.class,Java文件是怎么编译的
  • Android12 添加设置控制导航栏显示和状态栏下拉
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • 11111111
  • iOS小技巧之UIImagePickerController实现头像选择
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Laravel 中的一个后期静态绑定
  • MySQL的数据类型
  • nodejs:开发并发布一个nodejs包
  • Python进阶细节
  • Spark学习笔记之相关记录
  • uva 10370 Above Average
  • 给新手的新浪微博 SDK 集成教程【一】
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 记一次用 NodeJs 实现模拟登录的思路
  • 悄悄地说一个bug
  • 驱动程序原理
  • 容器服务kubernetes弹性伸缩高级用法
  • 如何邀请好友注册您的网站(模拟百度网盘)
  • 网页视频流m3u8/ts视频下载
  • 用Canvas画一棵二叉树
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • ![CDATA[ ]] 是什么东东
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (1)svelte 教程:hello world
  • (7)摄像机和云台
  • (C语言)字符分类函数
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (poj1.2.1)1970(筛选法模拟)
  • (二)windows配置JDK环境
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (亲测)设​置​m​y​e​c​l​i​p​s​e​打​开​默​认​工​作​空​间...
  • (三)Honghu Cloud云架构一定时调度平台
  • (一) storm的集群安装与配置
  • (已解决)什么是vue导航守卫
  • (正则)提取页面里的img标签
  • (转)Linux下编译安装log4cxx