CacheEvict注解导致Redis的CPU飚高90%报警!

王守钰 2021-07-30 13:07:58

背景:系统使用了Spring-Redis-Data来进行做数据的缓存。

CacheEvict

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
    ……
	boolean allEntries() default false;
    ……
}

在项目中使用了@CacheEvict(allEntries=true)属性的时候,系统出现了Redis的CPU飚高。并且还出现了keys xxx::*的命令。接下来就一探究竟,看看到底发生了什么。

SpringCacheAnnotationParser

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;
}

在parseEvictAnnotation方法中找到builder.setCacheWide(cacheEvict.allEntries()),系统将allEntries的属性设置到了builder中,最终调用CacheEvictOperation op = builder.build();方法,将cacheWide设置给CacheEvictOperation。

CacheInterceptor

public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();

		CacheOperationInvoker aopAllianceInvoker = () -> {
			try {
				return invocation.proceed();
			}
			catch (Throwable ex) {
				throw new CacheOperationInvoker.ThrowableWrapper(ex);
			}
		};

		try {
			return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
		}
		catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}

}

最终系统执行return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());接下来看下父类中的CacheAspectSupport.execute方法

@Nullable
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
	// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
	if (this.initialized) {
		Class<?> targetClass = getTargetClass(target);
		CacheOperationSource cacheOperationSource = getCacheOperationSource();
		if (cacheOperationSource != null) {
			Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
			if (!CollectionUtils.isEmpty(operations)) {
				return execute(invoker, method,
						new CacheOperationContexts(operations, method, args, target, targetClass));
			}
		}
	}

	return invoker.invoke();
}

继续调用execute方法

@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
	// Special handling of synchronized invocation
	if (contexts.isSynchronized()) {
		CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
		if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
			Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
			Cache cache = context.getCaches().iterator().next();
			try {
				return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
			}
			catch (Cache.ValueRetrievalException ex) {
				// The invoker wraps any Throwable in a ThrowableWrapper instance so we
				// can just make sure that one bubbles up the stack.
				throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
			}
		}
		else {
			// No caching required, only call the underlying method
			return invokeOperation(invoker);
		}
	}


	// Process any early evictions
	processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
			CacheOperationExpressionEvaluator.NO_RESULT);

	// Check if we have a cached item matching the conditions
	Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

	// Collect puts from any @Cacheable miss, if no cached item is found
	List<CachePutRequest> cachePutRequests = new LinkedList<>();
	if (cacheHit == null) {
		collectPutRequests(contexts.get(CacheableOperation.class),
				CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
	}

	Object cacheValue;
	Object returnValue;

	if (cacheHit != null && !hasCachePut(contexts)) {
		// If there are no put requests, just use the cache hit
		cacheValue = cacheHit.get();
		returnValue = wrapCacheValue(method, cacheValue);
	}
	else {
		// Invoke the method if we don't have a cache hit
		returnValue = invokeOperation(invoker);
		cacheValue = unwrapReturnValue(returnValue);
	}

	// Collect any explicit @CachePuts
	collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

	// Process any collected put requests, either from @CachePut or a @Cacheable miss
	for (CachePutRequest cachePutRequest : cachePutRequests) {
		cachePutRequest.apply(cacheValue);
	}

	// Process any late evictions
	processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

	return returnValue;
}

最终调用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);
		}
	}
}

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

最终会发现,当cacheWide为true的情况下会走doClear方法,接下来看下doClear方法。

protected void doClear(Cache cache) {
	try {
		cache.clear();
	}
	catch (RuntimeException ex) {
		getErrorHandler().handleCacheClearError(ex, cache);
	}
}

这里面最终调用了cache.clear()方法,这里使用的redisCache,直接进去redisCache

public void clear() {

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

conversionService.convert(createCacheKey("*"), byte[].class);这句话的目的是为了获取前缀信息。比如说fuck::123,那么转换后就变成了fuck::*。接下来看下cacheWriter.clean(name, pattern);方法。

@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";
	});
}

这段代码就是删除的一系列逻辑,导致cpu飚高的代码也就在这块了。byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()).toArray(new byte[0][]);从这段代码里面在细分一下connection.keys(pattern),这句话也就是罪魁祸首,转换完就变成了keys fuck::*,那这就是相当于查出来以fuck开头的所有key,然后再去调用connection.del(keys);删除,然鹅keys *是要付出代价的,也就是CPU飚高的原因。