缓存把我坑惨了..

故事

春天,办公室外的世界总是让人神往的,小猫带着耳机,托着腮帮,望着外面美好的春光神游着…

一声不和谐的座机电话声打破这份本该属于小猫的宁静,“hi,小猫,线上有个客户想购买A产品规格的商品,投诉说下单总是失败,帮忙看一下啥原因。”客服部小姐姐甜美的声音从电话那头传来。“哦哦,好,我看一下,把商品编号发一下吧…”

由于前一段时间的系统熟悉,小猫对现在的数据表模型已经了然于胸,当下就直接定位到了商品规格信息表,发现数据库中客户想购买的规格已经被下架了,但是前端的缓存好像并没有被刷新。

小猫在系统中找到了之前开发人员留的后门接口,直接curl语句重新刷新了一下接口,缓存问题搞定了。

关于商品缓存和数据库不一致的情况,其实小猫一周会遇到好几个这样的客诉,他深受DB以及缓存不一致的苦,于是他下定决心想要从根本上解决问题,而不是curl调用后门接口…

写在前面

小猫的态度其实还是相当值得肯定的,当他下定决心从根本上排查问题的时候开始,小猫其实就是一名合格而且负责的研发,这也是我们每一位软件研发人员所需要具备的处理事情的态度。

在软件系统演进的过程中,只有我们在修复历史遗留的问题的时候,才是真正意义上地对系统进行了维护,如果我们使用一些极端的手段(例如上述提到的后门接口curl语句)来保持古老而陈腐的代码继续工作的时候,这其实是一种苟且。一旦系统有了问题,我们其实就需要及时进行优化修复,否则会形成不好的示范,更多的后来者倾向于类似的方式解决问题,这也是为什么FixController存在的原因,这其实就是系统腐化的标志。

言归正传,关于缓存和DB不一致相信大家在日常开发的过程中都有遇到过,那么我们接下来就和大家好好盘一盘,缓存和DB不一致的时候,咱们是如何去解决的。接下来,大家会看到解决方案以及实战。
在这里插入图片描述

常规接口缓存读取更新

在这里插入图片描述

看到上面的图,我们可以清晰地知道缓存在实际场景中的工作原理。

  1. 发生请求的时候,优先读取缓存,如果命中缓存则返回结果集。
  2. 如果缓存没有命中,则回归数据库查询。
  3. 将数据库查询得到的结果集再次同步到缓存中,并且返回对应的结果集。

这是大家比较熟悉的缓存使用方式,可以有效减轻数据库压力,提升接口访问性能。但是在这样的一个架构中,会有一个问题,就是一份数据同时保存在数据库和缓存中,如果数据发生变化,需要同时更新缓存和数据库,由于更新是有先后顺序的,并且它不像数据库中多表事务操作满足ACID特性,所以这样就会出现数据一致性的问题。

DB和缓存不一致方案与实战DEMO

关于缓存和DB不一致,其实无非就是以下四种解决方案:

  1. 先更新缓存,再更新数据库
  2. 先更新数据库,再更新缓存
  3. 先删除缓存,后更新数据库
  4. 先更新数据库,后删除缓存

先更新缓存,再更新数据库(不建议)

在这里插入图片描述

这种方案其实是不提倡的,这种方案存在的问题是缓存更新成功,但是更新数据库出现异常了。这样会导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。

先更新数据库,再更新缓存

先更新数据库,再更新缓存,如果缓存更新失败了,其实也会导致数据库和缓存中的数据不一致,这样客户端请求过来的可能一直就是错误的数据。

在这里插入图片描述

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

这种场景在并发量比较小的时候可能问题不大,理想情况是应用访问缓存的时候,发现缓存中的数据是空的,就会从数据库中加载并且保存到缓存中,这样数据是一致的,但是在高并发的极端情况下,由于删除缓存和更新数据库非原子行为,所以这期间就会有其他的线程对其访问。于是,如下图。

在这里插入图片描述

解释一下上图,老猫罗列了两个线程,分别是线程1和线程2。

  1. 线程1会先删除缓存中的数据,但是尚未去更新数据库。
  2. 此时线程2看到缓存中的数据是空的,就会去数据库中查询该值,并且重新更新到缓存中。
  3. 但是此时线程1并没有更新成功,或者是事务还未提交(MySQL的事务隔离级别,会导致未提交的事务数据不会被另一个线程看到),由于线程2快于线程1,所以线程2去数据库查询得到旧值。
  4. 这种情况下最终发现缓存中还是为旧值,但是数据库中却是最新的。

由此可见,这种方案其实也并不是完美的,在高并发的情况下还是会有问题。那么下面的这种总归是完美的了吧,有小伙伴肯定会这么认为,让我们一起来分析一下。

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

先说结论,其实这种方案也并不是完美的。咱们通过下图来说一个比较极端的场景。

在这里插入图片描述

上图中,我们执行的时间顺序是按照数字由小到大进行。在高并发场景下,我们说一下比较极端的场景。

上面有线程1和线程2两个线程。其中线程1是读线程,当然它也会负责将读取的结果集同步到缓存中,线程2是写线程,主要负责更新和重新同步缓存。

  1. 由于缓存失效,所以线程1开始直接查询的就是DB。
  2. 此时写线程2开始了,由于它的速度较快,所以直接完成了DB的更新和缓存的删除更新。
  3. 当线程2完成之后,线程1又重新更新了缓存,那此时缓存中被更新之后的当然是旧值了。

如此,咱们又发现了问题,又出现了数据库和缓存不一致的情况。

那么显然上面的这四种方案其实都多多少少会存在问题,那么究竟如何去保持数据库和缓存的一致性呢?

保证强一致性

如果有人问,那我们能否保证缓存和DB的强一致性呢?回答当然是肯定的,那就是针对更新数据库和刷新缓存这两个动作加上锁。当DB和缓存数据完成同步之后再去释放,一旦其中任何一个组件更新失败,我们直接逆向回滚操作。我们可能还得做快照便于其历史缓存重写。那这种设计显然代价会很大。

其实在很大一部分情况下,要求缓存和DB数据强一致大部分都是伪需求。我们可能只要达到最终尽量保持缓存一致即可。有缓存要求的大部分业务其实也是能接受数据在短期内不一致的情况。所以我们就可以使用下面的这两种最终一致性的方案。

错误重试达到最终一致

如下示意图所示:

在这里插入图片描述

上面的图中我们看到。当然上述老猫只是画了更新线程,其实读取线程也一样。

  1. 更新线程优先更新数据,然后再去更新缓存。
  2. 此时我们发现缓存更新失败了,咱们就将其重新放到消息队列中。
  3. 单独写一个消费者接收更新失败记录,然后进行重试更新操作。

说到消息队列重试,还有一种方式是基于异步任务重试,咱们可以把更新缓存失败的这个数据保存到数据库,然后通过另外的一个定时任务进而扫描待执行任务,然后去做相关的缓存更新动作。

当然上面我们提到的这两种方案,其实比较依赖我们的业务代码做出相对应的调整。我们当然也可以借助Canal组件来监控MySQL中的binlog的日志。通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。先更新DB,然后再去更新缓存,这种方式,被称为 Cache Aside Pattern,属于缓存更新的经典设计模式之一。

在这里插入图片描述

上述我们总结了缓存使用的一些方案,我们发现其实没有一种方案是完美的,最完美的方案其实还是得去结合具体的业务场景去使用。方案已经同步了,那么如何去撸数据库以及缓存同步的代码呢?接下来,和大家分享的当然是日常开发中比较好用的SpringCache缓存处理框架了。

SpringCache实战

SpringCache是一个框架,实现了基于注解缓存功能,只需要简单地加一个注解,就能实现缓存功能。
SpringCache提高了一层抽象,底层可以切换不同的cache实现,具体就是通过cacheManager接口来统一不同的缓存技术,cacheManager是spring提供的各种缓存技术抽象接口。

目前存在以下几种:

  • EhCacheCacheManager:将缓存的数据存储在内存中,以提高应用程序的性能。
  • GuavaCaceManager:使用Google的GuavaCache作为缓存技术。
  • RedisCacheManager:使用Redis作为缓存技术。

配置

我们日常开发中用到比较多的其实是redis作为缓存,所以咱们就可以用RedisCacheManager,做一下代码演示。咱们以springboot项目为例。

老猫这里拿看一下redisCacheManager来举例,项目开始的时候我们当忽然要在pom文件依赖的时候就肯定需要redis启用项。如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用注解完成缓存技术-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

因为我们在application.yml中就需要配置redis相关的配置项:

spring:redis:host: localhostport: 6379database: 0 jedis:pool:max-active: 8 # 最大链接数据max-wait: 1ms # 连接池最大阻塞等待时间max-idle: 4 # 连接线中最大的空闲链接min-idle: 0 # 连接池中最小空闲链接cache:redis:time-to-live: 1800000 

常用注解

关于SpringCache常用的注解,整理如下:

在这里插入图片描述

针对上述的注解,咱们做一下demo用法,如下:

用法简单盘点
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableCaching
public class Application {public static void main(String[] args) {SpringApplication.run(ReggieApplication.class);}
}

在service层我们注入所需要用到的cacheManager:

@Autowired
private CacheManager cacheManager;/*** 公众号:程序员老猫* 我们可以通过代码的方式主动清除缓存,例如**/
public void clearCache(String productCode) {try {RedisCacheManager redisCacheManager = (RedisCacheManager) cacheManager;Cache backProductCache = redisCacheManager.getCache("backProduct");if(backProductCache != null) {backProductCache.evict(productCode);}} catch (Exception e) {logger.error("redis 缓存清除失败", e);}
}

接下来我们看一下每一个注解的用法,以下关于缓存用法的注解,我们都可以将其加到dao层:

第一种@Cacheable

在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中。

@Cacheable 注解中的核心参数有以下几个:

  • value:缓存的名称,可以是一个字符串数组,表示该方法的结果可以被缓存到哪些缓存中。默认值为一个空数组,表示缓存到默认的缓存中。
  • key:缓存的 key,可以是一个 SpEL 表达式,表示缓存的 key 可以根据方法参数动态生成。默认值为一个空字符串,表示使用默认的 key 生成策略。
  • condition:缓存的条件,可以是一个 SpEL 表达式,表示缓存的结果是否应该被缓存。默认值为一个空字符串,表示不考虑任何条件,缓存所有结果。
  • unless:缓存的排除条件,可以是一个 SpEL 表达式,表示缓存的结果是否应该被排除在缓存之外。默认值为一个空字符串,表示不排除任何结果。

上述提及的SpEL是是Spring Framework中的一种表达式语言,此处不展开,不了解的小伙伴可以自己去查阅一下相关资料。

代码使用案例:

@Cacheable(value="picUrlPrefixDO",key="#id")
public PicUrlPrefixDO selectById(Long id) {PicUrlPrefixDO picUrlPrefixDO = writeSqlSessionTemplate.selectOne("PicUrlPrefixDao.selectById", id);return picUrlPrefixDO;
}

第二种@CachePut

表示将方法返回的值放入缓存中。
注解的参数列表和@Cacheable的参数列表一致,代表的意思也一样。
代码使用案例:

@CachePut(value = "userCache",key = "#users.id")
@GetMapping()
public User get(User user){User users= dishService.getById(user);return users;
}

第三种@CacheEvict

表示从缓存中删除数据。使用案例如下:

@CacheEvict(value="picUrlPrefixDO",key="#urfPrefix")
public Integer deleteByUrlPrefix(String urfPrefix) {return writeSqlSessionTemplate.delete("PicUrlPrefixDao.deleteByUrlPrefix", urfPrefix);
}

上述和大家分享了一下SpringCache的用法,对于上述提及的三个缓存注解中,老猫在日常开发过程中用的比较多的是@CacheEvict以及@Cacheable,如果对SpringCache实现原理感兴趣的小伙伴可以查阅一下相关的源码。

使用缓存的其他注意点

当我们使用缓存的时候,除了会遇到数据库和缓存不一致的情况之外,其实还有其他问题。严重的情况下可能还会出现缓存雪崩。关于缓存失效造成雪崩,大家可以看一下这里【糟糕!缓存击穿,商详页进不去了】。

另外如果加了缓存之后,应用程序启动或服务高峰期之前,大家一定要做好缓存预热从而避免上线后瞬时大流量造成系统不可用。关于缓存预热的解决方案,由于篇幅过长老猫在此不展开了。不过方案概要可以提供,具体如下:

  • 定时预热。采用定时任务将需要使用的数据预热到缓存中,以保证数据的热度。
  • 启动时加载预热。在应用程序启动时,将常用的数据提前加载到缓存中,例如实现InitializingBean 接口,并在 afterPropertiesSet 方法中执行缓存预热的逻辑。
  • 手动触发加载:在系统达到高峰期之前,手动触发加载常用数据到缓存中,以提高缓存命中率和系统性能。
  • 热点预热。将系统中的热点数据提前加载到缓存中,以减轻系统压力。5
  • 延迟异步预热。将需要预热的数据放入一个队列中,由后台异步任务来完成预热。
  • 增量预热。按需预热数据,而不是一次性预热所有数据。通过根据数据的访问模式和优先级逐步预热数据,以减少预热过程对系统的冲击。

如果小伙伴们还有其他的预热方式也欢迎大家留言。

总结

上述总结了关于缓存在日常使用的时候的一些方案以及坑点,当然这些也是面试官最喜欢提问的一些点。文中关于缓存的介绍老猫其实并没有说完,很多其实还是需要小伙伴们自己去抽时间研究研究。不得不说缓存是一门以空间换时间的艺术。要想使用好缓存,死记硬背策略肯定是行不通的。真实的业务场景往往要复杂的多,当然解决方案也不同,老猫上面提及的这些大家可以做一个参考,遇到实际问题还是需要大家具体问题具体分析。

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

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

相关文章

SOLIDWORKS2024 | 轻松处理制造复杂几何体和有机形状

如今&#xff0c;工程师面临各种各样的挑战。预算紧缩的同时&#xff0c;排期也越来越短。客户需要智能、互联的产品&#xff0c;这一需求掀起了各行各业添加软件和电子模块的趋势。产品须变得更快、更轻、更好。在所有这些变化中&#xff0c;几何体也变得越来越复杂。 从工作…

InstantID Zero-shot Identity-Preserving Generation in Seconds

InstantID: Zero-shot Identity-Preserving Generation in Seconds TL; DR&#xff1a;InstantID IP-Adapter (Face) ControlNet&#xff0c;实现了具有较高保真度的人脸 ID 生成。 方法 InstantID 想做到的事情是&#xff1a;给定一张参考人脸 ID 图片&#xff0c;生成该…

基于单片机的电子秤设计

摘 要 本文设计了一种以51单片机来进行控制的电子秤系统&#xff0c;系统的电路部分由以下几个电路模块组成&#xff1a;数据采集和数据处理电路、模数转换电路、LED及蜂鸣器报警电路、最小系统电路、液晶显示电路、矩阵按键电路等。接通电源后&#xff0c;单片机会把压力传感…

抠图透明背景怎么做?3种方法教你抠图换背景

抠图透明背景怎么做&#xff1f;抠图透明背景是一项在图像处理中常见的任务&#xff0c;它可以帮助我们去除图片中的多余部分&#xff0c;使主体部分与背景分离&#xff0c;从而得到一个透明背景的效果。这一技巧在多个领域都有广泛应用&#xff0c;掌握这种技巧&#xff0c;不…

C++第四弹---类与对象(一)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 类与对象 1、面向过程和面向对象初步认识 2、类的引入 3、类的定义 4、类的访问限定符及封装 4.1、访问限定符 4.2、封装 5、类的作用域 6、类的…

如何在WordPress网站上设置多语言展示

在今天的全球化世界中&#xff0c;拥有多语言网站对于吸引更广泛的受众至关重要。前不就我们遇到Hostease的客户咨询我们的在线客服&#xff0c;他想要对他的wordpress网站支持多语言。我们提供给客户可以尝试以下的插件来支持多语言。 在本教程中&#xff0c;我们将逐步介绍如…

【PCB 小工具】saturnpcb

saturnpcb 官网 打开长这样子&#xff1b; 上面是老版本&#xff1b;看下最新的 推荐还是官方下载 有人在说 这玩意需要在设置板材时候选择&#xff1b;

软件杯 深度学习 opencv python 实现中国交通标志识别

文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 &#x1f525; 优质…

什么是大顶堆?举个例子

一、什么是大顶堆&#xff1f; 大顶堆&#xff08;Max Heap&#xff09;是一种二叉堆的实现&#xff0c;它满足以下性质&#xff1a; 任意节点的值都大于等于其子节点的值。根节点&#xff08;堆顶&#xff09;的值是整个堆中最大的。 大顶堆通常用于实现优先队列等数据结构…

嘿!终于等到了!应用开发云资源套餐如约而至!

MemFire Cloud平台更新啦&#xff01;&#xff01;此次更新我们推出了万众期待的计费套餐&#xff0c;下面给大家带来详细的介绍~ 计费模式为“基础套餐按量付费”&#xff0c;您可选择购买带有一定配额的基础套餐&#xff0c;超出配额部分可以通过开启“超限按量”功能来转为…

AbilityStorage理解与反思

1.简介&#xff1a; AbilityStage是一个Module级别的组件容器&#xff0c;应用的HAP在首次加载时会创建一个AbilityStage实例&#xff0c;可以对该Module进行初始化等操作。 2.那么Module分为三类&#xff1a;Hap,Har,Hsp 官网上的表述容易误解&#xff1a;实际上AbilitySta…

表单修饰符和事件修饰符

表单修饰符和事件修饰符 表单修饰符 v-model.lazy v-model.lazy 失去焦点后再收集数据 <div id"app"><textarea name"" id"" cols"30" rows"10" v-model.lazy"a"></textarea>{{a}}<textar…

【深度学习】深度估计,Depth Anything Unleashing the Power of Large-Scale Unlabeled Data

论文标题&#xff1a;Depth Anything Unleashing the Power of Large-Scale Unlabeled Data 论文地址&#xff1a;https://arxiv.org/pdf/2401.10891.pdf 项目主页&#xff1a;https://depth-anything.github.io/ 演示地址&#xff1a;https://huggingface.co/spaces/LiheYoung…

PyCharm 中 Python 解释器的配置

温馨提示&#xff1a;本文 PyCharm 版本是 2022.3.3 前言 作为 Python 新手&#xff0c;在了解了基本语法之后&#xff0c;肯定得先用 IDE 工具写个 Hello World&#xff0c;来了解 Python 编程语法及 IDE 工具的常规配置和使用&#xff0c;这里我用的 IDE 工具是 PyCharm。 …

unicloud update 修改

update 修改 使用腾讯云时更新方法必须搭配doc、where方法使用&#xff0c;db.collection(‘test’).update()会报如下错误&#xff1a;param should have required property ‘query’ collection.doc().update(Object data)未使用set、remove更新操作符的情况下&#xff0c…

如何通过libusb直接向zebra打印机发送zpl,跨平台win/linux

环境&#xff1a;windows & linux & Zebra打印机gt820 windows: 之前安装了Zebra打印机官方驱动&#xff0c;所以先卸载掉驱动。再安装Zadig&#xff0c;用Zadig工具来安装WinUSB驱动。 zadig下载&#xff1a;Zadig - USB driver installation made easy 记住这两个数…

c语言指针基础下(下)

指针 字符指针变量 字符串变量的一般使用 int main() {char ch w;char* p &ch;*p h;printf("%c", ch);return 0; }上面就是通过指针的解引用改变了ch的值 int main() {char* p "ni hao a";//这个是常量字符串printf("%s\n", p);//打印…

超融合如何助力水务公司实现虚拟化与容器环境统一管理?

近些年&#xff0c;企业 IT 基础架构现代化转型的步伐逐渐加快&#xff0c;不少金融、医疗、政府等行业的用户&#xff0c;已在生产环境部署 Kubernetes 等云原生基础设施&#xff0c;为业务应用提供敏捷支持。不过&#xff0c;一些企业的容器化转型仍处于起步阶段&#xff0c;…

学习Android的第二十八天

目录 Android Service (服务) 线程 Service (服务) Service 相关方法 Android 非绑定 Service startService() 启动 Service 验证 startService() 启动 Service 的调用顺序 Android 绑定 Service bindService() 启动 Service 验证 BindService 启动 Service 的顺序 …

Instant --java学习笔记

Instant 时间线上的某个时刻 / 时间戳过获取lnstant的对象可以拿到此刻的时间&#xff0c;该时间由两部分组成:从1970-01-01 00:00:00 开始走到此刻的总秒数不够1秒的纳秒数 Instant的常见方法&#xff1a; Instant可以用来记录代码的执行时间&#xff0c;或用于记录用户操作某…