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

前言

业务反馈 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 !!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

内网穿透的应用-如何使用Docker本地部署Dify LLM结合内网穿透实现公网访问本地开发平台

文章目录 1. Docker部署Dify2. 本地访问Dify3. Ubuntu安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 本文主要介绍如何在Linux Ubuntu系统以Docker的方式快速部署Dify,并结合cpolar内网穿透工具实现公网远程访问本地Dify&#xff01; Dify 是一款…

10款白嫖党必备的ai写作神器,你都知道吗? #媒体#人工智能#其他

从事自媒体运营光靠自己手动操作效率是非常低的&#xff0c;想要提高运营效率就必须要学会合理的使用一些辅助工具。下面小编就跟大家分享一些自媒体常用的辅助工具&#xff0c;觉得有用的朋友可以收藏分享。 1.飞鸟写作 这是一个微信公众号 面向专业写作领域的ai写作工具&am…

多叉树题目:子树中标签相同的结点数

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;子树中标签相同的结点数 出处&#xff1a;1519. 子树中标签相同的结点数 难度 5 级 题目描述 要求 给你一个树&#xff08;即一个连通的无向无环图…

2024年中国金融科技(FinTech)行业发展洞察报告

核心摘要&#xff1a; 金融监管体系的改革推动金融科技行业进入超级监管时代&#xff0c;数据要素应用与金融场景建设成为如今行业关注的重要领域&#xff0c;为金融机构提供以业务需求为导向的技术服务成为“厚积成势”阶段行业发展的新目标&#xff0c;市场参与者的“业技融…

峥嵘九载,逐云而上:青果乔迁新址,乘风破浪再起新篇

4月1日&#xff0c;近百名员工和诸多合作伙伴齐聚&#xff0c;共同见证了青果九周年庆典暨乔迁仪式这一里程碑式的时刻。 新起点&#xff0c;新征程&#xff0c;再启航&#xff01; 以新为序&#xff0c;共赴新征程 在典礼上&#xff0c;青果创始人和高管分别发表了致辞&#…

飞企互联-FE企业运营管理平台 druid路径 弱口令漏洞复现

0x01 产品简介 飞企互联-FE企业运营管理平台是一个基于云计算、智能化、大数据、物联网、移动互联网等技术支撑的云工作台。这个平台可以连接人、链接端、联通内外,支持企业B2B、C2B与O2O等核心需求,为不同行业客户的互联网+转型提供支持。 0x02 漏洞概述 飞企互联-FE企业…

Linux函数学习 select

1、Linux select 函数 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); nfds 最大文件fd 1 readfds 监听可读文件集合fd writefds 监听可写文件集合fd exceptfd 监听异常文件集…

我去,PMP原来不是所有人都能报!

很多人可能觉得PMP的报名条件很复杂&#xff0c;又是经验要求&#xff0c;又是学历要求的&#xff0c;网络上关于PMP报名条件说的层出不穷&#xff0c;今天给大家统一一下&#xff0c;报名PMP究竟需要什么条件&#xff1a; 官方报考条件&#xff1a; 一、报名考生必须具备35小…

Social Skill Training with Large Language Models

Social Skill Training with Large Language Models 关键字&#xff1a;社交技能训练、大型语言模型、人工智能伙伴、人工智能导师、跨学科创新 摘要 本文探讨了如何利用大型语言模型&#xff08;LLMs&#xff09;进行社交技能训练。社交技能如冲突解决对于有效沟通和在工作和…

rust学习(tokio中tcp_stream调用的问题)

问题&#xff1a; 我们涉及了一个socket连接的类&#xff0c;每次收到数据以后&#xff0c;我们都会把tokio::net::TcpStream对应的tcp_stream传递给其他线程。 起初设计如下&#xff1a; pub struct TarNetStream {stream:TcpStream, //1... }pub trait TarListener {fn on…

[C++][算法基础]字符串哈希(哈希表)

给定一个长度为 n 的字符串&#xff0c;再给定 m 个询问&#xff0c;每个询问包含四个整数 l1,r1,l2,r2&#xff0c;请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。 字符串中只包含大小写英文字母和数字。 输入格式 第一行包含整数 n 和 m&#x…

Web 后台项目,权限如何定义、设置、使用:菜单权限、按钮权限 ts element-ui-Plus

Web 后台项目&#xff0c;权限如何定义、设置、使用&#xff1a;菜单权限、按钮权限 ts element-ui-Plus 做一个后台管理项目&#xff0c;里面需要用到权限管理。这里说一下权限定义的大概&#xff0c;代码不多&#xff0c;主要讲原理和如何实现它。 一、权限管理的原理 权限…

MR-J4W2-77B 三菱伺服放大器2轴一体(750W型)

MR-J4W2-77B 三菱伺服放大器2轴一体&#xff08;750W型&#xff09; MR-J4W2-77B用户手册、MR-J4W2-77B外部连接 MR-J4W2-77B参数说明&#xff1a;2轴一体SSCNETⅢ/H接口型、0.75kW用、三相或单相AC200V~240V 三菱伺服放大器MR-J4W2-77B的详细规格说明&#xff1a; [输出] …

数据库基础认识

目录 数据库基本认识 见一见数据库 主流数据库 Windows下启动MySQL 服务器&#xff0c;数据库&#xff0c;表关系 MySQL架构 SQL分类 存储引擎 数据库基本认识 哪一个是客户端哪一个是服务端&#xff1f; 为什么需要数据库&#xff1f; 文件保存数据有以下几个缺点&a…

高效测试丨怿星RTP协议测试解决方案

近几年&#xff0c;车内音视频娱乐系统不断发展&#xff0c;功能不断丰富&#xff0c;对于音视频的传输需求也逐渐增多&#xff0c;随着车载以太网的日渐成熟&#xff0c;各主机厂逐步方案落地、成本逐步降低&#xff0c;基于车载以太网的音视频传输也在逐步应用&#xff0c;常…

推荐一款很强大的SCADA工业组态软件

可以广泛应用于化工、石化、制药、冶金、建材、市政、环保、电力等几十个行业。 I官网网站:www.hcy-soft.com |体验地址:http://www.byzt.net:60/sm/ 一、产品简介 BY组态是完全自主研发的集实时数据展示、动态交互等一体的全功能可视化平台。帮助物联网、工业互联网、电力能…

两相欠压继电器 WY-35A3 额定输入电压100V 导轨安装 JOSEF约瑟

系列型号&#xff1a; WY-35A4电压继电器&#xff1b;WY-35B4电压继电器&#xff1b; WY-35C4电压继电器&#xff1b;WY-35D4电压继电器&#xff1b; WY-35A4D电压继电器&#xff1b;WY-35A4T电压继电器&#xff1b; WY-35B4D电压继电器&#xff1b;WY-35B4T电压继电器&#xf…

管易云和金蝶云星空接口打通对接实战

管易云和金蝶云星空接口打通对接实战 对接系统&#xff1a;管易云 管易云是金蝶旗下专注提供电商企业管理软件服务的子品牌&#xff0c;先后开发了C-ERP、EC-OMS、EC-WMS、E店管家、BBC、B2B、B2C商城网站建设等产品和服务&#xff0c;涵盖电商业务全流程。 接入系统&#xff1…

Golang 开发实战day09 - package Scope

&#x1f3c6;个人专栏 &#x1f93a; leetcode &#x1f9d7; Leetcode Prime &#x1f3c7; Golang20天教程 &#x1f6b4;‍♂️ Java问题收集园地 &#x1f334; 成长感悟 欢迎大家观看&#xff0c;不执着于追求顶峰&#xff0c;只享受探索过程 Golang 教程09 - package Sc…

2024年上半年WSK-PETS5报名及考试时间公布

4月1日&#xff0c;中国教育考试网发布了2024年上半年全国外语水平考试WSK&#xff08;PETS5&#xff09;的报名及考试通知&#xff0c;为方便关注者&#xff0c;知识人网小编特做全文转载。 国家公派留学人员全国外语水平考试&#xff08;WSK-PETS5&#xff09;成绩作为国家留…