项目中 使用 spring cache redis 出现大量keys* 慢查询排查以及修复

前言

业务反馈 redis里有大量的慢查询 而且全是keys 的命令

排查

  • 首先登录 阿里云查看redis的慢查询日志 如下
    在这里插入图片描述

  • 主要使用到redis cache的注解功能 分别是 @CacheEvict 和 @Cacheable
    注意 CacheEvict 这个比较特殊 会进行驱逐缓存 说白就会删除缓存或者让缓存失效

  • 第一时间想到的就是我们自定义的 cacheManager 其中 自定义了 remove

    public class DefaultRedisCacheWriter implements RedisCacheWriter {/*** 删除,源码中逻辑是删除指定的键,* 目前修改为既可以删除指定键的数据,* 也是可以删除某个前缀开始的所有数据** @param name* @param key*/@Overridepublic void remove(String name, byte[] key) {Assert.notNull(name, "Name must not be null!");Assert.notNull(key, "Key must not be null!");execute(name, connection -> {// 获取某个前缀所拥有的所有的键,某个前缀开头,后面肯定是*Set<byte[]> keys = connection.keys(key);int delNum = 0;Assert.notNull(keys, "keys must not be null!");for (byte[] keyByte : keys) {delNum += connection.del(keyByte);}return delNum;});}@Overridepublic void clean(String name, byte[] pattern) {Assert.notNull(name, "Name must not be null!");Assert.notNull(pattern, "Pattern must not be null!");execute(name, connection -> {boolean wasLocked = false;try {if (isLockingCacheWriter()) {doLock(name, connection);wasLocked = true;}byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()).toArray(new byte[0][]);if (keys.length > 0) {connection.del(keys);}} finally {if (wasLocked && isLockingCacheWriter()) {doUnlock(name, connection);}}return "OK";});}@Overridepublic void clearStatistics(String s) {}}
    
  • 问题的重点 首先 在于这个remove方法 源码中的逻辑 是删除单个key 修改后的逻辑是删除 这个key匹配的所有的key 然后在循环删除 方便是方便了,如果某个格式的key过多的话 就会导致这个keys 命令执行过长 导致慢查询 其次还有clean方法这个方法也会触发 keys 的逻辑

    //源码中的删除逻辑 
    @Override
    public void remove(String name, byte[] key) {Assert.notNull(name, "Name must not be null!");Assert.notNull(key, "Key must not be null!");execute(name, connection -> connection.del(key));statistics.incDeletes(name);
    }
    

先说一下 解决思路 第一 就是将这个remove方法 修改为只删除当前key 而不是 模糊获取当前key匹配的key 并删除 最简单地办法直接将自定义DefaultRedisCacheWriter类中的remove方法替换为 源码中的实现

@Override
public void remove(String name, byte[] key) {Assert.notNull(name, "Name must not be null!");Assert.notNull(key, "Key must not be null!");execute(name, connection -> connection.del(key));statistics.incDeletes(name);
}

第二 屏蔽或者减少 clean 方法的触发 这个allEntries 属性设置相关 默认值为false 并不会触发 clean 方法的执行


本着追本溯源的精神 我们来继续看下源码

源码分析

可以看到这里DefaultRedisCacheWriter里有两个方法 一个是remove 一个clean 都是清除的方法 那么这两个方法分别怎么调用 以及什么时候调用

先来看 remove的调用链路

上层调用 org.springframework.cache.interceptor.AbstractCacheInvoker#doEvict

protected void doEvict(Cache cache, Object key, boolean immediate) {try {if (immediate) {cache.evictIfPresent(key);}else {cache.evict(key);}}catch (RuntimeException ex) {getErrorHandler().handleCacheEvictError(ex, cache, key);}
}

方法中会根据 是否立刻清除标记来决定使用哪个方法 immediate为true 标识 立即清除,会调用cache.evictIfPresent(key); 方法 这个方法在redisCache类中 并没有实现 走的时cache接口的默认实现

//org.springframework.cache.Cache#evictIfPresent
default boolean evictIfPresent(Object key) {evict(key);return false;
}

本质调用的还是 evict 接口 最后 evict 调用的时remove方法

在上层调用 org.springframework.cache.interceptor.CacheAspectSupport#performCacheEvict

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, operation.isBeforeInvocation());}else {if (key == null) {key = generateKey(context, result);}logInvalidating(context, operation, key);doEvict(cache, key, operation.isBeforeInvocation());}}
}

CacheAspectSupport 这个类就是缓存的核心逻辑
上层调用 org.springframework.cache.interceptor.CacheAspectSupport#processCacheEvicts

private void processCacheEvicts(Collection<CacheOperationContext> contexts, boolean beforeInvocation, @Nullable Object result) {for (CacheOperationContext context : contexts) {CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {performCacheEvict(context, operation, result);}}
}

在上层调用 org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.reflect.Method, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts)

在上层调用 : org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.Object, java.lang.reflect.Method, java.lang.Object[])

最终会由拦截器 进行调用 org.springframework.cache.interceptor.CacheInterceptor#invoke
完成 cache注解的拦截 会执行

clean 方法的调用链

第一个调用的位置 org.springframework.data.redis.cache.RedisCache#clear

@Override
public void clear() {byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);cacheWriter.clean(name, pattern);
}

上层调用的位置 org.springframework.cache.interceptor.AbstractCacheInvoker#doClear

protected void doClear(Cache cache, boolean immediate) {try {if (immediate) {cache.invalidate();}else {cache.clear();}}catch (RuntimeException ex) {getErrorHandler().handleCacheClearError(ex, cache);}
}

在上层和 remove的调用地方相同 org.springframework.cache.interceptor.CacheAspectSupport#performCacheEvict

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, operation.isBeforeInvocation());}else {if (key == null) {key = generateKey(context, result);}logInvalidating(context, operation, key);doEvict(cache, key, operation.isBeforeInvocation());}}
}

后面逻辑也和 remove方法顶层调用链相同

调用链路分析

重点来看下 这个performCacheEvict方法 循环体的判断条件operation.isCacheWide(), operation.isCacheWide() 属性值这个会影响进入到哪个逻辑 分支
这个属性的赋值具体在CacheEvictOperation的内部类 Builde中 org.springframework.cache.interceptor.CacheEvictOperation.Builder#setCacheWide方法中

具体的调用是在设置 CacheEvict 注解 属性到 实体类CacheEvictOperation 中 即 org.springframework.cache.annotation.SpringCacheAnnotationParser#parseEvictAnnotation

private CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheEvict cacheEvict) {CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder();builder.setName(ae.toString());builder.setCacheNames(cacheEvict.cacheNames());builder.setCondition(cacheEvict.condition());builder.setKey(cacheEvict.key());builder.setKeyGenerator(cacheEvict.keyGenerator());builder.setCacheManager(cacheEvict.cacheManager());builder.setCacheResolver(cacheEvict.cacheResolver());builder.setCacheWide(cacheEvict.allEntries());builder.setBeforeInvocation(cacheEvict.beforeInvocation());defaultConfig.applyDefault(builder);CacheEvictOperation op = builder.build();validateCacheOperation(ae, op);return op;
}

这里看到 CacheWide 属性来自注解 属性中allEntries 属性
在 @CacheEvict 注解allEntries的默认属性为 false
业务代码在使用过程也未进行修改
那这个逻辑就会进入到else逻辑中

else {if (key == null) {key = generateKey(context, result);}logInvalidating(context, operation, key);doEvict(cache, key, operation.isBeforeInvocation());
}

最终调用的时 doEvict 方法 org.springframework.cache.interceptor.AbstractCacheInvoker#doEvict
该方法定义如下

protected void doEvict(Cache cache, Object key, boolean immediate) {try {if (immediate) {cache.evictIfPresent(key);}else {cache.evict(key);}}catch (RuntimeException ex) {getErrorHandler().handleCacheEvictError(ex, cache, key);}
}

同样该方法又存在逻辑判断分支 取决于 immediate 传入值 。 在@CacheEvict 注解中beforeInvocation 默认也是false 于是就走到了else的逻辑 cache.evict(key);
该方法又会调用到

RedisCache 的 evict org.springframework.data.redis.cache.RedisCache#evict

/** (non-Javadoc) * @see org.springframework.cache.Cache#evict(java.lang.Object)*/
@Override
public void evict(Object key) {cacheWriter.remove(name, createAndConvertCacheKey(key));
}

最终也会调用到 我们自定义 DefaultRedisCacheWriter类的 remove 方法

org.springframework.cache.interceptor.AbstractCacheInvoker#doEvict 这个方法比较有意思的是 当immediate 为true时 调用的 cache.evictIfPresent(key); 然后在redisCache中并未实现该方法,会走到org.springframework.cache.Cache#evictIfPresent 接口的 默认方法 最后调用的也是DefaultRedisCacheWriter类的 remove 方法

protected void doEvict(Cache cache, Object key, boolean immediate) {try {if (immediate) {cache.evictIfPresent(key);}else {cache.evict(key);}}catch (RuntimeException ex) {getErrorHandler().handleCacheEvictError(ex, cache, key);}
}// org.springframework.cache.Cache#evictIfPresent
default boolean evictIfPresent(Object key) {evict(key);return false;
}

结论

影响org.springframework.cache.interceptor.CacheAspectSupport#performCacheEvict 方法调用链路的两个属性 取决于 @CacheEvict 注解中的allEntries 和 beforeInvocation的值

  • 当 allEntries 为true时会调用 doClear

    • 在doClear方法中 条件分支又取决于beforeInvocation
    • 当beforeInvocation 为true 调用 cache.invalidate(); 然而redisCache又没实现invalidate 默认调用 cache.clear(); 最终会调用 DefaultRedisCacheWriter的 clean
    • 当beforeInvocation 为true 调用 cache.clear(); 最终也会调用 DefaultRedisCacheWriter的 clean
  • 当 allEntries 为 false 时调用 doEvict

    • 在doEvict方法中 条件分支又取决于beforeInvocation
    • 当beforeInvocation 为true 调用 cache.evictIfPresent(); 然而redisCache又没实现evictIfPresent 默认调用 cache接口的默认实现 即调用 evict(); 而 evict方法 最终会调用 DefaultRedisCacheWriter的 remove
    • 当beforeInvocation 为true 调用 cache.evict(); 而evict方法 最终也会调用 DefaultRedisCacheWriter的 remove

the end !!!
good day !!!

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

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

相关文章

第十四届蓝桥杯省赛大学C组(C/C++)填充

原题链接&#xff1a;填充 有一个长度为 n 的 01 串&#xff0c;其中有一些位置标记为 ?&#xff0c;这些位置上可以任意填充 0 或者 1&#xff0c;请问如何填充这些位置使得这个 01 串中出现互不重叠的 0 和 1 子串最多&#xff0c;输出子串个数。 输入格式 输入一行包含一…

【保姆级教程】如何在 Windows 上实现和 Linux 子系统的端口映射

写在前面 上次分享【保姆级教程】Windows上安装Linux子系统&#xff0c;搞台虚拟机玩玩&#xff0c;向大家介绍了什么是虚拟机以及如何在Windows上安装Linux虚拟机。对于开发同学而言&#xff0c;经常遇到的一个问题是&#xff1a;很多情况下代码开发需要依赖 Linux 系统&…

多线程代码设计模式之单例模式

目录 设计模式引入 饿汉模式 懒汉模式 单例模式总结 设计模式引入 1.1.什么是设计模式 &#xff08;1&#xff09;设计模式就是一种代码的套用模板。例如&#xff1a;一类题型的步骤分别有哪些&#xff0c;是可以直接套用的。 &#xff08;2&#xff09;像棋谱&#xff…

代码随想录算法训练营DAY17|C++二叉树Part.4|110.平衡二叉树、257.二叉树的所有路径、404.左叶子之和

文章目录 110.平衡二叉树思路伪代码CPP代码 257.二叉树的所有路径思路伪代码实现CPP代码 404.左叶子之和思路伪代码CPP代码 110.平衡二叉树 力扣题目链接 文章讲解&#xff1a;110.平衡二叉树 视频讲解&#xff1a;后序遍历求高度&#xff0c;高度判断是否平衡 | LeetCode&…

lua学习笔记6(经典问题输出99乘法表)

print("************for循环的99乘法表*************") for i 1, 9 dolocal line "" -- 创建一个局部变量来累积每行的输出--local 是一个关键字&#xff0c;用于声明一个局部变量。for j 1, i doline line .. j .. "*" .. i .. ""…

电脑桌面上表格不见了怎么找回?这5个方法不要错过

在日常的办公和学习中&#xff0c;电脑桌面上的各种文件、文件夹和表格等无疑是我们较为频繁使用的资源。然而&#xff0c;有时我们可能会因为一些操作失误或者电脑问题&#xff0c;突然发现桌面上的某个表格文件神秘失踪了。面对这种情况&#xff0c;很多人可能会感到焦虑和不…

[WIP]Sora相关工作汇总VQGAN、MAGVIT、VideoPoet

视觉任务相对语言任务种类较多(detection, grounding, etc.)、粒度不同 (object-level, patch-level, pixel-level, etc.)&#xff0c;且部分任务差异较大&#xff0c;利用Tokenizer核心则为如何把其他模态映射到language space&#xff0c;并能让语言模型更好理解不同的视觉任…

Python-VBA函数基础知识-001

一、函数的定义&#xff1a; 函数(Function)是一段可重复使用的代码块&#xff0c;用于执行特定的任务或计算&#xff0c;并可以接受输入参数和返回输出结果。函数可以将复杂的问题分解为更小的子问题&#xff0c;提高代码的可读性和可维护性。 二、函数的组成&#xff1a; 在…

Spring Boot集成JWT快速入门demo

1.JWT是什么&#xff1f; JWT&#xff0c;英文全称JSON Web Token&#xff1a;JSON网络令牌。为了在网络应用环境间传递声明而制定的一种基于JSON的开放标准(RFC 7519)。这个规范允许我们使用JWT在客户端和服务端之间传递安全可靠的信息。JWT是一个轻便的安全跨平台传输格式&am…

前端零基础学习web3开发

目录 1 钱包 2 发起交易 3 出块 4 块高 5 矿工 6 Gas费 这一节&#xff0c;我们不说让人神往的比特币&#xff0c;不说自己会不会利用这个虚拟的货币来发财&#xff0c;也不说那些模模糊糊的知识&#xff0c;什么去中心化啦&#xff0c;什么奇妙的加密啦&#xff0c;我们…

AI 驱动强大是视频转换处理软件

由 AI 驱动的视频工具包。 增强、转换、录制和编辑视频AI 驱动的顶级视频工具包。 不论是老旧、低质、噪声或模糊的影片/图像&#xff0c;都能升级至 4K&#xff0c;稳定抖动的影片&#xff0c;提升帧率至 120/240fps&#xff0c;并能以全面 GPU 加速进行转换、压缩、录制和编辑…

盘点那些好用的SAP FIORI App (四)-应收账期报告

这个App的ID是IDCNAR, 其实也是一个T-Code, 也就是说&#xff0c;不光在FIORI app里面可以使用&#xff0c;在SAP GUI里面也是存在的&#xff0c;这个就属于我另一篇里面提到的&#xff0c;GUI和FIORI都可以使用的功能&#xff0c;但是前提是S4 HANA平台 操作的界面非常简单&am…

linux进阶篇:磁盘管理(一):LVM逻辑卷基本概念及LVM的工作原理

Linux磁盘管理(一)&#xff1a;LVM逻辑卷基本概念及LVM的工作原理 一、传统的磁盘管理 在传统的磁盘管理方案中&#xff0c;如果我们的磁盘容量不够了&#xff0c;那这个时候应该要加一块硬盘&#xff0c;但是新增加的硬盘是作为独立的文件系统存在的&#xff0c;原有的文件系…

即插即用篇 | RTDETR引入Haar小波下采样 | 一种简单而有效的语义分割下采样模块

本改进已集成到 RT-DETR-Magic 框架。 下采样操作如最大池化或步幅卷积在卷积神经网络(CNNs)中被广泛应用,用于聚合局部特征、扩大感受野并减少计算负担。然而,对于语义分割任务,对局部邻域的特征进行池化可能导致重要的空间信息丢失,这有助于逐像素预测。为了解决这个问…

接口日志处理类

类&#xff1a;ZCL_IFLOG_UTILITIES 属性&#xff1a;AUTH_RESULTS_LIST 类型&#xff1a; TY_AUTH_RESULT Private 受保护部分&#xff1a; PRIVATE SECTION.TYPES: BEGIN OF ty_auth_result,funcname TYPE ztall_logcfg-funcname,pass TYPE abap_bool,END OF ty_aut…

商城系统如何设计表

小商城&#xff1a;参考千小夜小程序 大商城&#xff1a; 首先根据某个商品的三级分类进来后&#xff0c;我们找到在这个分类下该商品的所有属性&#xff08;也就是泛指该商品不管怎么样都有这些属性&#xff09;&#xff0c;这里指的属性是规格包装&#xff0c;也就是基本属性…

线程池CompletableFuture异步编排复习笔记

一、线程回顾 1.1 初始化线程的 4 种方式 1&#xff09;、继承 Thread public static class Thread01 extends Thread {Overridepublic void run() {System.out.println("当前线程&#xff1a;" Thread.currentThread().getId());int i 10 / 2;System.out.print…

机器学习周记(第三十三周:文献阅读[GWO-GART])2024.4.1~2024.4.7

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文数据集 1.4 论文模型 2 相关知识 摘要 本周阅读了一篇使用GAT结合GRU预测PM2.5浓度的文章。论文模型为图注意力循环网络&#xff08;GART&#xff09;&#xff0c;首次提出了一种新型的多层GAT架构&…

AI预测福彩3D第27弹【2024年4月5日预测--第4套算法重新开始计算第12次测试】

今天继续按照合并后的算法进行测试&#xff0c;因为本套算法的命中率较高。以后有时间的话会在第二篇文章中发布排列3的预测结果。好了&#xff0c;废话不多说了&#xff0c;先上预测结果图&#xff0c;再上综合预测结果~ 2024年4月5日福彩3D的七码预测结果如下 第一套…

关于代码审查的一些思考

作为一名代码审查员&#xff0c;首先我们已经具备了丰富的代码开发经验&#xff0c;并且对提交的代码工程非常熟悉 代码审查可以发现并纠正代码中的错误、缺陷和不良实践。通过多人对代码进行仔细的检查和讨论&#xff0c;能够发现一些单独开发时难以察觉的问题&#xff0c;从…