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

news2024/11/13 9:03:40

引入缓存提高性能

如果你的业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可,这时你的架构模型是这样的:

但随着业务量的增长,你的项目请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。

这个阶段通常的做法是,引入「缓存」来提高读性能,架构模型就变成了这样:

如何提高缓存利用率?

想要缓存利用率「最大化」,我们很容易想到的方案是,缓存中只保留最近访问的「热数据」。

  • 写请求依旧只写数据库
  • 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
  • 同时,写入缓存中的数据,都设置失效时间

缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。

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

更新缓存和数据库有四种方案:

  • 先更新缓存,再更新数据库
  • 先更新数据库,再更新缓存
  • 先删除缓存,后更新数据库
  • 先更新数据库,后删除缓存

更新缓存

优点:
如果每次数据变化都能被及时更新,那么查询数据时不容易出现不命中的情况,
缺点:
1、如果数据的计算复杂,频繁的更新会造成服务器性能的消耗比较大
2、如果数据并不是被频繁使用,那么频繁更新也只是浪费服务器性能,对业务没有多大的帮助

删除缓存

优点:不需要顾忌数据的复杂性,直接删除即可
缺点:查询数据时,增大未命中的几率,从而增大数据库的访问压力
适用于数据使用频率不高的场景

先更新缓存,再更新 DB

这个方案一般不考虑。原因是更新缓存成功,更新数据库出现异常了, 导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。

先更新 DB,再更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。这种方案一般不考虑。

并发引发的一致性问题

同时有请求 A 和请求 B 进行更新操作,那么会出现

(1)线程 A 更新了数据库

(2)线程 B 更新了数据库

(3)线程 B 更新了缓存

(4)线程 A 更新了缓存

线程1 虽然先于 线程2 发生,但 线程2 操作数据库和缓存的时间,却要比 线程1 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。

如果是写多读少的场景,采用 这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

先删除缓存,后更新 DB

如果出现这样情况:
1、线程A更新数据库中的数据
2、线程A删除缓存中的数据,删除失败
3、线程B查询缓存中的数据,查询到旧数据
4、线程A异步重试删除缓存
这里,删除缓存中数据失败后就会造成线程B获取到缓存中的旧数据,从而导致数据不一致的情况。

还有并发情况:

来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;

此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,更新到Redis 中;

但是此时请求 A 并没有更新成功,或者事务还未提交,请求 B 去数据库查询得到旧值;

可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

如何解决呢?其 实最简单的解决办法就是延时双删的策略。

(1)先淘汰缓存

(2)再写数据库

(3)休眠 1 秒,再次淘汰缓存 这么做,可以将 1 秒内所造成的缓存脏数据,再次删除。

那么,这个 1 秒怎么确定的,具体该休眠多久呢? 针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数 据的休眠时间则在读数据业务逻辑的耗时基础上,加几百 ms 即可。这么做的目 的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

如果Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

请求 A(更新操作) 和请求 B(查询操作)

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1)

此时的解决办法有两个:

1、还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百 ms。

2、就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

这个「延迟删除」缓存,延迟时间到底设置要多久呢?

  • 问题1:延迟时间要大于「主从复制」的延迟时间
  • 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间

但是,这个时间在分布式和高并发场景下,其实是很难评估的

很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。

所以你看,采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。

所以实际使用中,建议采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。

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

先分析有失败情况:
1、线程A更新数据库中的数据
2、线程A删除缓存中的数据,删除失败
3、线程B查询缓存中的数据,查询到旧数据
4、线程A异步重试删除缓存
这里,删除缓存中数据失败后就会造成线程B获取到缓存中的旧数据,从而导致数据不一致的情况

假设这会有两个请求,一个请求 A 做查询操作,一个请求 B 做 更新操作,那么会有如下情形产生

(1)缓存刚好失效

(2)请求 A 查询数据库,得一个旧值

(3)请求 B 将新值写入数据库

(4)请求 B 删除缓存

(5)请求 A 将查到的旧值写入缓存

其实概率「很低」,这是因为它必须满足 3 个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存(步骤 2 和 5)时间短

仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

**更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功怎么办?**这个问题,在删除缓存类的方案都是存在的,那么此时再读取缓存的时候每次都是错误的数据了。

此时解决方案有两个,一是就是利用消息队列进行删除的补偿:

1、请求 A 先对数据库进行更新操作

2、在对 Redis 进行删除操作的时候发现报错,删除失败

3、此时将 Redis 的 key 作为消息体发送到消息队列中

4、系统接收到消息队列发送的消息后

5、再次对 Redis 进行删除操作

第二种方案,订阅数据库变更日志,再操作缓存

具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。

那什么时候操作缓存呢?这就和数据库的「变更日志」有关了。

拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。

想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做

总结

引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」

更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,且存在「缓存资源浪费」和「机器性能浪费」的情况发生

在更新数据库 + 删除缓存的方案中,「先删除缓存,再更新数据库」在「并发」场景下依旧有数据不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估,所以推荐用「先更新数据库,再删除缓存」的方案

在「先更新数据库,再删除缓存」方案下,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据一致性

在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。

掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题。

对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

如果不能容忍缓存数据不一致,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的 时候相当于无锁。

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

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

相关文章

期中考成绩一键私发

作为一名教师,期中考试后最繁忙的事情之一就是发布成绩。每个学生都希望尽快知道自己的成绩,而作为老师,我们需要一种更高效、更方便的方式来完成这项任务。今天,我就来给大家介绍一种成绩查询系统,让我们一起告别繁琐…

【接口自动化测试框架】YAML管理接口框架配置的最佳实践

管理接口框架配置是构建强大的接口测试框架的关键一环。良好的配置管理可以提高测试效率、可维护性和可扩展性。在本文中,我们将重点介绍使用YAML(YAML Ain’t Markup Language)来管理接口框架配置的最佳实践,并通过实例演示其用法…

肺癌不再是老年病:33岁作家的离世引发关注,有这些情况的请注意

近期,90后网络小说家七月新番和26岁男艺人蒋某某因肺癌去世,引发关注。他们都没有吸烟习惯,那么他们为什么会得肺癌呢?浙大二院呼吸内科副主任医师兰芬说,现在年轻人熬夜、加班导致身体过劳,在劳累情况下身…

【嵌入式】Linux C编程——C要注意的东西

1、语法分析中的“贪心法”: 编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分&…

java_stream流

stream 流 文章目录 1. stream 流初体验2. 获取 Stream 流2.1 单列集合 获取 stream 流2.2 双列集合 获取 Stream 流2.3 数组获取 stream 流2.4 一堆零散数据获取 Stream 流 3. Stream 流的中间方法3.1 filter 过滤方法3.2 limit 和 skip 方法3.3 distinct 去重方法3.4 cancat…

计算机网络第3章-TCP协议(2)

TCP拥塞控制 TCP拥塞控制的三种方式: 慢启动、拥塞避免、快速恢复 慢启动 当一条TCP连接开始时,cwnd的值是一个很小的MSS值,这使得初始发送速率大约为MSS/RTT。 在慢启动状态,cwnd的值以1个MSS开始并且每当传输的报文段首次被…

4.2 SSAO算法 屏幕空间环境光遮蔽

一、SSAO介绍 AO 环境光遮蔽,全程Ambient Occlustion,是计算机图形学中的一种着色和渲染技术,模拟光线到达物体能力的粗略的全局方法,描述光线到达物体表面的能力。 SSAO 屏幕空间环境光遮蔽,全程 Screen Space Amb…

Android 处理多个TextView, 文案过长时前面文本省略的问题

遇到显示多个TextView,文案过短时,这几个TextView跟随显示,文案过程时,前面TextView省略,后个的TextView全显示。效果如下: 用ConstraintLayout 没有得到解决,采用 RelativeLayout 解决 代码如…

【小黑嵌入式系统第六课】嵌入式系统软件设计基础——C语言简述、程序涉及规范、多任务程序设计、状态机建模(FSM)、模块化设计、事件触发、时间触发

上一课: 【小黑嵌入式系统第五课】嵌入式系统开发流程——开发工具、交叉开发环境、开发过程(生成&调试&测试)、发展趋势 文章目录 一 单片机的C语言简述1、为什么要用C语言?2、单片机的C语言怎么学?之一:变量定义之二&am…

高并发和存储之间的关系是什么?

文章目录 🔊博主介绍🤖博主的简介📥博主的目标 🥤本文内容🍊 一、高并发对存储的压力🍊 二、存储的性能和可扩展性 📢总结 🔊博主介绍 📕我是廖志伟,一名Java…

【JAVA学习笔记】53 - 集合-List类及其子类Collection、ArrayList、LinkedList类

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter14/src/com/yinhai/collection_ https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter14/src/com/yinhai/list_ 集合 一、集合引入 前面我们保存多个数据使用的是数组…

thinkphp链接mqtt服务器,然后在订阅下发布消息

cmd打开项目根目录&#xff0c;安装插件&#xff0c;执行下面的命令 composer require php-mqtt/client执行完成之后会在vendor 目录下有php-mqtt 文件 然后在你的 extend文件下 新建mqtt文件 在文件中新建 Mqtt.php 下面是代码 <?php /** S: * Name: 控制器: * Autho…

金蝶云星空创建自动下推并保存公共服务

文章目录 金蝶云星空创建自动下推并保存公共服务创建公共方法按单下推数据按明细行下推数据调用下推操作 调用公共方法 金蝶云星空创建自动下推并保存公共服务 创建公共方法 按单下推数据 /// <summary>/// 获取单据转换数据包/// </summary>public DynamicObjec…

“证意联盟”聊聊亚马逊云科技认证的价值和意义(文末有福利)

云职场“卷”人都在干嘛&#xff1f;通勤途中刷刷线上课程&#xff0c;每天提升一点云技能&#xff1b;周末时间做做官方题库&#xff0c;每周增加一点考试通过的概率&#xff1b;月底试试模拟考&#xff0c;每月加强一点信心&#xff1b;年末准备充分考取亚马逊云科技认证&…

Java获取指定时间一周至周日的日期

Java获取指定时间一周至周日的日期&#xff1a; /*** 获取指定时间 当前周的周一至周日的时间* return*/public static List<String> getWeekData(Date dataTime){/*** 转为calendar格式* calendar.get(Calendar.MONTH)1 calendar中的月份以0开头* Calendar.DAY_OF_WEE…

5分钟带你认识web自动化测试

1.什么是自动化测试&#xff1f; 自动化测试的概念: 软件自动化测试就是通过测试工具或者其他手段&#xff0c;按照测试人员的预定计划对软件产品进行自动化测试&#xff0c;他是软件测试的一个重要组成部分&#xff0c;能够完成许多手工测试无法完成或者难以实现的测试工作&a…

【3ds max】给指定的面设置材质

1. 首先将物体转换为可编辑多边形 2. 选中需要赋予材质的面 按m键弹出材质编辑器 点击“将材质指定给选定对象”

4.1 Bloom算法

一、Bloom算法介绍 1.具体效果 2.实现思路 先将原图按照一定阈值提取较亮的区域模糊提取出的图像将模糊过的图像与原图混合 3.HDR与LDR LDR&#xff08;Low Dynamic Range&#xff0c;低动态范围&#xff09; JPG、PNG格式图片RGB范围在[0,1]之间 HDR&#xff08;High Dynam…

SpringCloud(四) Nacos注册中心

目录 一, Nacos 1.1 Nacos的安装 1.2 服务注册到Nacos 1, 引入依赖 2, 配置Nacos地址 3, 重启 4, 进行访问 二, 服务分级存储模型 2.1 分级模型 2.2 Nacos的集群配置 1, 给user-service配置集群 2, 同集群优先的负载均衡 2.3 权重配置 国内公司一般都推崇阿里巴巴…

【嵌入式】HC32F07X CAN通讯配置和使用

目录 一 背景说明 二 原理分析 三 CAN通讯硬件设计 四 CAN通讯软件配置 五 CAN通讯测试 一 背景说明 使用小华&#xff08;华大&#xff09;的MCU HC32F07X实现 CAN 通讯配置和使用 二 原理分析 【1】CAN原理说明&#xff08;参考文章《CAN通信详解》&#xff09;&#x…