缓存把我坑惨了..

这篇具有很好参考价值的文章主要介绍了缓存把我坑惨了..。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

故事

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

一声不和谐的座机电话声打破这份本该属于小猫的宁静,“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: localhost
    port: 6379
    database: 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
  • 延迟异步预热。将需要预热的数据放入一个队列中,由后台异步任务来完成预热。
  • 增量预热。按需预热数据,而不是一次性预热所有数据。通过根据数据的访问模式和优先级逐步预热数据,以减少预热过程对系统的冲击。

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

总结

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

到了这里,关于缓存把我坑惨了..的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 离职时HR把我的职位写高了

    离职时HR把我的职位写高了 把项目助理开成项目经理,再就业时的简历漂亮多了,你觉得HR这么做合适吗? 这种情况的出现可能是出于以下原因: 提高员工的自尊心和满意度 HR可能认为,将员工的职位写得更高,可以让他们感觉更被认可和重要,从而提高他们的自尊心和满意

    2024年02月07日
    浏览(35)
  • 室外定位:高精度北斗RTK定位技术

    北斗RTK定位技术,也称北斗差分定位技术,利用我国自主研发的北斗卫星定位系统实现精确定位功能。定位精度可根据需要,通过选择不同精度的人员定位终端来实现。   在科技强国的战略驱动下,北斗RTK定位技术迎来了广阔的发展机遇,不断向高精度位置服务领域发展,赋

    2023年04月08日
    浏览(45)
  • 重构这段烂代码,差点把我整凌乱了...

    🍀注重实效,不要靠巧合编程。 🍀在构造一个对象的过程中,应避免依赖对象已经设置的field来继续给对象的其他field赋值,而应该基于原始对象的field去判断。   先看这段代码,烂不烂,你可以品一下,多半味道不怎么好。   在不改变业务逻辑的基础上,我重构这段代码

    2024年02月05日
    浏览(83)
  • git把我本地文件传到我的指定的仓库

    在使用Git将本地文件推送到指定仓库之前,请确保已经安装了Git并进行了基本配置。接下来,遵循以下步骤将本地文件推送到远程仓库:   兄弟先赏析悦目一下,摸个鱼   首先,在本地文件夹中打开命令行界面(在Windows上是命令提示符或PowerShell,而在Mac和Linux上是终端)。

    2024年02月06日
    浏览(44)
  • ChatGPT是智能硬件的春天

            智能音箱,一度被亚马逊带领引爆。         国内京东,阿里,百度,小米,腾讯等厂家参下,蓬勃发展。         然而,在2021到2022年,智能音箱就可开始下滑,叮咚音箱退出历史舞台。         转机出现在2022年底,2023年初,openai旗下的这款chatGPT产

    2024年02月02日
    浏览(40)
  • 年薪40万程序员辞职炒股,把一年工资亏光了,得了抑郁症,太惨了

    年薪40万的程序员辞职全职炒股 把一年的工资亏光了 得了抑郁症 刚才在网上看了一篇文章 是一位北京的一位在互联网 大厂上班的程序员 在去年就是股市行情比较好的时候 他买了30多万股票 结果连续三个月都赚钱 然后呢 他是就把每天就996这种工作就辞掉了 然后在家全是炒

    2024年02月02日
    浏览(43)
  • Spring来了,春天还会远吗?

    结束了JVM的学习后,要进入的是JavaEE进阶的学习了。JavaEE进阶学习内容很多很丰富,并且也很有难度。今天我们就从Spring开始讲起。 目录 框架的好处 怎么学框架 Spring核心与设计思想 容器 IoC Spring IoC DI(Dependency Injection) Spring的创建和使用 创建Maven项目 创建Bean 1.配置文件

    2024年01月16日
    浏览(29)
  • 基于stm32的室外环境监测系统的设计和实现

    目 录 摘 要 Ⅰ Abstract Ⅱ 第1章 绪论 9 1.1 课题研究背景与意义 9 1.2 国内外研究现状 9 1.3 课题研究的主要内容 10 1.4 本文组织结构 10 第2章 系统关键技术介绍 12 2.1 无线传感器网络技术 12 2.2 WiFi通讯技术 13 2.3 单片机技术 13 2.4 物联网云平台 13 第3章 系统需求分析 15 3.1 系统需求

    2023年04月14日
    浏览(42)
  • springboot第17集:Spring我的春天

    Spring是一个开源免费的框架和容器,具有轻量级和非侵入式的特点。它支持控制反转(IoC)和面向切面(AOP),同时提供了对事务和其他框架的支持。因此,简单来说,Spring就是一个轻量级的IoC和AOP容器框架。 假设有一个应用程序需要使用数据库连接池进行数据存储操作,使用S

    2024年02月03日
    浏览(34)
  • 【网安】学了这份网络安全资源,可千万别把我给供出来~

    大家周末好。接下来我会写系列的文章,给大家整理下网络安全的详细的学习步骤和学习资源推荐。 今天的主题是 —— Web 安全 。 Web 安全是网络渗透中很重要的一个组成部分,今天跟大家聊一下,如何在三个月内从零基础掌握 Web 安全。 第一周 :HTML+CSS,学会网页基本格式

    2024年02月12日
    浏览(78)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包