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