【Java】缓存与数据库:双写一致性、缓存问题

这里我讲的是缓存和数据库,以redis和mysql举例,实际上缓存包括不限于浏览器缓存、redis、memcache、本地缓存guava等等,数据库也有很多种,这里我们仅仅以较常见的redis和mysql举例。

一,缓存与数据库的查询、写入

1,双写一致性

在以数据库作为查询终点的项目里,一般在查询的时候,使用被动更新,即查询不到数据,从数据库获取数据,将其更新到缓存中。而在进行写操作的时候,如果只是依靠缓存的过期,会存在比较大的一致性问题,因此需要使用主动更新,即需要主动对缓存进行更新。而主动更新,存在双写一致性问题,下面是4种主动更新的方案。

更新缓存、更新数据库
基本上更新缓存的方案都存在一致性的问题,虽然不一致的原因不一样,但是结果都是数据一致性问题比较严重。比如这个先更新缓存,再更新数据库。如果更新数据库失败,直到数据库回滚后,将缓存数据更新为旧值之前,缓存中被更新的值都存在一致性问题,数据库写操作和回滚比较耗时,这期间缓存数据一直是不一致的,因此这个方案一般不采用。

更新数据库、更新缓存
这个方案的一致性问题是因为多线程导致的。假设a线程更新数据库后,还没有更新缓存,此时b线程更新了数据库与缓存,最后a线程更新了缓存。b线程后更新的数据库,但是最后缓存里的数据是先更新的a线程的数据,如果后面再有新的线程来查询,并且读多写少,那么数据会在相当一段时间内存在一致性问题。

删除缓存、更新数据库
这个方案相比“更新缓存、更新数据库”,即使a线程更新数据库失败了,也不会存在一致性问题,因为b线程查询时缓存没有数据会从数据库中去读数据,读到的快照都是更新成功的数据,a线程更新失败的数据并不会被读到。

这个方案的问题是,更新数据库相比查询数据库(mysql更新基于LBCC,需要加锁锁定;查询在可重复读和读已提交是基于MVCC,是非锁定的快照读,读的undolog,往往都很快)往往是比较耗时的操作,因此在a线程删除了缓存,并更新数据库未完成期间,b线程查询旧快照,并将旧数据更新到了缓存中。在客户端看来,先去更新的数据后查询的数据,但是查出来的是旧数据,因此造成一致性问题。

这种一致性问题是“删除缓存、再更新数据库”方案的固有问题,如果一定要选择这种方案,只能选择“延迟双删”方案,即在更新数据库后,进行第二次删除缓存。删除第二次缓存之前,需要先休眠 业务读取数据库时间 + 读写分离同步时间 + 少量用于保险的时间。这个方案的问题在于,评估这三个时间,比较麻烦,而且因为需要休眠影响吞吐量,不可避免的要用到线程池。如果有人硬要考虑到第二次删除可能失败,那么还需要用mq来处理第二次删除缓存,如果第二次删除缓存失败,就由mq重新推送并消费,直到重试失败,进入死信队列,由人工手动处理。

或者由canal监听mysql的binlog,来处理第二次删除缓存。由监听canal的项目来循环处理缓存,直到更新成功为止。到此,一致性终于得到了解决,但是因此带来的业务侵入、开发复杂度、架构复杂度,实在是太大了。因此这种方案一般也不考虑。在我看来这种延迟双删更接近水多了加面、面多了加水的一种方案。

如果真的对一致性有极致的要求,可以直接使用Read/Write Through或者Write Behind 架构。

更新数据库、删除缓存
这种方案也叫做cache aside模式。这种方案一定要挑刺的话,就是1、删除缓存有可能失败,2、在缓存无数据,数据库有数据的情况下,a线程从数据库查询到旧值,b线程更新数据库并删除缓存,a线程最后将旧值更新到缓存中。

对于第一个问题,由于项目的瓶颈一般是数据库,mysql数据库更新失败的可能性远大于简单快速的kv形式基于内存的redis。因此redis更新失败的可能性很小。对于第二个问题,不同于”删除缓存、更新数据库“,因为mysql查询速度远快于写入速度,所以查询线程阻塞的时间大于写入线程阻塞时间的可能性很小,再加上前提缓存无数据数据库有数据,第二个问题可能性就更小了。也就是说,这两个问题出现的概率都很小,因此一般就不会考虑可能性很小的问题,即使真的出现了这样的问题,需要我们容忍缓存数据ttl期间的一致性问题。还是那句话,如果对于一致性有着很高的要求,就不应该使用cache aside架构了。

2、Read/Write Through、Write Behind
这两种架构适用于流量比较高的场景,以缓存作为最终的查询终点。

Read/Write Through
这种架构方案,查询时,到缓存为止,不会在缓存没查到时再去查数据库,缓存就是查询的终点了;写入时,将写缓存、写入数据库放在一个事务中,确保缓存先被更新,以及数据库和缓存的一致。这种方案在数据库写入失败时,需要将缓存数据回滚为旧值,这期间数据可能被读取,出现脏读问题。这种方案适合读多写少的场景。

Write Behind
这种架构方案,查询和Read Through医院,区别在于写入时将写入数据库的操作异步化,通过最大努力通知方案,达到最终一致性,优点在于提高了系统的吞吐量、减小了RT,相比Read/Write Through写入性能更高,缺点在架构方案要更加复杂,mq虽然能够保证数据的最终一致性,但是加入了新的中间件,提高了架构复杂度。

二、缓存清理机制

时效性清理
时效性清理,以redis过期key举例,redis对于过期key,存在被动和主动两种方式。主动方式为定时任务随机抽取一定数目的key进行检测并剔除过期key,如果超过25%就立即进行下一次抽取并剔除。被动方式为被客户端调用到过期key会将过期key剔除并返回nil。

数目阈值式清理
同样以redis举例,对应redis的内存淘汰策略,当使用内存超过阈值时,进行内存淘汰,注意此时淘汰的key不一定过期。

三、缓存问题

下面的缓存问题有不少人总结过,但是大多是从单一的角度来看待的,这里我将从双写、缓存清理两个角度来一起总结这些问题。

1,缓存穿透
客户端请求了数据库中没有的数据,导致请求穿透了缓存打到了数据库。

分析:这种情况一般是客户端恶意请求,解决方案有两个:1、数据库查询为空时缓存空数据。2、使用布隆过滤器,redisson自带布隆过滤器。个人比较推荐缓存空数据,布隆过滤器存在一定错误率,且只能增加数据不能减少数据,不方便使用。

2,缓存雪崩
大量缓存突然失效,导致大量的请求,倾泻到数据库上,引起数据库压力骤增。

分析:可能是批量缓存定期ttl,同时过期导致的缓存雪崩;也有可能是内存淘汰导致批量缓存失效导致雪崩。如果是前一种情况,在业务允许的范围内,在ttl的基础上+随机一定时间即可。如果是后一种情况,建议使用lru或者lfu淘汰策略,防止热key被淘汰。

3,缓存击穿
高频率的缓存,突然失效,大量请求倾泻到数据库上。

分析:可能是热key过期了,或者是cache aside模式删除了缓存。前一个问题,可以设置key永不过期+配合缓存架构或者在重建热key时使用分布式锁。需要注意的是如果设置key永不过期,一定要需要配合read wirte through/ write behind架构使用或者基于cache aside架构在删除缓存后,重建热key时使用分布式锁。如果是后一个问题,则只能使用分布式锁。永不过期不能解决cache aside架构下击穿的问题。

参考文章:
[1],缓存模式(Cache Aside、Read Through、Write Through、Write Behind)
[2],三种缓存策略分析:Cache aside,Read/Write through,Write Back

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

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

相关文章

【node】Linux下安装node和npm

Linux下安装node和npm 下面的版本虽然安装失败了,第一次尝试不容易,只需要更换一下node的版本为v16.20.2即可安装成功,20这样的高版本对大部分linux服务器来讲还是版本太高了,GLIBC动态库不支持,升级颇为麻烦&#xff…

她是军统美女特工,色诱汉奸一把好手!一件事之后竟......

一.前言 我们在上一篇里简单了解了什么是树,以及树的一种特殊结构——二叉树。而我们对二叉树息息相关的堆进行了简单的介绍。我们知道了堆是借助二叉树中完全二叉树来实现的。它实现了二叉树的顺序存储。但对于普通的二叉树来说,顺序存储会造成空间浪费…

贪心+背包

这道题比较坑的就是我们的对于相同截止时间的需要排个序&#xff0c;因为我们这个工作是有时间前后顺序的&#xff0c;我们如果不排序的话我们一些截止时间晚的工作就无法得到最优报酬 #include<bits/stdc.h> using namespace std;#define int long long int t; int n; c…

看板项目之vue代码分析

目录&#xff1a; Q1、vue项目怎么实现的输入localhost&#xff1a;8080就能自动跳到index页面Q2、组合饼状图如何实现Q3、vue项目如何实现环境的切换Q4、vue怎么实现vue里面去调用js文件里面的函数 Q1、vue项目怎么实现的输入localhost&#xff1a;8080就能自动跳到index页面 …

内部 API 与外部 API - 这重要吗?

内部和外部 API 的受众和用途有所不同。公司的内部利益相关者使用内部 API 作为其工作角色的一部分。目标是提高内部生产力和效率。外部 API 可以产生收入&#xff0c;将公司品牌打造为开源产品&#xff0c;或者改进 API。 本文讨论内部 API 和外部 API 之间的差异。 公共 AP…

数据结构——串

语言&#xff1a;C语言软件&#xff1a;Visual Studio 2022笔记书籍&#xff1a;数据结构——用C语言描述如有错误&#xff0c;感谢指正。若有侵权请联系博主 一、串的基本概念 子串&#xff1a;串中任意连续的字符组成的子序列称为该串的子串。 主串&#xff1a;包含子串的串称…

做一个能和你互动玩耍的智能机器人之三

内容节选自英特尔的开源项目openbot的body目录下diy下的readme&#xff0c;这是一个组装和连线方式的说明文档&#xff0c;接线需要配合firmware固件使用&#xff0c;固件代码的接线柱是对应的。 body目录内部十分丰富&#xff0c;主要介绍了这个项目的背景和硬件以及如何让他…

【SQL 新手教程 4/20】关系模型 --索引

&#x1f497; 关系数据库建立在关系模型上⭐ 关系模型本质上就是若干个存储数据的二维表 记录 (Record)&#xff1a; 表的每一行称为记录&#xff08;Record&#xff09;&#xff0c;记录是一个逻辑意义上的数据 字段 (Column)&#xff1a;表的每一列称为字段&#xff08;Colu…

iOS 评论弹窗手势问题

基本原理 给slidePopupView添加一个滑动手势 并设置代理 我们在 shouldReceiveTouch 代理方法中 通过 touch.view 判断 是否是滑动tableView, 如果 touch.view 是tableView &#xff0c; isDragScrollView 就是YES, 否则就是NO 并且要 shouldRecognizeSimultaneouslyWithGestu…

Unity发布安卓解决报错: You will no longer be able to disable R8

Unity发布安卓解决报错&#xff1a; You will no longer be able to disable R8 问题描述1 版本&#xff1a;2 问题描述&#xff1a;3 分析&#xff1a; 解决方案1 升级工程2 选择安卓版本12L3 直接打包 问题描述 1 版本&#xff1a; unity 2021 2 问题描述&#xff1a; da…

go run command

文章目录 1.简介2.格式3.示例4.注意事项5.常见用途参考文献 1.简介 编译并运行 Go 程序。 2.格式 go run [build flags] [-exec xprog] package [arguments...]run 编译并运行指定的 main 包。 通常&#xff0c;main 包被指定为来自单个目录的 .go 源文件列表&#xff0c;但…

echarts实现在市级行政区点击县级行政区,显示单个县级行政区地图数据

因需兼容ie&#xff0c;此处所有变量声明都用var。如无需支持&#xff0c;可另做let修改。 这里以常州市为例,我们可以去阿里云提供的地理工具去截取地图json数据DataV.GeoAtlas地理小工具系列 点击所选区域&#xff0c;右侧会对应显示json数据&#xff0c;再次点击右侧红框内…

MySQL 索引相关基本概念

文章目录 前言一. B Tree 索引1. 概念2. 聚集索引/聚簇索引3. 辅助索引/二级索引4. 回表5. 联合索引/复合索引6. 覆盖索引 二. 哈希索引三. 全文索引 前言 InnoDB存储引擎支持以下几种常见索引&#xff1a;BTree索引&#xff0c;哈希索引&#xff0c;全文索引 一. B Tree 索引…

java如何实现接口之间的继承

java如果要实现接口之间的继承需要用到语句 interface 接口1 extends 接口2&#xff0c;接口3 一个接口可以继承多个接口 示例代码如下 interface Animal03{public String name"牧羊犬";public void info(); } interface Color{public void black(); } interface…

2024巴黎奥运会竟然用AI做这些?

人工智能将成为 2024 年巴黎奥运会的焦点&#xff0c;组织者于四月制定了《奥运会人工智能议程》&#xff0c;这是一个涵盖人工智能对奥运会未来影响的框架。 该议程体现了国际奥委会及其主要合作伙伴的承诺&#xff0c;确保在奥运会上使用人工智能来促进团结、提高可持续性并加…

从零到一使用 Ollama、Dify 和 Docker 构建 Llama 3.1 模型服务

本篇文章聊聊&#xff0c;如何使用 Ollama、Dify 和 Docker 来完成本地 Llama 3.1 模型服务的搭建。 如果你需要将 Ollama 官方不支持的模型运行起来&#xff0c;或者将新版本 llama.cpp 转换的模型运行起来&#xff0c;并且想更轻松的使用 Dify 构建 AI 应用&#xff0c;那么…

网络传输层——UDP与TCP

前言&#xff1a; 1.国际网络体系结构&#xff1a; OSI模型: open system interconnect 理论模型 1977 国际标准化组织 各种不同体系结构的计算机能在世界范围内互联成网。 应用层:要传输的数据信息&#xff0c;如文件传输&#xff0c;电子邮件等…

数据结构:队列(顺序存储和链式存储)

文章目录 1. 队列的概念和结构2. 队列的链式存储实现2.1 初始化2.2 判断队列是否为空2.3 入队列2.4 出队列2.5 取队头数据2.6 取队尾数据2.7 队列有效数据的个数2.8 打印队列数据2.9 销毁2.10 源代码 3. 队列的顺序存储实现(循环队列)3.1 初始化3.2 判断队列是否为空3.3 判断队…

【数据结构之C语言实现动态顺序表】

引 入: 在讲顺序表之前得先了解线性表是什么&#xff1f; 线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表&#xff0c;链表&#xff0c;栈&#xff0c;队列&#xff0c;字符串…… 线性表…

Meta 发布地表最大、最强大模型 Llama 3.1

最近这一两周看到不少互联网公司都已经开始秋招提前批了。不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友解…