前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java项目冷更新数据双缓存方案 ( Redis + GuavaCache )

Java项目冷更新数据双缓存方案 ( Redis + GuavaCache )

作者头像
RRT冻羊
发布2022-11-03 13:54:03
9350
发布2022-11-03 13:54:03
举报
文章被收录于专栏:冻羊技术思考冻羊技术思考

Java项目冷更新数据双缓存方案

本文章主讲思想,不限于使用什么缓存 但为了写作方便,故中间件缓存采用redis,本地缓存采用guava cache

应用场景

1、接口对缓存的需求高,不允许没有缓存的情况。 2、本地缓存临时为redis分担压力,缓存热点数据到本地 3、缓存数据一般涉及大量运算,耗时较大,而且不会频繁的更新,多用于计算后进行展示

本人以下方案着重场景1: 本人的项目遇到的问题的是,某个数据展示的接口,用户会频繁访问,因此做了redis缓存。但是redis偶尔会出现连接拿不到等不可用的情况。在这种情况下,就会直接访问数据库再进行复杂的计算,导致接口延迟过高,用户体验极差。

我认为,redis的高可用是必不可少的。但是不可避免的是,存在redis不可用的情况。而这种时候,如果是高并发的应用,轻则接口延迟过高、重则直接压垮数据库。

双缓存方案前言

考虑到中间件缓存存在不可用的可能性,因此解决方案有: (1)结果存入数据库。数据库不可用的情况基本不可能,如果出现了,首先业务肯定做不下去,一般不会造成太大影响,顶多就是用户这段时间无法享受功能。因此可以把缓存的数据结果,存到数据库中。

弊端:中间件缓存不可用时,虽然避免了耗时较高的数据库操作和计算,但是之后所有请求走数据库。我们通常做缓存的目的,就是把压力从数据库中分担出来。

(2)采用本地缓存作为备用缓存。本地缓存只要内存充足,是保证可用的。

需要考虑的点

1、缓存数据的大小

(1)本地内存资源是否足以支撑这部分数据缓存 (2)本地内存资源昂贵,缓存数据是否值得占用内存

2、本地缓存的缓存时机

主要是考虑,什么时候启用本地缓存最合适。如:双缓存共存 还是 中间件缓存不可用时,再启用本地缓存 (1)考虑中间件缓存不可用的频率 (2)如果不走缓存,接口的耗时有多少 (3)中间件缓存失效那一刻,重新获取数据的耗时是否能接受 (4)双缓存共存,需要考虑本地缓存一直占用内存,但是又基本很少用上所带来的内存浪费问题

3、并发情况下,首次缓存数据的性能浪费问题

我们传统的方案一般是 (1)有缓存,直接读缓存 (2)无缓存,走数据库,然后写入缓存。

重点出在第2步中,如果在无缓存的情况下,并发量为N(N>1),假设获取数据代码耗时10s。那么最终这N个并发请求,每个请求的耗时都是10s。而对于后台来说,一共就是10*N的开销,也会有N次的更新缓存操作。而我们知道,实际上更新缓存只需要1次即可,其余N-1次都是没有意义的。并且其余的N-1次的数据计算也是没有意义的。在那一时刻,假设运算复杂,就会给CPU带来很大的开销。如果此时还有其他不相关的业务操作,就会受到影响。

因此此处着重要考虑的是,在并发情况下,是否只允许一个线程进行数据的计算和更新缓存操作。其余线程等待该线程的处理结果即可,而不需要每个线程都做重复的事情。

上述再次举例:(此处有锁机制的情况) 假设无缓存情况下,N个请求并发,数据计算代码耗时10s。 那么:

  • 只有1个线程拥有数据计算和更新缓存的权利,其余N-1个线程会被阻塞,直到缓存更新完毕。
  • 那么对于每个请求来说,还是10s。(N-1个线程有可能会因为等待,而大于10s)
  • 但是对于后台来说,就是单纯的10s,而不是10*N。因为只有1个线程进行了10s的计算,其余N-1个线程对于他们来说,是不参与复杂的计算的。他们直接读取结果即可。(而且这里还得结合锁的特性,比如synchronnized,当线程数大于2时就会升级为重量级锁,它在阻塞的过程中,是不占用CPU的)
  • 对于更新缓存来说,只有1次,而非N次
  • 假设,在这10s的期间。比如过了5s,又有一个新的请求进来。那么它的接口耗时会是5s。而如果走传统方案,它需要重新计算,那么它的接口耗时会是10s。因此在数据计算的这段时间,来多少个请求,就有多少个请求的性能开销是完全浪费的。

上述中仅仅讨论耗时问题,而实际上我们还要想到的是,线程上下文切换带来的性能开销,在这段时间内其他功能的体验效果会不会因为这个接口大打折扣。

4、心跳检测redis是否可用

当redis不可用时,本地缓存会代替redis工作,达到缓存可用的效果。但是我们肯定需要注意的是,中间件缓存何时恢复。 (1)我们做一个功能,手动触发检测redis是否可用,如果可用了,就从本地缓存切回中间件缓存 优点:开发简单 缺点:人为干预,系统不可自己恢复

(2)我们用一个线程,定期去检测redis是否可用。可用的话,就切回redis。

你可能会问,我们的代码难道不是 1、redis是否可用,可用的话直接读redis缓存。结束 2、redis不可用,走本地缓存。结束 我们直接try-catch,redis。redis不可用的话肯定会抛异常,抛异常了就走本地缓存就行了呗。

我一开始也是这么想的,但是等我实践后,我发现了一个问题。redis的参数中有一个timeout,它的作用是:redis超时。通常这里不会设置0,因为容易导致项目死掉。一般设置一个值,超过这个值redis就会报错。 1、可能redis不可用了 2、可能redis连接被拿完了,导致你拿不到连接,所以超时了 3、可能网络原因,超时了

OK,有了这个值的存在。就会导致你,在redis不可用时,走本地缓存之前,一定会经历这timeout秒。假设你的timeout设为2s。那么当redis失效时,你即使有本地缓存。 接口的延迟也会一直是2s。因此我们要把这2s给优化掉,就是设置一个标记,来标记redis是否可用,不可用的话直接走本地缓存。可用的话则走redis。这样 才能真正意义上的,走本地缓存。

因此,上述提到了标记。我们需要去维护这个标记,以达到系统能感知redis何时恢复可用,系统何时切换回redis缓存。

(1)此时,最简单的方案是,开启一个定时任务,去判断redis是否可用,去维护这个标记 但是在本篇文章中,一再强调的是,redis不可用的情况很少发生,本地缓存基本派不上用场。但是由于接口的特殊性 ,又不能脱离缓存独立存在。

上述方案的缺点:大部分情况下,redis是可用的。因此你的定时任务,在大部分情况下,都是没有意义的。

(2)因此我们更需要去考虑好性能,让心跳检测这个行为更加智能化,在有需要的时候启动,在不需要的时候不工作

方案思路

流程

1、从标记判断redis是否可用。可用的话走2,不可用走3 2、redis缓存是否存在, 存在的话直接读数据,结束; 不存在的话,从数据库获取数据进行计算,然后更新redis缓存,返回结果,结束;

3、设置标记,标记redis不可用。启动心跳检测任务,定期去判断redis是否可用,直到redis可用时,将标记恢复。 4、本地缓存是否存在, 存在的话直接读数据,结束; 不存在的话,从数据库获取数据进行计算,然后更新本地缓存,返回结果,结束;

可优化的地方有很多,结合上述"需要考虑的点"来优化步骤。

代码

暂不贴

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-01-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java项目冷更新数据双缓存方案
  • 应用场景
  • 双缓存方案前言
  • 需要考虑的点
    • 1、缓存数据的大小
      • 2、本地缓存的缓存时机
        • 3、并发情况下,首次缓存数据的性能浪费问题
          • 4、心跳检测redis是否可用
          • 方案思路
            • 流程
              • 代码
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档