网易首页 > 网易号 > 正文 申请入驻

增强Spring改造@CacheEvict,支持缓存批量模糊删除!

0
分享至

系统中集成了Spring cache 使用@CacheEvict进行缓存清除,@CacheEvict可以清除指定的key,同时可以指定allEntries = true清空namespace下的所有元素,现在遇到一个问题使用allEntries = true清空namespace的值只能是常量,但是我现在需要将缓存根据租户的唯一TelnetID进行分离,这就导致allEntries = true不能使用了,否则一旦触发清除缓存,将会导致全部的缓存清空,而我只想清空当前租户的缓存,熟悉redis命令的人都知道,查询和删除都可以做模糊匹配,所以我就想让SpringCache的@CacheEvict也支持模糊匹配清除。

先去搞清楚@CacheEvict是怎么实现缓存清理的,因为之前看过redis的源码,知道@CacheEvict是通过AOP实现的,其中核心的类是CacheAspectSupport,具体的源码分析细节大家可以自行Google,我只简单的分析一下CacheAspectSupport这个类里面重点的几个方法

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 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会根据CacheOperationContext进行缓存清理处理,我们可以看到调用的一个performCacheEvict方法

private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {

Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) { // 如果allEntries为ture就执行这个逻辑
logInvalidating(context, operation, null);
doClear(cache);
}
else {// 否则执行删除指定的key
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
doEvict(cache, key);
}
}
}

看到这里,我们就知道怎么回事了,继续debug跟进去,看到doClear和doEvict最终分别会调用RedisCache中的evict和clear方法

@Override
public void evict(Object key) {
cacheWriter.remove(name, createAndConvertCacheKey(key));

/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#clear()
*/
@Override
public void clear() {
// 支持模糊删除
byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
cacheWriter.clean(name, pattern);
}

我们看到如果allEntries为ture的时候最终执行的是clear()这个方法,其实他也是模糊删除的,只是他的key规则是namespace:: *,看到这里就看到希望了我们只需要想办法在namespace *中插入我们的telnetID就可以变成namespace ::telnetID:*这种格式,也就达到了我们的目的了。

重点需要重写RedisCache的evict方法,新建一个RedisCacheResolver集成RedisCache,重写evict方法

public class RedisCacheResolver extends RedisCache {

private final String name;
private final RedisCacheWriter cacheWriter;
private final ConversionService conversionService;

protected RedisCacheResolver(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
this.name = name;
this.cacheWriter = cacheWriter;
this.conversionService = cacheConfig.getConversionService();
}

/**
*
* @Title: evict
* @Description: 重写删除的方法
* @param @param key
* @throws
*
*/
@Override
public void evict(Object key) {
// 如果key中包含"noCacheable:"关键字的,就不进行缓存处理
if (key.toString().contains(RedisConstant.NO_CACHEABLE)) {
return;
}

if (key instanceof String) {
String keyString = key.toString();
// 后缀删除
if (StringUtils.endsWith(keyString, "*")) {
evictLikeSuffix(keyString);
return;
}
}
// 删除指定的key
super.evict(key);
}

/**
* 后缀匹配匹配
*
* @param key
*/
private void evictLikeSuffix(String key) {
byte[] pattern = this.conversionService.convert(this.createCacheKey(key), byte[].class);
this.cacheWriter.clean(this.name, pattern);
}
}

现在我们需要让我们的这个RedisCacheResolver生效,所以需要将我们的RedisCacheResolver 注入到RedisCacheManager中,因此我们需要定义一个我们自己的RedisCacheManagerResolver集成z

public class RedisCacheManagerResolver extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;

public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public RedisCacheManagerResolver(RedisConnectionFactory redisConnectionFactory, RedisCacheConfiguration cacheConfiguration) {
this(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),cacheConfiguration);
}

/**
* 覆盖父类创建RedisCache,采用自定义的RedisCacheResolver
* @Title: createRedisCache
* @Description: TODO
* @param @param name
* @param @param cacheConfig
* @param @return
* @throws
*
*/
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RedisCacheResolver(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}

@Override
public Map getCacheConfigurations() {
Map configurationMap = new HashMap<>(getCacheNames().size());
getCacheNames().forEach(it -> {
RedisCache cache = RedisCacheResolver.class.cast(lookupCache(it));
configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
});
return Collections.unmodifiableMap(configurationMap);
}
}

至此我们就完成了关键的步骤,最后只需要RedisConfig中管理我们自己的RedisCacheManagerResolver即可。Java技术进阶路线:https://www.yoodb.com/

public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 设置全局过期时间,单位为秒
Duration timeToLive = Duration.ZERO;
timeToLive = Duration.ofSeconds(timeOut);
// RedisCacheManager cacheManager = new RedisCacheManager(
// RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
// this.getRedisCacheConfigurationWithTtl(timeToLive), // 默认策略,未配置的 value 会使用这个
// this.getRedisCacheConfigurationMap() // 指定 value策略
RedisCacheManagerResolver cacheManager =
new RedisCacheManagerResolver(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
this.getRedisCacheConfigurationWithTtl(timeToLive), // 默认策略,未配置的 value 会使用这个
this.getRedisCacheConfigurationMap() // 指定 value策略

// RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();
return cacheManager;
}

经过上面的步骤,我们已经实现了重写evict方法,用来模糊删除缓存了

@CacheEvict(value = "BASE_CACHE, key = "#modelClassName + #telenetId+ '*'", allEntries = false)

只需要用上面这个注解,我们就可以删除telnetID下所有的缓存了。

作者:Crystalqy https://blog.csdn.net/Crystalqy/article/details/110681684

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
肖华将军正被关押,哨兵通知他有人找他,他心里嘀咕:什么事呀?

肖华将军正被关押,哨兵通知他有人找他,他心里嘀咕:什么事呀?

云端书馆
2024-06-08 08:12:13
男性要注意了!这个部位的毛发变白,证明寿命在逐渐减少

男性要注意了!这个部位的毛发变白,证明寿命在逐渐减少

慎独赢
2024-06-07 01:24:33
窦唯近照大变,身材变瘦刮掉胡子显年轻,本人低调和粉丝热聊

窦唯近照大变,身材变瘦刮掉胡子显年轻,本人低调和粉丝热聊

娱圈小愚
2024-06-08 16:28:19
中央确定辽宁省8个中等城市:盘锦第2,阜新第5,朝阳入围

中央确定辽宁省8个中等城市:盘锦第2,阜新第5,朝阳入围

鬼菜生活
2024-06-08 07:55:02
广州番禺一学校将停止办学

广州番禺一学校将停止办学

新快报新闻
2024-06-08 15:46:16
“男孩高考估710,爸爸喊话9月清华见”被嘲,成绩出来,网友沉默

“男孩高考估710,爸爸喊话9月清华见”被嘲,成绩出来,网友沉默

华人星光
2024-06-07 13:06:33
巡回赛23冠!中国一哥创造斯诺克历史,丁俊晖仍是亚洲历史第一人

巡回赛23冠!中国一哥创造斯诺克历史,丁俊晖仍是亚洲历史第一人

世界体坛观察家
2024-06-07 18:31:54
小米SU7至1死3伤的危机公关,可以进教科书了

小米SU7至1死3伤的危机公关,可以进教科书了

桃溪读书
2024-06-08 08:31:24
这个城市,有超百万家民企要找接班人

这个城市,有超百万家民企要找接班人

中国新闻周刊
2024-06-08 08:19:45
亚预赛最令人遗憾的一场3:0——亚洲最大黑马,彻底无缘2026世界杯

亚预赛最令人遗憾的一场3:0——亚洲最大黑马,彻底无缘2026世界杯

凌锐风语
2024-06-08 12:58:30
中国十大顶级奢华酒店

中国十大顶级奢华酒店

心灵和大脑在路上
2024-06-06 05:20:49
打脸民进党!最新民调指有5成7民众支持民意机构改革法案

打脸民进党!最新民调指有5成7民众支持民意机构改革法案

海峡导报社
2024-06-07 16:46:05
一家人搬到广州居住后,把旧房子送给亲兄弟,多年后想要回起冲突

一家人搬到广州居住后,把旧房子送给亲兄弟,多年后想要回起冲突

大苏专栏
2024-06-06 17:23:10
6月8日俄乌:泽连斯基称不会放弃领土,普京称俄将获胜但愿意和谈

6月8日俄乌:泽连斯基称不会放弃领土,普京称俄将获胜但愿意和谈

山河路口
2024-06-08 14:18:49
2014年,印度商人为了庆祝45岁生日,穿4公斤黄金打造的纯金衬衫

2014年,印度商人为了庆祝45岁生日,穿4公斤黄金打造的纯金衬衫

茅舍品史
2024-06-01 23:40:02
大陆芯片迎突破,台积电称绝不会被华为超越,并断言两岸不会生战

大陆芯片迎突破,台积电称绝不会被华为超越,并断言两岸不会生战

贺文萍
2024-06-07 18:40:02
法国努力白费?美联合6国加征关税,中国抛美债,囤1.35亿吨粮食

法国努力白费?美联合6国加征关税,中国抛美债,囤1.35亿吨粮食

元芳
2024-06-08 16:47:43
美415比0通过法案,中国的发展中国家地位,不是美方说取消就取消

美415比0通过法案,中国的发展中国家地位,不是美方说取消就取消

农人李先生呀
2024-06-08 17:02:18
实探搬迁风暴中的周大福深圳工厂:裁员早有征兆,原本五六百人的工厂现在剩下几十个人

实探搬迁风暴中的周大福深圳工厂:裁员早有征兆,原本五六百人的工厂现在剩下几十个人

华夏时报
2024-06-07 11:01:11
为男友摘子宫,剃寸头打舌环,从影后到魔女:46岁的她活成这样……

为男友摘子宫,剃寸头打舌环,从影后到魔女:46岁的她活成这样……

LULU生活家
2024-06-08 18:08:03
2024-06-08 18:42:44
Java精选
Java精选
一场永远也演不完的戏
1551文章数 3855关注度
往期回顾 全部

科技要闻

今年数学到底有多难?大模型:我也不太会

头条要闻

威廉回应凯特近况 查尔斯、卡米拉双双流泪

头条要闻

威廉回应凯特近况 查尔斯、卡米拉双双流泪

体育要闻

她拯救了WNBA,却为何被疯狂针对?

娱乐要闻

汤唯抵达巴黎将担任奥运火炬手

财经要闻

重磅详解:为什么美国经济还没有衰退?

汽车要闻

上汽大通大家9售26.99万起 综合续航1300km+

态度原创

艺术
旅游
家居
本地
教育

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

旅游要闻

广州长隆野生动物世界推出“粽子”盛宴

家居要闻

柔和婉转 让阳光洒满空间

本地新闻

我和我的家乡|踏浪营口,心动不止一夏!

教育要闻

英语故事绘本——画蛇添足,英文讲故事比赛素材,中英双语对照

无障碍浏览 进入关怀版