广告
返回顶部
首页 > 资讯 > 精选 >基于Spring Cache如何实现Caffeine+Redis二级缓存
  • 924
分享到

基于Spring Cache如何实现Caffeine+Redis二级缓存

2023-06-29 15:06:22 924人浏览 八月长安
摘要

这篇文章主要为大家展示了“基于spring Cache如何实现Caffeine+Redis二级缓存”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“基于Spring Cache如

这篇文章主要为大家展示了“基于spring Cache如何实现Caffeine+Redis二级缓存”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“基于Spring Cache如何实现Caffeine+Redis二级缓存”这篇文章吧。

具体如下:

基于Spring Cache如何实现Caffeine+Redis二级缓存

一、聊聊什么是硬编码使用缓存?

在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存。

我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下:

    @Autowire    private UserMapper userMapper;    @Autowire    private RedisCache redisCache;    //查询用户    public User getUserById(Long userId) {        //定义缓存key        String cacheKey = "userId_" + userId;        //先查询redis缓存        User user = redisCache.get(cacheKey);        //如果缓存中有就直接返回,不再查询数据库        if (user != null) {            return user;        }        //没有再查询数据库        user = userMapper.getUserById(userId);        //数据存入缓存,这样下次查询就能到缓存中获取        if (user != null) {            strinGCommand.set(cacheKey, user);        }        return user;    }

相信很多同学都写过类似风格的代码,这种风格符合面向过程的编程思维,非常容易理解。但它也有一些缺点:

代码不够优雅。业务逻辑有四个典型动作:存储,读取,修改,删除。每次操作都需要定义缓存Key ,调用缓存命令的api,产生较多的重复代码;

缓存操作和业务逻辑之间的代码耦合度高,对业务逻辑有较强的侵入性。侵入性主要体现如下两点:

  • 开发联调阶段,需要去掉缓存,只能注释或者临时删除缓存操作代码,也容易出错

  • 某些场景下,需要更换缓存组件,每个缓存组件有自己的API,更换成本颇高

如果说是下面这样的,是不是就优雅多了。

@Mapperpublic interface UserMapper  {            @Cacheable(key = "'cache_user_id_' + #userId")    User getUserById(Long userId);}

再看实现类

    @Autowire    private UserMapper userMapper;    //查询用户    public User getUserById(Long userId) {        return userMapper.getUserById(userId);    }

这么一看是不是完全和缓存分离开来,如果开发联调阶段,需要去掉缓存那么直接注释掉注解就好了,是不是非常完美。

而且这一整套实现都不要自己手动写,Spring Cache就已经帮我定义好相关注解和接口,我们可以轻易实现上面的功能。

二、Spring Cache简介

Spring Cache是Spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在

方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。

Spring Cache核心的接口就两个:CacheCacheManager

1、Cache接口

该接口定义提供缓存的具体操作,比如缓存的放入、读取、清理:

package org.Springframework.cache;import java.util.concurrent.Callable;public interface Cache {// cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheNameString getName();//得到底层使用的缓存,如EhcacheObject getNativeCache();// 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值ValueWrapper get(Object key);// 通过key获取缓存值,返回的是实际值,即方法的返回值类型<T> T get(Object key, Class<T> type);// 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。// 因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库<T> T get(Object key, Callable<T> valueLoader);// 将@Cacheable注解方法返回的数据放入缓存中void put(Object key, Object value);// 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据ValueWrapper putIfAbsent(Object key, Object value);// 删除缓存void evict(Object key);// 清空缓存void clear();// 缓存返回值的包装interface ValueWrapper {// 返回实际缓存的对象Object get();}}

2、CacheManager接口

主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。

package org.Springframework.cache;import java.util.Collection;public interface CacheManager {// 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存        //对象(如Caffeine)重新创建后原来缓存内容丢失的情况Cache getCache(String name);// 返回所有的cacheNameCollection<String> getCacheNames();}

3、常用注解说明

@Cacheable:主要应用到查询数据的方法上。

public @interface Cacheable {    // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean@AliasFor("cacheNames")String[] value() default {};@AliasFor("value")String[] cacheNames() default {};    // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)String key() default "";// 缓存key生成器,默认实现是SimpleKeyGeneratorString keyGenerator() default "";// 指定使用哪个CacheManager,如果只有一个可以不用指定String cacheManager() default "";// 缓存解析器String cacheResolver() default "";// 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断String condition() default "";            // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断String unless() default "";// 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法boolean sync() default false;}

@CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性

public @interface CacheEvict {  // ...相同属性说明请参考@Cacheable中的说明// 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法boolean allEntries() default false;// 调用方法之前或之后清除缓存boolean beforeInvocation() default false;}

@CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable

@Caching:用于在一个方法上配置多种注解

@EnableCaching:启用Spring cache缓存,作为总的开关,在SpringBoot的启动类或配置类上需要加上此注解才会生效

三、使用二级缓存需要思考的一些问题?

我们知道关系数据库(Mysql)数据最终存储在磁盘上,如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了

像redis这种的内存缓存。

通过内存缓存确实能够很大程度的提高查询速度,但如果同一查询并发量非常的大,频繁的查询redis,也会有明显的网络IO上的消耗,

那我们针对这种查询非常频繁的数据(热点key),我们是不是可以考虑存到应用内缓存,如:caffeine。

当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。

应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存

整个流程如下

基于Spring Cache如何实现Caffeine+Redis二级缓存

流程看着是很清新,但其实二级缓存需要考虑的点还很多。

如何保证分布式节点一级缓存的一致性?

我们说一级缓存是应用内缓存,那么当你的项目部署在多个节点的时候,如何保证当你对某个key进行修改删除操作时,使其它节点

的一级缓存一致呢?

是否允许存储空值?

这个确实是需要考虑的点。因为如果某个查询缓存和数据库中都没有,那么就会导致频繁查询数据库,导致数据库Down,这也是我们

常说的缓存穿透。

但如果存储空值呢,因为可能会存储大量的空值,导致缓存变大,所以这个最好是可配置,按照业务来决定是否开启。

是否需要缓存预热?

也就是说,我们会觉得某些key一开始就会非常的热,也就是热点数据,那么我们是否可以一开始就先存储到缓存中,避免缓存击穿。

一级缓存存储数量上限的考虑?

既然一级缓存是应用内缓存,那你是否考虑一级缓存存储的数据给个限定最大值,避免存储太多的一级缓存导致OOM。

一级缓存过期策略的考虑?

我们说redis作为二级缓存,redis是淘汰策略来管理的。具体可参考redis的8种淘汰策略。那你的一级缓存策略呢?就好比你设置一级缓存

数量最大为5000个,那当第5001个进来的时候,你是怎么处理呢?是直接不保存,还是说自定义LRU或者LFU算法去淘汰之前的数据?

一级缓存过期了如何清除?

我们说redis作为二级缓存,我们有它的缓存过期策略(定时、定期、惰性),那你的一级缓存呢,过期如何清除呢?

这里4、5、6小点如果说用我们传统的Map显然实现是很费劲的,但现在有更好用的一级缓存库那就是Caffeine

四、Caffeine 简介

Caffeine,一个用于Java的高性能缓存库。

缓存和Map之间的一个根本区别是缓存会清理存储的项目

1、写入缓存策略

Caffeine有三种缓存写入策略:手动同步加载异步加载

2、缓存值的清理策略

Caffeine有三种缓存值的清理策略:基于大小基于时间基于引用

基于容量:当缓存大小超过配置的大小限制时会发生回收。

基于时间

  • 写入后到期策略。

  • 访问后过期策略。

  • 到期时间由 Expiry 实现独自计算。

基于引用:启用基于缓存键值的垃圾回收。

  • Java种有四种引用:强引用,软引用,弱引用和虚引用,caffeine可以将值封装成弱引用或软引用。

  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会
    回收它的内存。

3、统计

Caffeine提供了一种记录缓存使用统计信息的方法,可以实时监控缓存当前的状态,以评估缓存的健康程度以及缓存命中率等,方便后

续调整参数。

4、高效的缓存淘汰算法

缓存淘汰算法的作用是在有限的资源内,尽可能识别出哪些数据在短时间会被重复利用,从而提高缓存的命中率。常用的缓存淘汰算法有

LRU、LFU、FIFO等。

FIFO:先进先出。选择最先进入的数据优先淘汰。LRU:最近最少使用。选择最近最少使用的数据优先淘汰。LFU:最不经常使用。选择在一段时间内被使用次数最少的数据优先淘汰。

LRU(Least Recently Used)算法认为最近访问过的数据将来被访问的几率也更高。

LRU通常使用链表来实现,如果数据添加或者被访问到则把数据移动到链表的头部,链表的头部为热数据,链表的尾部如冷数据,当

数据满时,淘汰尾部的数据。

LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问

的频率也更高”。根据LFU的思想,如果想要实现这个算法,需要额外的一套存储用来存每个元素的访问次数,会造成内存资源的浪费。

Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,其特点:高命中率、低内存占用。

5、其他说明

Caffeine的底层数据存储采用ConcurrentHashMap。因为Caffeine面向jdk8,在jdk8中ConcurrentHashMap增加了红黑树,在hash冲突

严重时也能有良好的读性能。

五、基于Spring Cache实现二级缓存(Caffeine+Redis)

前面说了,使用了redis缓存,也会存在一定程度的网络传输上的消耗,所以会考虑应用内缓存,但有点很重要的要记住:

应用内缓存可以理解成比redis缓存更珍惜的资源,所以,caffeine 不适用于数据量大,并且缓存命中率极低的业务场景,如用户维度的缓存。

当前项目针对应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。

可以有多种方式来实现这种效果,比如:ZooKeeperMQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就

不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作。

当某个key进行更新删除操作时,通过发布订阅的方式通知其它节点进行删除该key本地的一级缓存就可以了。

具体具体项目代码这里就不再粘贴出来了,这样只粘贴如何引用这个starter包。

1、Maven引入使用

   <dependency>            <groupId>com.jincou</groupId>            <artifactId>redis-caffeine-cache-starter</artifactId>            <version>1.0.0</version>   </dependency>

2、application.yml

添加二级缓存相关配置

# 二级缓存配置# 注:caffeine 不适用于数据量大,并且缓存命中率极低的业务场景,如用户维度的缓存。请慎重选择。l2cache:  config:    # 是否存储空值,默认true,防止缓存穿透    allowNullValues: true    # 组合缓存配置    composite:      # 是否全部启用一级缓存,默认false      l1AllOpen: false      # 是否手动启用一级缓存,默认false      l1Manual: true      # 手动配置走一级缓存的缓存key集合,针对单个key维度      l1ManualKeySet:      - userCache:user01      - userCache:user02      - userCache:user03      # 手动配置走一级缓存的缓存名字集合,针对cacheName维度      l1ManualCacheNameSet:      - userCache      - GoodsCache    # 一级缓存    caffeine:      # 是否自动刷新过期缓存 true 是 false 否      autoRefreshExpireCache: false      # 缓存刷新调度线程池的大小      refreshPoolSize: 2      # 缓存刷新的频率(秒)      refreshPeriod: 10      # 写入后过期时间(秒)      expireAfterWrite: 180      # 访问后过期时间(秒)      expireAfterAccess: 180      # 初始化大小      initialCapacity: 1000      # 最大缓存对象个数,超过此数量时之前放入的缓存将失效      maximumSize: 3000    # 二级缓存    redis:      # 全局过期时间,单位毫秒,默认不过期      defaultExpiration: 300000      # 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高      expires: {userCache: 300000,goodsCache: 50000}      # 缓存更新时通知其他节点的topic名称 默认 cache:redis:caffeine:topic      topic: cache:redis:caffeine:topic

3、启动类上增加@EnableCaching

@EnableCaching@SpringBootApplicationpublic class CacheApplication {public static void main(String[] args) {SpringApplication.run(CacheApplication.class, args);}}

4、在需要缓存的方法上增加@Cacheable注解

@Servicepublic class CaffeineCacheService {    private final Logger logger = LoggerFactory.getLogger(CaffeineCacheService.class);        private static Map<String, UserDTO> userMap = new HashMap<>();    {        userMap.put("user01", new UserDTO("1", "张三"));        userMap.put("user02", new UserDTO("2", "李四"));        userMap.put("user03", new UserDTO("3", "王五"));        userMap.put("user04", new UserDTO("4", "赵六"));    }        @Cacheable(key = "'cache_user_id_' + #userId", value = "userCache")    public UserDTO queryUser(String userId) {        UserDTO userDTO = userMap.get(userId);        try {            Thread.sleep(1000);// 模拟加载数据的耗时        } catch (InterruptedException e) {            e.printStackTrace();        }        logger.info("加载数据:{}", userDTO);        return userDTO;    }        @Cacheable(value = "userCache", key = "#userId", sync = true)    public List<UserDTO> queryUserSyncList(String userId) {        UserDTO userDTO = userMap.get(userId);        List<UserDTO> list = new ArrayList();        list.add(userDTO);        logger.info("加载数据:{}", list);        return list;    }        @CachePut(value = "userCache", key = "#userId")    public UserDTO putUser(String userId, UserDTO userDTO) {        return userDTO;    }        @CacheEvict(value = "userCache", key = "#userId")    public String evictUserSync(String userId) {        return userId;    }}

以上是“基于Spring Cache如何实现Caffeine+Redis二级缓存”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注编程网精选频道!

--结束END--

本文标题: 基于Spring Cache如何实现Caffeine+Redis二级缓存

本文链接: https://www.lsjlt.com/news/325455.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 基于Spring Cache实现Caffeine+Redis二级缓存
    目录一、聊聊什么是硬编码使用缓存?二、Spring Cache简介1、Cache接口2、CacheManager接口3、常用注解说明三、使用二级缓存需要思考的一些问题?四、Caffe...
    99+
    2022-11-13
  • 基于Spring Cache如何实现Caffeine+Redis二级缓存
    这篇文章主要为大家展示了“基于Spring Cache如何实现Caffeine+Redis二级缓存”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“基于Spring Cache如...
    99+
    2023-06-29
  • 基于Spring接口集成Caffeine+Redis两级缓存
    目录前言改造JSR107 规范CacheCacheManager配置&使用分布式环境改造定义消息体Redis消息配置消息消费逻辑修改DoubleCache测试总结前言 在上一...
    99+
    2022-11-13
  • Redis+Caffeine实现分布式二级缓存组件实战教程
    目录前言所谓二级缓存分布式二级缓存的优势如何使用组件?核心实现方法关于分布式本地缓存失效前言 在生产中已有实践,本组件仅做个人学习交流分享使用。github:https://github.com/axinSoochow/...
    99+
    2022-08-08
    Redis Caffeine分布式二级缓存 Redis Caffeine二级缓存
  • 使用Spring如何实现注解Cache与Redis缓存
    使用Spring如何实现注解Cache与Redis缓存?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1. redis 和 ehcache的区别:简单了解了下,个人觉得 从部署上...
    99+
    2023-05-31
    spring redis 缓存
  • 基于Java实现Redis多级缓存方案
    目录一、多级缓存1. 传统缓存方案2. 多级缓存方案二、JVM本地缓存1. 实用案例三、缓存一致性1. 常见方案1.1 设置有效期1.2 同步双写1.3 异步通知2. 基于Canal...
    99+
    2022-11-13
  • 基于Java怎么实现Redis多级缓存
    这篇文章主要介绍“基于Java怎么实现Redis多级缓存”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“基于Java怎么实现Redis多级缓存”文章能帮助大家解决问题。一、多级缓存1. 传统缓存方案请...
    99+
    2023-06-29
  • 基于Ehcache如何实现Spring缓存
    这篇文章主要介绍了基于Ehcache如何实现Spring缓存,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一 简介缓存,通过将数据保存在缓冲...
    99+
    2022-10-19
  • 在springboot使用spring 如何实现缓存cache
    本篇文章为大家展示了在springboot使用spring 如何实现缓存cache,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。声明式缓存Spring 定义 CacheManager 和 Cache...
    99+
    2023-05-31
    springboot spring cache
  • redis如何实现多级缓存
                                 ...
    99+
    2022-10-18
  • 使用MyBatis如何实现一级缓存与二级缓存
    这期内容当中小编将会给大家带来有关使用MyBatis如何实现一级缓存与二级缓存,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。MyBatis缓存我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB...
    99+
    2023-05-31
    mybatis 一级缓存 二级缓存
  • Go应该如何实现二级缓存
    目录一、需求二、实现连接Mysql并执行查询语句三、写一个错误处理函数四、设置二级缓存一、需求 实现二级缓存 程序运行起来后提示:“请输入命令:”,如果输入geta...
    99+
    2022-11-12
  • spring redis注解如何实现缓存机制
    这篇文章给大家分享的是有关spring redis注解如何实现缓存机制的内容。小编觉得挺实用的,因此分享给大家做个参考。一起跟随小编过来看看吧。1、xml配置 <bean id="poolC...
    99+
    2022-10-18
  • Redis如何实现在Spring Boot中做缓存
    Redis如何实现在Spring Boot中做缓存?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。1、创建UserServicepublic interface UserSe...
    99+
    2023-05-31
    springboot redis edi
  • 基于 Spring Aop 环绕通知实现 Redis 缓存双删功能(示例代码)
    基于 spring aop 常规应用场景多是用于日志记录以及实现 Redis 分布式锁,在 github 中也有项目是把它拿来当作缓存的异常捕捉。从而避免影响实际业务的开发;在某天,笔者有个业务开发是给某个服务模块增加 ...
    99+
    2022-08-16
    Spring Aop Redis 缓存双删 Spring Aop Redis 缓存
  • Spring 如何结合Redis数据库实现缓存
    这篇文章将为大家详细讲解有关 Spring 如何结合Redis数据库实现缓存,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. 依赖包安装pom.xml 加入:<!-- redis c...
    99+
    2023-05-31
    spring redis 缓存
  • 使用Mybatis如何实现配置二级缓存
    这篇文章给大家介绍使用Mybatis如何实现配置二级缓存,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Mybatis的二级缓存配置相当容易,要开启二级缓存,只需要在你的Mapper 映射文件中添加一行:<...
    99+
    2023-05-31
    mybatis 二级缓存
  • hibernate在java中使用如何实现二级缓存
    本篇文章为大家展示了hibernate在java中使用如何实现二级缓存,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Hibernate的二级缓存一、缓存概述缓存(Cache): 计算机领域非常通用的...
    99+
    2023-05-31
    hibernate 二级缓存 java
  • 在Spring Boot中使用注解如何实现Redis 缓存
    在Spring Boot中使用注解如何实现Redis 缓存?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、创建 Caching 配置类RedisKeys.Javapackag...
    99+
    2023-05-31
    springboot redis 注解
  • Python和Spring框架如何实现二维码缓存的优化?
    二维码是一种广泛应用于现代社会中的技术,其可以用于快速扫描获取信息。然而,由于二维码的生成需要耗费大量的计算资源,因此在高并发场景下,二维码的生成可能会成为系统的瓶颈。为了解决这一问题,我们可以使用缓存技术对二维码进行优化。本文将介绍如何...
    99+
    2023-10-07
    spring 缓存 二维码
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作