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

news2024/12/23 1:46:03

前言

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

排查

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

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

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

    public class DefaultRedisCacheWriter implements RedisCacheWriter {
    
      
        /**
         * 删除,源码中逻辑是删除指定的键,
         * 目前修改为既可以删除指定键的数据,
         * 也是可以删除某个前缀开始的所有数据
         *
         * @param name
         * @param key
         */
        @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 -> {
                // 获取某个前缀所拥有的所有的键,某个前缀开头,后面肯定是*
                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;
            });
        }
    
        @Override
        public 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";
            });
        }
    
        @Override
        public 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.coloradmin.cn/o/1564455.html

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

相关文章

零基础入门多媒体音频(7)-AAOS audio

概览 Android Automotive OS (AAOS) 是基于核心的 Android 音频堆栈打造&#xff0c;以支持用作车辆信息娱乐系统。AAOS 负责实现信息娱乐声音&#xff08;即媒体、导航和通讯&#xff09;&#xff0c;但不直接负责具有严格可用性和时间要求的铃声和警告。 虽然 AAOS 提供了信号…

AJAX——介绍

同步与异步 原生的AJAX 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"wi…

UE4_碰撞_碰撞蓝图节点——Get/Set Collision Object Type

一、get collision object type set collision object type 二、 使用方法&#xff1a; 通过对射线检测命中物体的碰撞中的对象类型object type进行判定来重新设置碰撞的对象类型&#xff0c;来更改碰撞响应的物体响应的方式。比方说一开始不让你进门&#xff0c;你可以通…

Ansys Zemax | 如何将光栅数据从Lumerical导入至OpticStudio(上)

附件下载 联系工作人员获取附件 本文介绍了一种使用Ansys Zemax OpticStudio和Lumerical RCWA在整个光学系统中精确仿真1D/2D光栅的静态工作流程。将首先简要介绍方法。然后解释有关如何建立系统的详细信息。 本篇内容将分为上下两部分&#xff0c;上部将首先简要介绍方法工…

OpenHarmony实战:Combo解决方案之ASR芯片移植案例

本方案基于 OpenHarmony LiteOS-M 内核&#xff0c;使用 ASR582X 芯片的 DEV.WIFI.A 开发板进行开发移植。作为典型的 IOT Combo&#xff08;Wi-FiBLE&#xff09;解决方案&#xff0c;本文章介绍 ASR582X 的适配过程。 编译移植 目录规划 本方案的目录结构使用 Board 和 So…

Shadow

Shadow Mapping 最关键的思想是阴影出现的点是我们可以看到而光源看不到的点。 主要思路&#xff1a; 从光源渲染一个深度图表示能看到的地方从我们的眼中看到的点投射到光源相机中看光源相机能不能看到(是不是对应的深度) 这个方法中有一些问题&#xff1a; Hard shadows (poi…

UE4 方块排序动画

【动画效果】 入动画&#xff1a; 出动画&#xff1a; 【分析】 入动画&#xff1a;方块动画排序方式为Z字形&#xff0c;堆砌方向为X和Y轴向 出动画&#xff1a;方块动画排序方式为随机 【关键蓝图】 1.构建方块砌体 2.入/出动画

xss【2】

1.xss钓鱼 钓鱼攻击利用页面&#xff0c;fish.php黑客钓鱼获取到账号密码存储的位置 xss进行键盘记录 2.xss常规防范 3.xss验证payload XSS&#xff08;跨站攻击&#xff09;_details/open/ontoggle-CSDN博客

达梦DMHS-Manager工具安装部署

目录 1、前言 1.1、平台架构 1.2、平台原理 2、环境准备 2.1、硬件环境 2.2、软件环境 2.3、安装DMHS 2.3.1、源端DMHS前期准备 2.3.2、源端DMHS安装 2.3.3、目的端DMHS安装 3、DMHS-Manager客户端部署 3.1、启动dmhs web服务 3.2、登录web管理平台 4、添加DMHS实…

元宇宙虚拟空间的场景构造(二)

前言 该文章主要讲元宇宙虚拟空间的场景构造&#xff0c;基本核心技术点&#xff0c;不多说&#xff0c;直接引入正题。 场景的构造 使用引入的天空模块 this.sky new Sky(this); 在Sky模块里&#xff0c;有设置对其中的阳光进行不同时间段的光线处理。而天空又是怎么样的…

ArcGis研究区边界提取

ArcGis研究区边界提取 *0* 引言*1* 有的步骤0 引言 GRACE数据处理前要先确定研究范围,而大多情况下所选的研究区都是有特殊意义的,比如常年干旱、经济特区、降水丰富等,这些区域往往有精确的边界,那就要从大的区块中将研究范围抠出来,获取相应坐标,以量化区域重力变化。那…

视频基础学习四——视频编码基础一(冗余信息)

文章目录 前言一、编码压缩的原理1.空间冗余帧内预测 2.时间冗余帧间预测运动估计运动补偿 3.编码冗余4.视觉冗余 二、压缩编码的流程1.编码器2.编解码流程 总结 前言 上一篇文章介绍了视频帧率、码率、与分辨率。也介绍了为什么需要对视频进行压缩&#xff0c;因为720P、rgb2…

【隐私计算实训营007——隐语SCQL的架构详细拆解】

1.SCQL Overview SCQL属于隐私保护的BI。 1.1 对于安全聚合查询语言的两种常见的技术方案 1.2 SCQL系统组件 SCDB 部署在可信第三方&#xff0c;负责将query翻译成密态执行图&#xff0c;下发给SCQLEngine&#xff0c;本身不参与计算 SCQLEngine 部署在数据参与方&#xff…

某音乐平台歌曲信息逆向之webpack扣取

逆向网址 aHR0cHM6Ly95LnFxLmNvbS8 逆向链接 aHR0cHM6Ly95LnFxLmNvbS9uL3J5cXEvc29uZ0RldGFpbC8wMDJkdzRndjFabWlHdA 逆向接口 aHR0cHM6Ly91Ni55LnFxLmNvbS9jZ2ktYmluL211c2ljcy5mY2c 逆向过程 请求方式&#xff1a;POST 逆向参数 sign zzbd8c72309rdslvlnjwk8pthj2lw462f12…

Java 设计模式系列:备忘录模式

简介 备忘录模式是一种软件设计模式&#xff0c;用于在不破坏封闭的前提下捕获一个对象的内部状态&#xff0c;并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。 备忘录模式提供了一种状态恢复的实现机制&#xff0c;使得用户可以方便地回到一个特定…

微信小程序自定义弹窗组件

业务背景&#xff1a;弹窗有时字体较多&#xff0c;超过7个字&#xff0c;不适用wx.showToast. 组件代码 <view class"toast-box {{isShow? show:}}" animation"{{animationData}}"><view class"toast-content" ><view class&q…

20240402—Qt如何通过动态属性设置按钮样式?

前言 正文 1、点击UI文件 2、选择Bool型或是QString 3、设置后这里出现动态属性 4、这qss文件中绑定该动态属性 QPushButton[PopBlueBtn"PopBlueBtn"]{background-color:#1050B7;color:#FFFFFF;font-size:20px;font-family:Source Han Sans CN;//思源黑体 CNbor…

实验四 微信小程序智能手机互联网程序设计(微信程序方向)实验报告

请编写一个用户登录界面&#xff0c;提示输入用户名和密码进行登录&#xff1b; 代码 index.wxml <view class"user"> <form bindreset""> <view>用户名&#xff1a;</view><input type"text"name""/>…

UE4_动画基础_ 瞄准偏移1D(Aim Offset Blend Space 1D)

瞄准偏移1D基本上可以完成角色的向左看向右看或者向上看向下看&#xff0c;像混合空间1D一样只有一个轴向可用。 操作步骤&#xff1a; 1、新建第三人称模板项目。 2、右键——动画——瞄准偏移1D 选取骨骼 双击打开 3、瞄准偏移混合的是姿势&#xff0c;我们需要创建姿势。 …

FPGA高端项目:解码索尼IMX327 MIPI相机+图像缩放+视频拼接+HDMI输出,提供开发板+工程源码+技术支持

目录 1、前言免责声明 2、相关方案推荐本博主所有FPGA工程项目-->汇总目录我这里已有的 MIPI 编解码方案 3、本 MIPI CSI-RX IP 介绍4、个人 FPGA高端图像处理开发板简介5、详细设计方案设计原理框图IMX327 及其配置MIPI CSI RX图像 ISP 处理自研HLS图像缩放详解Video Mixer…