Redis+Caffeine 太强了!二级缓存可以这样实现!

在实际的项目中,我们通常会将一些热点数据存储到RedisMemCache这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。

在一些场景下可能还需要进一步配合本地缓存使用,例如Guava cacheCaffeine,从而再次提升程序的响应速度与服务性能。

于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。

二级缓存的访问流程可以用下面这张图来表示:

图片

优点与问题

图片

准备工作

图片

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.9.2</version>
</dependency>
<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>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.8.1</version>
</dependency>

application.yml中配置Redis的连接信息:

spring:redis:host: 127.0.0.1port: 6379database: 0timeout: 10000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0

我们使用RedisTemplate来对redis进行读写操作。

下面在单机环境下,将按照对业务侵入性的不同程度,分三个版本来实现两级缓存的使用。

V1.0版本

在使用Cache前,需要先配置一下相关参数:

@Configuration
public class CaffeineConfig {@Beanpublic Cache<String,Object> caffeineCache(){return Caffeine.newBuilder().initialCapacity(128)//初始大小.maximumSize(1024)//最大数量.expireAfterWrite(60, TimeUnit.SECONDS)//过期时间.build();}
}

图片

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {private final OrderMapper orderMapper;@Overridepublic Order getOrderById(Long id) {  Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));    return order;}@Overridepublic void updateOrder(Order order) {      orderMapper.updateById(order);}@Overridepublic void deleteOrder(Long id) {orderMapper.deleteById(id);}
}

接下来,对上面的OrderService进行改造,在执行正常业务外再加上操作两级缓存的代码,先看改造后的查询操作:

public Order getOrderById(Long id) {String key = CacheConstant.ORDER + id;Order order = (Order) cache.get(key,k -> {//先查询 RedisObject obj = redisTemplate.opsForValue().get(k);if (Objects.nonNull(obj)) {log.info("get data from redis");return obj;}// Redis没有则查询 DBlog.info("get data from database");Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));redisTemplate.opsForValue().set(k, myOrder, 120, TimeUnit.SECONDS);return myOrder;});return order;
}

图片

图片

图片

图片

public void updateOrder(Order order) {log.info("update order data");String key=CacheConstant.ORDER + order.getId();orderMapper.updateById(order);//修改 RedisredisTemplate.opsForValue().set(key,order,120, TimeUnit.SECONDS);// 修改本地缓存cache.put(key,order);
}

看一下下面图中接口的调用、以及缓存的刷新过程。可以看到在更新数据后,同步刷新了缓存中的内容,再之后的访问接口时不查询数据库,也可以拿到正确的结果:

图片

最后再来看一下删除操作,在删除数据的同时,手动移除ReidsCaffeine中的缓存:

public void deleteOrder(Long id) {log.info("delete order");orderMapper.deleteById(id);String key= CacheConstant.ORDER + id;redisTemplate.delete(key);cache.invalidate(key);
}

我们在删除某个缓存后,再次调用之前的查询接口时,又会出现重新查询数据库的情况:

图片

图片

V2.0版本

图片

@Configuration
public class CacheManagerConfig {@Beanpublic CacheManager cacheManager(){CaffeineCacheManager cacheManager=new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(60, TimeUnit.SECONDS));return cacheManager;}
}

图片

@Cacheable(value = "order",key = "#id")
//@Cacheable(cacheNames = "order",key = "#p0")
public Order getOrderById(Long id) {String key= CacheConstant.ORDER + id;//先查询 RedisObject obj = redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)){log.info("get data from redis");return (Order) obj;}// Redis没有则查询 DBlog.info("get data from database");Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);return myOrder;
}

图片

#参数名
#参数对象.属性名
#p参数对应下标

图片

@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {log.info("update order data");orderMapper.updateById(order);//修改 RedisredisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),order, 120, TimeUnit.SECONDS);return order;
}

图片

@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {log.info("delete order");orderMapper.deleteById(id);redisTemplate.delete(CacheConstant.ORDER + id);
}

图片

V3.0版本

模仿spring通过注解管理缓存的方式,我们也可以选择自定义注解,然后在切面中处理缓存,从而将对业务代码的入侵降到最低。

首先定义一个注解,用于添加在需要操作缓存的方法上:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {String cacheName();String key(); //支持springEl表达式long l2TimeOut() default 120;CacheType type() default CacheType.FULL;
}

我们使用cacheName + key作为缓存的真正key(仅存在一个Cache中,不做CacheName隔离),l2TimeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型,枚举类型定义如下:

public enum CacheType {FULL,   //存取PUT,    //只存DELETE  //删除
}

因为要使key支持springEl表达式,所以需要写一个方法,使用表达式解析器解析参数:

public static String parse(String elString, TreeMap<String,Object> map){elString=String.format("#{%s}",elString);//创建表达式解析器ExpressionParser parser = new SpelExpressionParser();//通过evaluationContext.setVariable可以在上下文中设定变量。EvaluationContext context = new StandardEvaluationContext();map.entrySet().forEach(entry->context.setVariable(entry.getKey(),entry.getValue()));//解析表达式Expression expression = parser.parseExpression(elString, new TemplateParserContext());//使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文String value = expression.getValue(context, String.class);return value;
}

参数中的elString对应的就是注解中key的值,map是将原方法的参数封装后的结果。简单进行一下测试:

public void test() {String elString="#order.money";String elString2="#user";String elString3="#p0";   TreeMap<String,Object> map=new TreeMap<>();Order order = new Order();order.setId(111L);order.setMoney(123D);map.put("order",order);map.put("user","Hydra");String val = parse(elString, map);String val2 = parse(elString2, map);String val3 = parse(elString3, map);System.out.println(val);System.out.println(val2);System.out.println(val3);
}

执行结果如下,可以看到支持按照参数名称、参数对象的属性名称读取,但是不支持按照参数下标读取,暂时留个小坑以后再处理。

123.0
Hydra
null

至于Cache相关参数的配置,我们沿用V1版本中的配置即可。准备工作做完了,下面我们定义切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。

@Slf4j @Component @Aspect 
@AllArgsConstructor
public class CacheAspect {private final Cache cache;private final RedisTemplate redisTemplate;@Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")public void cacheAspect() {}@Around("cacheAspect()")public Object doAround(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();//拼接解析springEl表达式的mapString[] paramNames = signature.getParameterNames();Object[] args = point.getArgs();TreeMap<String, Object> treeMap = new TreeMap<>();for (int i = 0; i < paramNames.length; i++) {treeMap.put(paramNames[i],args[i]);}DoubleCache annotation = method.getAnnotation(DoubleCache.class);String elResult = ElParser.parse(annotation.key(), treeMap);String realKey = annotation.cacheName() + CacheConstant.COLON + elResult;//强制更新if (annotation.type()== CacheType.PUT){Object object = point.proceed();redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);cache.put(realKey, object);return object;}//删除else if (annotation.type()== CacheType.DELETE){redisTemplate.delete(realKey);cache.invalidate(realKey);return point.proceed();}//读写,查询CaffeineObject caffeineCache = cache.getIfPresent(realKey);if (Objects.nonNull(caffeineCache)) {log.info("get data from caffeine");return caffeineCache;}//查询RedisObject redisCache = redisTemplate.opsForValue().get(realKey);if (Objects.nonNull(redisCache)) {log.info("get data from redis");cache.put(realKey, redisCache);return redisCache;}log.info("get data from database");Object object = point.proceed();if (Objects.nonNull(object)){//写入RedisredisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);//写入Caffeinecache.put(realKey, object);        }return object;}
}

图片

@DoubleCache(cacheName = "order", key = "#id",type = CacheType.FULL)
public Order getOrderById(Long id) {Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));return myOrder;
}@DoubleCache(cacheName = "order",key = "#order.id",type = CacheType.PUT)
public Order updateOrder(Order order) {orderMapper.updateById(order);return order;
}@DoubleCache(cacheName = "order",key = "#id",type = CacheType.DELETE)
public void deleteOrder(Long id) {orderMapper.deleteById(id);
}

到这里,基于切面操作缓存的改造就完成了,Service的代码也瞬间清爽了很多,让我们可以继续专注于业务逻辑处理,而不用费心去操作两级缓存了。

总结

本文按照对业务入侵的递减程度,依次介绍了三种管理两级缓存的方法。

本文中只是介绍了最基础的使用,实际中的并发问题、事务的回滚问题都需要考虑,还需要思考什么数据适合放在一级缓存、什么数据适合放在二级缓存等等的其他问题。

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

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

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

相关文章

Keil5 配置jlink及jlink下载程序,程序没有运行

1.先选好对应的芯片设备 之后步骤参考这个&#xff1a;MDK5 JLINK配置流程_keil5配置jlink仿真器步骤-CSDN博客 2.jlink下载程序之后板子没有运行可以查看reset and run是否有没有勾选

享元模式(Flyweight Pattern)

定义 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;其主要目的是通过共享尽可能多的数据来减少内存使用和提高性能。它通常在需要创建大量相似对象的情况下使用&#xff0c;以减少对象的数量和内存消耗。 示例 #include <iostream…

扫雷2(2537)

题目描述 扫雷游戏是一款十分经典的单机小游戏。它的精髓在于&#xff0c;通过已翻开格子所提示的周围格地雷数&#xff0c;来判断未翻开格子里是否是地雷。现在给出 m 行 n 列的雷区中的地雷分布&#xff0c;要求计算出每个非地雷格的周围格地雷数。 注&#xff1a;每个格子周…

docker build基本命令

背景 我们经常会构建属于我们应用自己的镜像&#xff0c;这种情况下编写dockerfile文件不可避免&#xff0c;本文就来看一下常用的dockerfile的指令 常用的dockerfile的指令 首先我们看一下docker build的执行过程 ENV指令&#xff1a; env指令用于设置shell的环境变量&am…

1019: 【C1】【循环】【for】满足条件的数累加

题目描述 将正整数 m 和 n 之间&#xff08;包括 m 和 n&#xff09;能被 17 整除的数累加。其中&#xff0c;0 < m < n < 1000。 输入 一行&#xff0c;包含两个整数m和n&#xff0c;其间&#xff0c;以一个空格间隔 输出 输出一行&#xff0c;包行一个整数&…

【c语言】字符函数和字符串函数(下)

前言 书接上回 【c语言】字符函数和字符串函数(上) 上一篇讲解的strcpy、strcat、strcmp函数的字符串长度是不受限制的 而本篇strncpy、strncat、strcnmp函数的字符串长度是受限制的 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;…

JANGOW: 1.0.1

kali:192.168.223.128 主机发现 nmap -sP 192.168.223.0/24 端口扫描 nmap -p- 192.168.223.154 开启了21 80端口 web看一下&#xff0c;有个busque.php参数是buscar,但是不知道输入什么&#xff0c;尝试文件包含失败 扫描目录 dirsearch -u http://192.168.223.154 dirse…

Python的字符串操作你用对了吗?

1.创建字符串 字符串指的是使用一对单引号或者一对双引号或者一对三个单引号或者一对三个双引号包裹的文本 # 创建字符串str1 hellostr2 "world"print(str1)print(str2) # 使文本原样输出str3 床上明月光&#xff0c;疑是地上霜。str4 """举头望…

零基础韩语怎么学,柯桥韩语培训留学培训

应用”ㅗ”的单词为&#xff1a; 辅音ㄱ 发音时&#xff0c;将舌后部分向上抬起&#xff0c;舌根接触软腭堵住气流&#xff0c;然后放开使气流冲出而成音。它跟汉语中的”g” 歌(ge)古(gu) 国(guo)中的声母比较相似。 应用”ㄱ”的单词为&#xff1a; &#xff08;1&#x…

10.vue学习笔记(组件数据传递-props回调函数子传父+透传Attributes+插槽slot)

文章目录 1.组件数据传递2.透传Attributes&#xff08;了解&#xff09;禁用Attributes继承 3.插槽slot3.1.插槽作用域3.2.默认内容3.3.具名插槽3.4.插槽中的数据传递3.5.具名插槽传递数据 1.组件数据传递 我们之前讲解过了组件之间的数据传递&#xff0c;props 和 自定义事件…

算法【线性表的查找-顺序查找】

线性表的查找-顺序查找 顺序查找基本思想应用范围顺序表的表示数据元素类型定义查找算法示例分析 时间效率分析顺序查找的特点如何提高查找效率 顺序查找 基本思想 在表的多种结构定义方式中&#xff0c;线性表是最简单的一种。而顺序查找是线性表查找中最简单的一种。 顺序查…

Superhuman 邮箱的替代方案是什么?

Superhuman是一个极好的人工智能工具在电子邮件助理领域。根据SimilarWeb的最新统计&#xff0c;它在全球网站排名中排名第21980位&#xff0c;月访问量为1751798。然而市场上还有许多其他优秀的选择。为了帮助您找到最适合您需求的解决方案&#xff0c;我们为您精心挑选了10种…

Python进阶学习:json.dumps()和json.dump()的区别

Python进阶学习&#xff1a;json.dumps()和json.dump()的区别 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您…

Golang基础面试题1

来源于fullstack&#xff0c;绿色为重点 1. Go语言是什么 Go 是一种通用编程语言&#xff0c;设计初衷是为了进行系统编程。它最初是由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 在 2007 年开发的。Go 语言是强类型且静态类型的&#xff0c;它内置了对垃圾回收…

在Ubuntu上为ARM 8处理器安装Python 3.10.4虚拟环境指南

在Ubuntu上为ARM 8处理器安装Python 3.10.4虚拟环境指南 安装Anaconda或Miniconda&#xff1a; 首先&#xff0c;您需要从官方网站下载适用于ARM架构的Anaconda或Miniconda安装包。下载完成后&#xff0c;在终端中使用bash Anaconda3-2019.10-Linux-armv8.sh&#xff08;文件…

【JVM】StringTable 字符串常量池

参考&#xff1a;javaGuide 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串&#xff08;String 类&#xff09;专门开辟的一块区域&#xff0c;主要目的是为了避免字符串的重复创建 String的不可变性 1.通过字面量的方式&#xff08;区别于new&#xff09;给一个…

利用小红书笔记详情API:轻松获取海量笔记内容

小红书是一个非常流行的社交分享平台&#xff0c;特别是深受年轻人喜欢&#xff0c;因为它提供了用户分享生活方式、时尚、美妆、旅行、美食等内容的功能。如果你希望从小红书上获取大量的笔记内容&#xff0c;那么利用小红书的API是一个很好的选择。下面&#xff0c;我将为你介…

图像复原天花板!IR开创性新作实现最佳视觉质量,修复更智能、更逼真

图像复原&#xff08;IR&#xff09;指在已知图像退化的原因和模型的情况下&#xff0c;通过一系列的逆过程来恢复出原始图像的过程。这是一个长期的低级视觉任务&#xff0c;也是图像处理领域的一个重要课题。 随着深度学习技术的发展&#xff0c;图像复原领域不断出现新的网…

容器库(13)-std::unordered_multimap

unordered_multimap是含有键值对的无序关联容器&#xff0c;搜索、移除和插入操作是平均常数的时间复杂度。unordered_multimap在内部没有按任何顺序排列&#xff0c;而是放在桶当中的&#xff0c;放进哪个桶是通过计算key的hash值来决定的。和unordered_map不同的是&#xff0…

Python算法题集_全排列

Python算法题集_全排列 题46&#xff1a;全排列1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【标记数组递归】2) 改进版一【指针递归】3) 改进版二【高效迭代模块】4) 改进版三【高效迭代模块极简代码】 4. 最优算法5. 相关资源 本文为Python…