【看源码】@Cacheable和@CacheEvict的原理, 批量key过期失效的原因分析

news2024/11/25 4:49:49

@Cacheable和@CacheEvict的坑, 批量key过期失效的原因分析

      • 前言
      • 测试代码
      • 源码
        • put缓存时最终key的产生
        • 看不同情况下, 是否能匹配Evict过期缓存
          • 1. 没有入参没有指定key的情况
          • 2. 有入参的情况
          • 3. 配置了allEntries的情况
      • 总结
    • 补充

前言

最近发现自己搭的一个项目返回的数据不太准确, 第一时间想到了是缓存的问题, 缓存代码如下

@Cacheable(value = "allroom")
    public List<Roomuserinfo> queryAll(){
        return mapper.queryAll();
    }
    @CacheEvict(value = {"roominfosbrobbot","roominfobyid","allroom"},key = "#roominfo.room_id")
    public Roominfo insert(Roominfo roominfo){
         boolean ans=mapper.insert(roominfo);
        return ans?roominfo:null;
    }

有个困扰我很久的问题就是allroom的evict是否生效.
类似上面这个场景, 我们经常有list()方法没指定key缓存了全部数据, 以及get(Integer id)指定了key是id缓存了单个数据, 最后我们在update(Integer id)方法想evict过期上面两种缓存

众所周知 @Cacheable 和 @CacheEvict 的使用要求其中之一就是value和key一样, 以及方法参数要一样. 因此单个的evict肯定没问题, 问题是所有的如何过期呢

测试代码

如下, 我将平时常见的几个场景都列出来了. 前提@Cacheable是仅指定了value,没有指定key
再看@CacheEvict

  • 有value没指定key
    • 有入参 insert
    • 没入参 flush
    • 有入参且配置allEntries insertAndAllEntries
  • 有value且指定key
    • 有入参 insertWithKey
    • 有入参且配置allEntries insertWithKeyAndAllEntries

    private List<String> data = new ArrayList<>();

    @Cacheable("testcach")
    public List<String> getAll(){
        return data;
    }

    @CacheEvict("testcach")
    public void insert(String aa){
        data.add(aa);
    }

    @CacheEvict(value = "testcach",allEntries = true)
    public void insertAndAllEntries(String aa){
        data.add(aa);
    }

    @CacheEvict("testcach")
    public void flush(){
    }

    @CacheEvict(value = "testcach",key = "#aa")
    public void insertWithKey(String aa){
        data.add(aa);
    }


    @CacheEvict(value = "testcach",key = "#aa",allEntries = true)
    public void insertWithKeyAndAllEntries(String aa){
        data.add(aa);
    }

直接放测试结果

    @Test
    public void fun01(){
        cacheBiz.insert("aa"); //key aa, 无效
        List<String> all = cacheBiz.getAll();
        cacheBiz.insert("bb");//key bb, 无效
        all = cacheBiz.getAll();
        cacheBiz.insertAndAllEntries("cc");//有效
        all = cacheBiz.getAll();
        cacheBiz.insertWithKey("dd"); //key dd,无效
        all = cacheBiz.getAll();
        cacheBiz.insertWithKeyAndAllEntries("ee");//有效
        all = cacheBiz.getAll();
        cacheBiz.flush(); //有效
    }

源码

来看源码是为什么

put缓存时最终key的产生

  1. 首先我们的缓存注解的方法, 都会被代理并触发拦截器org.springframework.cache.interceptor.CacheInterceptor的invoke()方法
  2. invoke方法里跳转到org.springframework.cache.interceptor.CacheAspectSupport#execute()方法
  3. 重点来了, 这个方法做的事包括: 过期缓存, 判断条件然后获取缓存, put缓存
  4. 在按顺序处理缓存时, 会调用一个重要的方法
private void performCacheEvict(
			CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {

		Object key = null;
		for (Cache cache : context.getCaches()) {
			if (operation.isCacheWide()) {
				logInvalidating(context, operation, null);
				doClear(cache);
			}
			else {
				if (key == null) {
					key = generateKey(context, result);
				}
				logInvalidating(context, operation, key);
				doEvict(cache, key);
			}
		}
	}
		protected Object generateKey(@Nullable Object result) {
			if (StringUtils.hasText(this.metadata.operation.getKey())) {
			//如果我们写了el表达式的key
				EvaluationContext evaluationContext = createEvaluationContext(result);
				return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
			}
			//如果没有,默认生成
			return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
		}

/**
* 生成key
*/
	public static Object generateKey(Object... params) {
		if (params.length == 0) {
		//如果没有入参, 返回SimpleKey对象作为入参
			return SimpleKey.EMPTY;
		}
		if (params.length == 1) {
		//如果有一个, 直接返回
			Object param = params[0];
			if (param != null && !param.getClass().isArray()) {
				return param;
			}
		}
		//如果有多个入参, 包装返回
		return new SimpleKey(params);
	}
  1. 如果key为null, 会生成一个key, 再看generateKey里, 第一个if就是我们手动指定了EL表达式的key, 就会解析表达式, 否则就用默认的key生成器, 也就是org.springframework.cache.interceptor.SimpleKeyGenerator返回的SimpleKey实例.
  2. 再回到execute方法, apply方法里把put请求一个个执行
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}
		public void apply(@Nullable Object result) {
			if (this.context.canPutToCache(result)) {
				for (Cache cache : this.context.getCaches()) {
					doPut(cache, this.key, result);
				}
			}
		}
  1. 按doPut一路点下来, 一直到RedisCache的put方法, 里面cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());这就是我们很熟悉的redis.setValue(name,key,value,expireTime)
  2. 再从createAndConvertCacheKey方法一路点下来, 可以看到将key对象转成了string, 并和name(即@cacheable的value)拼装在一起. 就得到了testcach::SimpleKey []
	protected String createCacheKey(Object key) {
		//将object转string, 默认就是 simpleKey[]
		String convertedKey = convertKey(key);

		if (!cacheConfig.usePrefix()) {
			return convertedKey;
		}

		return prefixCacheKey(convertedKey);
	}
	private String prefixCacheKey(String key) {

		// allow contextual cache names by computing the key prefix on every call.
		//这里拼装name和key. 后面其实name就只用于了执行命令时lock的锁(取决于你配置的是lock还是非lock的RedisWiter 
		//@see  RedisCacheWriter.nonLockingRedisCacheWriter)
		return cacheConfig.getKeyPrefixFor(name) + key;
	}

看不同情况下, 是否能匹配Evict过期缓存

1. 没有入参没有指定key的情况

这也是我测试的时候使用的@Cacheable的情况
可以看到key也是一样SimpleKey对象, name和key匹配, 可以删除
在这里插入图片描述

2. 有入参的情况

有入参, 按上面的generateKey方法可得知, 如果有el表达式则解析, 没有的话, 如果有一个参数, 则以这个参数为key, 我测试方法正好是1个参数
name 和 key 不匹配, 不会过期缓存
在这里插入图片描述

3. 配置了allEntries的情况

从图中可知, 方法会走到isCacheWide调用clear方法而不是evict方法

到RedisCache的clear方法看看

	@Override
	public void clear() {

		byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
		cacheWriter.clean(name, pattern);
	}


...
	byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
			.toArray(new byte[0][]);

	if (keys.length > 0) {
		connection.del(keys);
	}

可以看到使用的*模糊匹配, keys命令, 这个在一般生产环境是禁止使用的, 性能影响很大
在这里插入图片描述

总结

经上总结, 如果有这种getAll()方法, 只有value没有key的话, 比较推荐两种Evict方法:

  1. 使用常量key, 即用单引号包裹的el表达式. @Cacheable(value=“test”,key=“‘list’”), @CacheEvict同理.
  2. 不要和有入参的方法混合使用, 单独开一个无参方法和Cacheable无参一样.
  3. 如果非要和有入参的一起用, 用@Caching
    @Caching(
            evict = {
                    @CacheEvict(value = {"roominfosbrobbot","roominfobyid"},key = "#roominfo.room_id"),
                    @CacheEvict(value = "allroom",key = "'list'")
            }
    )

补充

在测试的时候发现了一个坑.

配置redis序列器Jackson2JsonRedisSerializer在反序列化字符串数组时报错springboot redis: xxx as a subtype of [simple type, class java.lang.Object]: no such class found
是这个序列器的问题

后面换成GenericFastJsonRedisSerializer, 能是能反序列化解析了, 但是解析后是JsonArray对象, 强转String[] 同样报错
JSONArray cannot be cast to class [Ljava.lang.String

解决:

  1. 尽量不要用字符串数组作为缓存的return结果
  2. GenericFastJsonRedisSerializer, 但是用List来接收 (因为JSONArray实现了List接口)

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

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

相关文章

网工Python之路——Netmiko模块实验(思科)

小白网工的python之路 「Python 网络自动化」Netmiko 实验环境 我的实验环境是GNS3搭建拓扑图&#xff0c;用云桥接到在VMware Workstation 16运行的CentOS 7, CentOS 7上搭建好了python3.8&#xff0c; 所有交换机已经预配好了SSH服务&#xff0c;ssh登录账号为python&…

paddle

paddle预测库 git config --global http.postBuffer 1048576000 git clone --recursive https://github.com/PaddlePaddle/Paddle.git 修改CMakeLists.txt mkdir build cd buildcmake -DWITH_CONTRIBOFF -DWITH_MKLOFF -DWITH_MKLDNNOFF -DWITH_TESTINGOFF -DCMAKE_BUILD_TY…

DBCO-PEG-Ferrocene,Ferrocene-PEG-DBCO,DBCO偶联修饰二茂铁

DBCO-PEG-Ferrocene &#xff0c;Ferrocene-PEG-DBCO&#xff0c;二苯并环辛炔-聚乙二醇-二茂铁&#xff0c;DBCO偶联修饰二茂铁产品规格&#xff1a; 1.CAS号&#xff1a;N/A 2.分子量MV&#xff1a;1000、2000、3400、5000、10000、20000等可按需进行定制 3.包装规格&#xf…

数据结构与算法_空间复杂度

同时间复杂度一样&#xff0c;空间复杂度也是数学的函数表达式。 空间复杂度不是程序占用了多少 bytes的空间&#xff0c;因为这个也没太大意义&#xff0c;所以空间复杂度算的是运行的过程中临时的、额外的变量的个数。 空间复杂度计算规则基本跟实践 复杂度类似&#xff0c…

Flutter For App——一个简单的豆瓣APP

一个简单的豆瓣APP效果视频功能简述功能第三方库接口简述底部导航栏效果图实现初始化BottomNavigationBarItembottomNavigationBar切换页面导致bottomNavigationBar子页面重绘Top250榜单效果图实现Widget树FutureBuilder异常ListView上拉加载电影详情效果图实现高斯模糊网络数据…

设计模式原则 - 单一职责原则(一)

单一职责原则一 官方定义基本介绍二 案例演示普通方式实现解决方案解决方案一解决方案案例分析解决方案二解决方案案例分析案例总结三 注意事项&细节四 如何遵守单一职责原则&#xff1f;一 官方定义 单一职责原则&#xff08;Single Responsibility Principle, SRP&#x…

用Spark写入Mysql的特别注意事项

相信有部分刚入门的小伙伴对于spark写入Mysql的过程不太熟悉。特意写一篇文章讲一下这个注意事项&#xff0c;以免“上大当” 我们先看一个小伙伴写的一段spark写入mysql的代码 public static void trans(SparkSession spark,String pro_table, String pro_url, String pro_dr…

微服务框架 SpringCloud微服务架构 服务异步通讯 52 惰性队列 52.1 消息堆积问题

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 服务异步通讯 文章目录微服务框架服务异步通讯52 惰性队列52.1 消息堆积问题52.1.1 消息堆积问题52 惰性队列 52.1 消息堆积问题 52.1.1 消…

11基于主从博弈理论的共享储能与综合能源微网优化运行研究(MATLAB程序)

参考文献&#xff1a; 基于主从博弈理论的共享储能与综合能源微网优化运行研究——帅轩越&#xff08;2022电网技术&#xff09; 主要内容; 代码主要做的是基于主从博弈理论的共享储能与综合能源微网优化运行研究&#xff0c;首先介绍了系统运行框架&#xff0c;分析了系统内…

一文了解各种高精度室内定位技术

在消费需求和创新技术驱动下&#xff0c;可穿戴设备和物联网产品的发展驶入了快车道&#xff0c;GNSS定位功能在无人驾驶、智能设备、资产追踪等日趋智能化中广泛应用&#xff0c;而随着万物互联时代的到临&#xff0c;物联网技术围绕人员、资产的室内位置服务需求也愈加强烈。…

vue使用

目录 路由History模式打包页面空白 项目放根目录 -- 配置 项目放二级目录 -- 配置 路由History模式打包页面空白 项目放根目录 -- 配置 router > index.js 修改 base const router new VueRouter({mode: history,// base: process.env.BASE_URL,base: /,routes, }) ngi…

【Redis集群专题】「集群技术三部曲」介绍一下常用的Redis集群机制方案的原理和指南(入门篇)

集群化的方案 Redis的Sentinel解决了主从复制故障不能自动迁移的问题&#xff0c;但是主节点的写性能和存储能力依然是受到了Redis单机容量有限的限制&#xff0c;所以使用Redis集群去解决这个问题&#xff0c;将Redis的数据根据一定的规则分配到多台机器。 Redis集群方案 R…

【git】

目录第一章 简介 1&#xff0e;1 版本控制 1.1.1 本地版本控制1.1.2 集中式版本控制1.1.3 分布式版本控制 第二章 基础篇 2.1 下载代码 2.2 更新代码 2.2.1 清空本地未被跟踪内容2.2.2更新代码使之与库上同步 2.3 修改 2.3.1 Vim2.3.2 Sed2.3.3 Awk 2.4 查看状态 2.5 保存代…

通俗易懂的java设计模式(3)-观察者设计模式

什么是观察者设计模式 观察者模式主要应用在对象存在一对多关系的情况下&#xff0c;那么如果一个对象&#xff0c;依赖于另一个对象&#xff0c;那个被依赖的对象一旦被修改&#xff0c;依赖于他的那个对象也会被观察者所告知。 观察者模式又被称作为发布-订阅模式&#xff0c…

2022 UUCTF

目录 <1> Web (1) websign(禁用js绕过) (2) ez_rce(?>闭合 rce) (3) ez_unser(引用传递) (4) ez_upload(apache后缀解析漏洞) (5) ezsql(union注入) (6) funmd5(代码审计 %0a绕过preg_replace) (7) phonecode(伪随机数漏洞) (8) ezpop(反序列化字符串逃逸) …

[附源码]Nodejs计算机毕业设计教师业绩考核和职称评审系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

[附源码]Nodejs计算机毕业设计教务管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

燃尽图——项目管理中的必备工具

燃尽图可以使项目经理和团队可以快速查看其工作负荷的进度以及项目按计划完成的情况。 在项目管理中&#xff0c;通过燃尽图不仅能了解项目进度&#xff0c;还能分析出项目中问题&#xff0c;及时进行风险控制。 燃尽图是用于表示剩余工作量的工作图表&#xff0c;由横轴&…

WebDAV之葫芦儿•派盘+RS文件管理器

RS文件管理器 支持WebDAV方式连接葫芦儿派盘。 手机本地文件,网盘、共享文件,如何集中管理?推荐您使用Rs文件管理器,还支持WebDAV等功能。 Rs文件管理器是一款功能强大的手机文件管理器。有强大的本地和网络文件管理功能,让您更方便的管理你的手机、平板、电脑和网盘。…

MOMO CODE SEC INSPECTOR-Java项目静态代码安全审计idea插件工具

工具地址 https://github.com/momosecurity/momo-code-sec-inspector-java安装 1、确认IDE版本&#xff1a;Intellij IDEA ( Community / Ultimate ) > 2018.32、IDEA插件市场搜索"immomo"安装。使用方式 被动&#xff1a;装完愉快的打代码&#xff0c;一边它会提…