【专栏】核心篇09| 怎么保证缓存与DB的数据一致性

news2024/9/22 4:21:52

计算机类PDF整理:【详细!!】计算机类PDF整理

 Redis专栏合集

【专栏】01| Redis夜的第一章

【专栏】基础篇02| Redis 旁路缓存的价值

【专栏】基础篇03| Redis 花样的数据结构

【专栏】基础篇04| Redis 该怎么保证数据不丢失(上)

【专栏】基础篇05| Redis 该怎么保证数据不丢失(下)

【专栏】核心篇06| Redis 存储高可用背后的模式

【专栏】核心篇07| Redis “jio”美的集群模式

【专栏】实践篇08| All in docker!动手搭建Redis集群

欢迎关注公号:【离心计划】,逃离技术舒适圈

前言

    在平常工作中,我们使用Redis作为缓存时,只有两种模式:只读缓存读写缓存,两者的区别就是是否接收写请求。而无论是哪种方式,因为缓存和DB的同时存在,我们的基本工作模式就是:请求来了先去缓存中拿,如果缓存中有数据就返回,没有数据就查DB。而查完DB后读写缓存的做法是update缓存数据,只读缓存的做法是delete缓存数据。是update还是delete是两者的手法上的基本区别。而当更新请求来的时候,比如库存加减,那么读写缓存就是要做两步:update缓存、update数据库,只读缓存则是:delete缓存、update数据库。

    因此我们今天讨论的问题就是在两种模式下,数据的增删改查出现缓存与数据库数据不一致的问题,为了更清楚,我们只拿只读缓存举例,最后对比一下两者即可。

操作失败导致的不一致

    只读缓存下我们做的两步分别是delete缓存和update数据库,那么两步操作必然有个先后顺序,如果先删除缓存再更新数据库,那么如果删除缓存成功,更新数据库失败,那么就会导致下一次读取数据库会读到旧值导致数据不一致;如果删除缓存失败,更新数据库成功,那么请求依旧会读到缓存中的旧值导致数据不一致。而交换删除缓存和更新数据库的位置,也是一样的情况,因此我们得出结论:操作失败会导致数据不一致问题

时间

线程A(写)

线程B(读)
t1删除缓存
t2更新DB(失败)
t3缓存缺失查询DB
t4更新缓存,脏数据

那么操作失败,在我们代码中属于异常捕获的一部分,我们常见的机制就是补偿,这边的补偿就是重试。重试我们需要考虑两个问题

  1. 同步还是异步

  2. 重试多少次

    同步重试意味着这次请求必然会更耗时,而且重试的次数比较多时耗时更严重,因此我们毅然决然采用异步重试。那么异步重试我们是起异步线程重试么,为了保证最终的一致性,异步线程还得保证一直重试直到成功,如果此时应用重启那么这条重试就永远失效了。这里存在本质的问题就是,“重试”这个操作需要被可靠执行,所谓可靠执行就是操作一旦生成不会消失,并且一定会被执行直到成功。

    所以,消息队列就是符合我们的场景,我们可以在操作失败后生产一条消息告诉消费者需要重试,因为消息队列保证了消息的可靠投递与可靠消费,它具有两个特性:

  1. 消息一旦生产,只有被所有消费者消费成功后才会删除

  2. 消息只有被消费成功后才会返回ack,否则一直会投递给消费者

    另外,还有一种异步策略,是将消息机制下沉到DB层,也就是利用一些中间件比如canel订阅Mysql binlog这种方式,这样只要写数据库成功了,就能订阅到binlog,消费者只要刷新缓存就行了。这种方案的优势在于,应用层不用主动发消息,毕竟发消息也有可能发送失败,将这种异常问题留给中间件,应用层只用关心写入DB是否成功,成功就正常进行,不成功也没有缓存数据一致性问题,直接报错就可以或者有其他降级策略。

并发导致的不一致

    我们这边只考虑特殊的读写并发,由于读读并发不会有问题,写写并发和读写并发是一样的逻辑,因此我们的前提是删除缓存与更新DB两步都能成功,由于不是原子操作导致的并发问题,现象就像下面这种情况(现象一)

时间

线程A(写)

线程B(读)
t1删除缓存
t2缓存缺失,读取DB
t3更新DB
t4更新缓存

    这样就会导致线程B更新的缓存值就是旧值,后续读到的都会是脏数据,当这种情况时我们可以在线程A更新完DB后过一小段时间再进行删除缓存,这样可以减少脏数据的影响时间,这种策略也叫延迟双删,当然其实并不建议采用这种方式,因为这种方案有个大问题就是延迟多久,固定的值在变化不断的场景下就显得心有余而力不足了。

    当然,如果我们交换一下两步的顺序,先更新DB再删除缓存,那么情况就是如下:(现象二)

时间

线程A(写)

线程B(读)
t1更新DB
t2读取到旧值返回
t3删除缓存
t4第二次读刷新缓存
 

    这样的问题是,线程B在t2时间内会返回旧值,但是这种情况会马上被线程A通过删除缓存解决掉,因此影响的时间会比先删除缓存的策略更短。

    除了这个时序,还有一种时序就是下面这样,就会导致缓存中存在的是旧值,这和先删除缓存是一样的问题,但是这种情况发生的概率会先删缓存小很多,因为首先要满足缓存正好过期,其次线程A更新DB与删除缓存的耗时要比线程B更新缓存还要快,这样下来概率就非常小了(现象三)

时间线程A(写)线程B(读)
t1缓存过期,查询DB
t2更新DB
t3删除缓存
t4更新缓存

    因此结合操作失败和读写并发,我们得出结论,先更新DB再删除缓存,并配合异步机制的方案更加稳定

从缓存利用率考虑

    这样看下来,写请求时有两种做法,删除缓存或者直接更新缓存,这两种做法看起来最终的效果一致,但是当我们从缓存利用率的角度出发,更新缓存会将所有更新DB的值都重新刷回缓存,如果大量的更新值都不会频繁读,那么这种策略就会导致大量的无效缓存,因此这种方案更适合读写流量差不多的情况下,写完后马上会被读的场景,否则,采用删除缓存的做法会更好,因为只有读时才会重刷缓存。

    可见,具体采用怎样的策略还是需要看具体应用场景,没有银弹。

强一致性怎么做

    上面我们讨论的数据一致性其实本质上是最终一致性,为了保证最后的结果是一致的就行,这种方案适合数据一致性不敏感的场景,但是非要做强一致性也就是不允许出现gap,那怎么做的呢?

    这个问题其实其实就是解决并发问题,并且是在分布式环境下的话,就需要用到分布式锁方案,考虑到现象二和现象三(上面有标记),大致的流程如下,在更新DB与查询DB时加锁,主要就是为了防止出现二三两个现象。

(图片引用自《携程最终一致和强一致性缓存实践》,见引用)

    当然,最后缓存强一致性引入的代价可能会导致性能上有所损耗,但是在缓存命中率高的情况下,依旧能够提升接口响应时间,减少一定的DB压力。这边推荐一种Redis分布式锁方案Redisson,大家感兴趣可以了解一下。

小结

    这一节我们梳理了一下在,当我们为了提升接口性能,减少DB压力时,增加了旁路缓存,但是引入了新的中间件也引入了数据不一致的问题,如果业务允许,我们可以采用最终一致性方案;如果业务不允许,则需要强一致性方案。无论怎样,缓存依旧是一把利大于弊的双刃剑,其中的各种问题不容小觑,下一节我们会继续讨论一下缓存的各种异常情况以及常见的解决方案~

引用:

  1. https://www.infoq.cn/article/hh4iouiijhwb4x46vxeo

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

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

相关文章

Python -- 模块和包

目录 1.Python中的模块 1.1 import 1.3 from...import * 1.4 as别名 2.常见的系统模块和使用 2.1 OS模块 2.2 sys模块 2.3 math模块 2.4 random模块 2.5 datetime模块 2.6 time模块 2.7 calendar模块 2.8 hashlib模块 2.9 hmac模块 2.10 copy模块 3.pip命令的使…

【机器学习---01】机器学习

文章目录1. 什么是机器学习?2. 机器学习分类2.1 基本分类2.2 按模型分类2.3 其他分类(不重要)3. 机器学习三要素4. 监督学习的应用(分类、标注、回归问题)1. 什么是机器学习? 定义:给定训练集D,让计算机从一个函数集合F {f1(x)&…

虚拟机打不开,提示“此主机不支持虚拟化实际模式”的详细解决方法

虚拟机打不开,提示“此主机不支持虚拟机实际模式”的解决方法 一、第一种情况安装/启动虚拟机失败, 在VMWare软件中,安装/启动虚拟机时,如果出以类似以下的错误提示: 出现该提示是由于电脑不支持虚拟化技术或是相关功…

IDEA报错:类文件具有错误的版本 61.0,应为52.0

springboot项目启动报错: 类文件具有错误的版本 61.0,应为52.0 请删除该文件或确保该文件位于正确的类路径子目录中 查阅了网上的很多资料,普遍原因说是springboot版本过高,高于3.0 需要在pom文件中降低版本 也有说是idea的maven配置java版…

网购商城网站

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字):

【Python机器学习】层次聚类AGNES、二分K-Means算法的讲解及实战演示(图文解释 附源码)

需要源码和数据集请点赞关注收藏后评论区留言私信~~~ 层次聚类 在聚类算法中,有一类研究执行过程的算法,它们以其他聚类算法为基础,通过不同的运用方式试图达到提高效率,避免局部最优等目的,这类算法主要有网格聚类和…

easypoi导入excel空指针异常

问题描述 前端页面停留在导入页面,通过后端返回的接口,确认后端已经抛出异常查看系统调用错误日志为 java.lang.NullPointerException: nullat org.apache.poi.xssf.usermodel.XSSFClientAnchor.setCol2(XSSFClientAnchor.java:231)at org.apache.poi.…

基于EKF的四旋翼无人机姿态估计matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 卡尔曼滤波是一种高效率的递归滤波器(自回归滤波器),它能够从一系列的不完全包含噪声的测量中,估计动态系统的状态。这种滤波方法以它的发明者鲁道夫E卡尔曼(R…

Android koin

1.源码地址 1.源码地址 2.作用 1.让代码看起来更简洁 现在是这样创建对象的 2.解耦 我们有一个类,然后有100个地方使用它,这个时候如果我们要修改构造参数,加入一个参数,那么我们就要修改100个地方;如果过了一个…

怎样让chatGPT给你打工然后月入过千?

前言 chatGPT最近火出圈了,怎么薅一个文字模型给你打工呢? 这个UP给了个思路:哔哩哔哩 emmm有点尴尬,可能是热度比较高,b站的视频作者自己下架了。 总结一下: 薅的对象百度文库创作中心:地址…

设计模式之装饰器模式

decorator design pattern 装饰模式的概念、装饰模式的结构、装饰模式的优缺点、装饰模式的使用场景、装饰模式与代理模式的区别、装饰模式的实现示例、装饰模式的源码分析 1、装饰模式的概念 装饰模式,即在不改变现有对象结构的前提下,动态的给对象增加…

【云原生】Grafana 介绍与实战操作

文章目录一、概述二、Grafana 安装1)下载安装2)安装包信息3)启动服务4)Grafana 访问三、Grafana 功能介绍四、使用mysql存储1)安装mysql2)修改grafana配置1、创建grafana用户和grafana库2、修改grafana配置…

[附源码]Python计算机毕业设计Django学分制环境下本科生学业预警帮扶系统

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

Innodb存储引擎-索引和算法(B+树索引、Cardinality、联合索引、覆盖索引、MRR优化、ICP优化、哈希索引、全文索引)

文章目录索引和算法B树索引聚集索引辅助索引B 树索引的分裂B 树索引的管理Cardinality 值B 树索引的使用联合索引覆盖索引优化器选择不使用索引的情况索引提示Multi-Range Read 优化(MRR)Index Condition Pushdown优化(ICP)哈希索引全文索引倒排索引InnoDB全文检索的实现全文检…

第一个MyBatis查询

⭐️前言⭐️ 在连接程序与数据库的工具中,我们之前使用的是JDBC技术,但是JDBC的操作流程极为繁琐,因此才有了更优秀框架——MyBatis,下边我们一起来看这个优秀框架MyBatis的操作与使用。 🍉欢迎点赞 👍 收…

Innodb存储引擎-锁(数据库锁的查看、快照读当前读、MVCC、自增长与锁、外键与锁、行锁、并发事务的问题、阻塞、死锁、锁升级、锁的实现)

文章目录锁lock 与latch读锁/写锁/意向锁INNODB_TRX/INNODB_LOCKS/INNODB_LOCK_WAITS一致性非锁定读(快照读)一致性锁定读(当前读)MVCC版本链Read View流程自增长与锁外键和锁行锁类型记录锁(record lock)间隙锁(gap lock)下一键锁(next-key lock)并发事务带来的问题阻塞死锁锁…

数据挖掘Java——DBSCAN算法的实现

一、DBSCAN算法的前置知识 DBSCAN算法:如果一个点q的区域内包含多于MinPts个对象,则创建一个q作为核心对象的簇。然后,反复地寻找从这些核心对象直接密度可达的对象,把一些密度可达簇进行合并。当没有新的点可以被添加到任何簇时…

7.加载properties属性文件

一、加载properties属性文件 目的:将数据库的连接参数抽取到一个单独的文件中,与Spring配置文件解耦 1. 编写jdbc.properties属性文件 jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/spring_db jdbc.usernameroot jdbc.passwo…

基于萤火虫算法改进的DELM预测-附代码

萤火虫算法改进的深度极限学习机DELM的回归预测 文章目录萤火虫算法改进的深度极限学习机DELM的回归预测1.ELM原理2.深度极限学习机(DELM)原理3.萤火虫算法4.萤火虫算法改进DELM5.实验结果6.参考文献7.Matlab代码1.ELM原理 ELM基础原理请参考&#xff1…

代码中的坏味道

学习笔记自https://zhuanlan.zhihu.com/p/141435233 识别代码中的坏味道系列 如下图是工作中常见的代码的坏味道: 上图中的坏味道出自《重构》这本书,虽然并不是全部,但是涵盖了日常中最常见的一些代码坏味道。 接触这些坏代码可以分为三类&…