前言
用过 spring cache 的敌人应该会晓得,Spring Cache 默认是不反对在 @Cacheable 上增加过期工夫的,尽管能够通过配置缓存容器时对立指定。形如
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {RedisCacheManager cacheManager= new RedisCacheManager(redisTemplate);
cacheManager.setDefaultExpiration(60);
Map<String,Long> expiresMap = new HashMap<>();
expiresMap.put("customUser",30L);
cacheManager.setExpires(expiresMap);
return cacheManager;
}
但有时候咱们会更习惯通过注解指定过期工夫。明天咱们就来聊一下如何扩大 @Cacheable 实现缓存主动过期以及缓存行将到期主动刷新
实现注解缓存过期前置常识
SpringCache 蕴含两个顶级接口,Cache 和 CacheManager,通过 CacheManager 能够去治理一堆 Cache。因而咱们要扩大 @Cacheable,就脱离不了对 Cache 和 CacheManager 进行扩大
其次要实现过期工夫,首先是引入的缓存产品,他自身就要反对过期工夫,比方引入的缓存为 ConcurrentHashMap,他本来就是不反对过期工夫,如果要扩大,就要十分消耗精力实现
实现注解缓存过期
办法一、通过自定义 cacheNames 形式
形如下
@Cacheable(cacheNames = "customUser#30", key = "#id")
通过 #分隔,# 前面局部代表过期工夫(单位为秒)
实现逻辑步骤为:
1、自定义缓存管理器并继承 RedisCacheManager,同时重写 createRedisCache 办法
示例:
public class CustomizedRedisCacheManager extends RedisCacheManager {public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
if (array.length > 1) {long ttl = Long.parseLong(array[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}
2、将默认的缓存管理器改成咱们自定义的缓存管理器
示例:
@EnableCaching
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1));
CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory()), defaultCacheConfig);
return redisCacheManager;
}
}
通过如上 2 个步骤,即可实现缓存过期
办法二:通过自定义派生 @Cacheable 注解
第一种办法的实现是简略,但毛病是语义不直观,因而得做好宣导以及 wiki,不然对于新人来说,他可能都不晓得 cacheName 用 #宰割是代表啥意思
办法二的实现逻辑步骤如下
1、自定义注解 LybGeekCacheable
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Cacheable(cacheManager = CacheConstant.CUSTOM_CACHE_MANAGER,keyGenerator = CacheConstant.CUSTOM_CACHE_KEY_GENERATOR)
public @interface LybGeekCacheable {@AliasFor(annotation = Cacheable.class,attribute = "value")
String[] value() default {};
@AliasFor(annotation = Cacheable.class,attribute = "cacheNames")
String[] cacheNames() default {};
@AliasFor(annotation = Cacheable.class,attribute = "key")
String key() default "";
@AliasFor(annotation = Cacheable.class,attribute = "keyGenerator")
String keyGenerator() default "";
@AliasFor(annotation = Cacheable.class,attribute = "cacheResolver")
String cacheResolver() default "";
@AliasFor(annotation = Cacheable.class,attribute = "condition")
String condition() default "";
@AliasFor(annotation = Cacheable.class,attribute = "unless")
String unless() default "";
@AliasFor(annotation = Cacheable.class,attribute = "sync")
boolean sync() default false;
long expiredTimeSecond() default 0;
long preLoadTimeSecond() default 0;}
大部分注解和 @Cacheable 保持一致,新增 expiredTimeSecond 缓存过期工夫以及缓存主动刷新工夫
preLoadTimeSecond
2、自定义缓存管理器并继承 RedisCacheManager 并重写 loadCaches 和 createRedisCache
public class CustomizedRedisCacheManager extends RedisCacheManager implements BeanFactoryAware {
private Map<String, RedisCacheConfiguration> initialCacheConfigurations;
private RedisTemplate cacheRedisTemplate;
private RedisCacheWriter cacheWriter;
private DefaultListableBeanFactory beanFactory;
private RedisCacheConfiguration defaultCacheConfiguration;
protected CachedInvocation cachedInvocation;
public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations,RedisTemplate cacheRedisTemplate) {super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
this.initialCacheConfigurations = initialCacheConfigurations;
this.cacheRedisTemplate = cacheRedisTemplate;
this.cacheWriter = cacheWriter;
this.defaultCacheConfiguration = defaultCacheConfiguration;
// 采纳 spring 事件驱动亦可
//EventBusHelper.register(this);
}
public Map<String, RedisCacheConfiguration> getInitialCacheConfigurations() {return initialCacheConfigurations;}
@Override
protected Collection<RedisCache> loadCaches() {List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> entry : getInitialCacheConfigurations().entrySet()) {caches.add(createRedisCache(entry.getKey(), entry.getValue()));
}
return caches;
}
@Override
public RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {CustomizedRedisCache customizedRedisCache = new CustomizedRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfiguration);
return customizedRedisCache;
}
}
3、在 spring bean 初始化实现后, 设置缓存过期工夫,并从新初始化缓存
Component
@Slf4j
public class CacheExpireTimeInit implements SmartInitializingSingleton, BeanFactoryAware {
private DefaultListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = (DefaultListableBeanFactory)beanFactory;
}
@Override
public void afterSingletonsInstantiated() {Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(Component.class);
if(MapUtil.isNotEmpty(beansWithAnnotation)){for (Object cacheValue : beansWithAnnotation.values()) {ReflectionUtils.doWithMethods(cacheValue.getClass(), method -> {ReflectionUtils.makeAccessible(method);
boolean cacheAnnotationPresent = method.isAnnotationPresent(LybGeekCacheable.class);
if(cacheAnnotationPresent){LybGeekCacheable lybGeekCacheable = method.getAnnotation(LybGeekCacheable.class);
CacheHelper.initExpireTime(lybGeekCacheable);
}
});
}
CacheHelper.initializeCaches();}
}
注: 为啥要从新初始化缓存,次要是为了一开始默认的是没设置缓存过期,从新初始化是为了设置过期工夫。为啥调用 initializeCaches()这个办法,看下官网形容就晓得了
/**
* Initialize the static configuration of caches.
* <p>Triggered on startup through {@link #afterPropertiesSet()};
* can also be called to re-initialize at runtime.
* @since 4.2.2
* @see #loadCaches()
*/
public void initializeCaches() {Collection<? extends Cache> caches = loadCaches();
synchronized (this.cacheMap) {this.cacheNames = Collections.emptySet();
this.cacheMap.clear();
Set<String> cacheNames = new LinkedHashSet<>(caches.size());
for (Cache cache : caches) {String name = cache.getName();
this.cacheMap.put(name, decorateCache(cache));
cacheNames.add(name);
}
this.cacheNames = Collections.unmodifiableSet(cacheNames);
}
}
他就是在运行的时候,能够从新初始化缓存
4、将默认的缓存管理器改成咱们自定义的缓存管理器
@Bean(CacheConstant.CUSTOM_CACHE_MANAGER)
public CacheManager cacheManager(RedisConnectionFactory connectionFactory,RedisTemplate cacheRedisTemplate) {RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1));
Map<String, RedisCacheConfiguration> initialCacheConfiguration = new HashMap<>();
return new CustomizedRedisCacheManager(redisCacheWriter,defaultCacheConfig,initialCacheConfiguration,cacheRedisTemplate);
}
5、测试
@LybGeekCacheable(cacheNames = "customUser", key = "#id",expiredTimeSecond = 30)
public User getUserFromRedisByCustomAnno(String id){System.out.println("get user with id by custom anno:【" + id + "】");
Faker faker = Faker.instance(Locale.CHINA);
return User.builder().id(id).username(faker.name().username()).build();}
@Test
public void testCacheExpiredAndPreFreshByCustom() throws Exception{System.out.println(userService.getUserFromRedisByCustomAnno("1"));
}
以上就是扩大缓存过期的实现次要形式了,接下来咱们来聊一下缓存主动刷新
缓存主动刷新
一般来说,当缓存生效时,申请就会打到后端的数据库上,此时可能就会造成缓存击穿景象。因而咱们在缓存行将过期时被动刷新缓存,进步缓存的命中率,进而进步性能。
spring4.3 的 @Cacheable 提供了一个 sync 属性。当缓存生效后,为了防止多个申请打到数据库, 零碎做了一个并发管制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞
缓存行将到期主动刷新实现步骤
1、封装缓存注解对象 CachedInvocation
/**
* @description: 标记了缓存注解的办法类信息, 用于被动刷新缓存时调用原始办法加载数据
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public final class CachedInvocation {
private CacheMetaData metaData;
private Object targetBean;
private Method targetMethod;
private Object[] arguments;
public Object invoke()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {final MethodInvoker invoker = new MethodInvoker();
invoker.setTargetObject(this.getTargetBean());
invoker.setArguments(this.getArguments());
invoker.setTargetMethod(this.getTargetMethod().getName());
invoker.prepare();
return invoker.invoke();}
}
2、编写一个获取行将到期工夫参数切面,并进行事件公布调用对象 CachedInvocation
@Component
@Aspect
@Slf4j
@Order(2)
public class LybGeekCacheablePreLoadAspect {
@Autowired
private ApplicationContext applicationContext;
@SneakyThrows
@Around(value = "@annotation(lybGeekCacheable)")
public Object around(ProceedingJoinPoint proceedingJoinPoint,LybGeekCacheable lybGeekCacheable){buildCachedInvocationAndPushlish(proceedingJoinPoint,lybGeekCacheable);
Object result = proceedingJoinPoint.proceed();
return result;
}
private void buildCachedInvocationAndPushlish(ProceedingJoinPoint proceedingJoinPoint,LybGeekCacheable lybGeekCacheable){Method method = this.getSpecificmethod(proceedingJoinPoint);
String[] cacheNames = getCacheNames(lybGeekCacheable);
Object targetBean = proceedingJoinPoint.getTarget();
Object[] arguments = proceedingJoinPoint.getArgs();
KeyGenerator keyGenerator = SpringUtil.getBean(CacheConstant.CUSTOM_CACHE_KEY_GENERATOR,KeyGenerator.class);
Object key = keyGenerator.generate(targetBean, method, arguments);
CachedInvocation cachedInvocation = CachedInvocation.builder()
.arguments(arguments)
.targetBean(targetBean)
.targetMethod(method)
.metaData(CacheMetaData.builder()
.cacheNames(cacheNames)
.key(key)
.expiredTimeSecond(lybGeekCacheable.expiredTimeSecond())
.preLoadTimeSecond(lybGeekCacheable.preLoadTimeSecond())
.build())
.build();
// EventBusHelper.post(cachedInvocation);
applicationContext.publishEvent(cachedInvocation);
}
3、自定义缓存管理器,接管 CachedInvocation
示例
public class CustomizedRedisCacheManager extends RedisCacheManager implements BeanFactoryAware {
//@Subscribe
@EventListener
private void doWithCachedInvocationEvent(CachedInvocation cachedInvocation){this.cachedInvocation = cachedInvocation;}
4、自定义 cache 并重写 get 办法
@Slf4j
public class CustomizedRedisCache extends RedisCache {private ReentrantLock lock = new ReentrantLock();
public CustomizedRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {super(name, cacheWriter,cacheConfig);
}
@Override
@Nullable
public ValueWrapper get(Object key) {ValueWrapper valueWrapper = super.get(key);
CachedInvocation cachedInvocation = CacheHelper.getCacheManager().getCachedInvocation();
long preLoadTimeSecond = cachedInvocation.getMetaData().getPreLoadTimeSecond();
if(ObjectUtil.isNotEmpty(valueWrapper) && preLoadTimeSecond > 0){String cacheKey = createCacheKey(key);
RedisTemplate cacheRedisTemplate = CacheHelper.getCacheManager().getCacheRedisTemplate();
Long ttl = cacheRedisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
if(ObjectUtil.isNotEmpty(ttl) && ttl <= preLoadTimeSecond){log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",cacheKey,ttl,preLoadTimeSecond);
ThreadPoolUtils.execute(()->{lock.lock();
try{CacheHelper.refreshCache(super.getName());
}catch (Exception e){log.error("{}",e.getMessage(),e);
}finally {lock.unlock();
}
});
}
}
return valueWrapper;
}
}
5、缓存行将到期被动刷新缓存办法
public static void refreshCache(String cacheName){boolean isMatchCacheName = isMatchCacheName(cacheName);
if(isMatchCacheName){CachedInvocation cachedInvocation = getCacheManager().getCachedInvocation();
boolean invocationSuccess;
Object computed = null;
try {computed = cachedInvocation.invoke();
invocationSuccess = true;
} catch (Exception ex) {
invocationSuccess = false;
log.error(">>>>>>>>>>>>>>>>> refresh cache fail",ex.getMessage(),ex);
}
if (invocationSuccess) {Cache cache = getCacheManager().getCache(cacheName);
if(ObjectUtil.isNotEmpty(cache)){Object cacheKey = cachedInvocation.getMetaData().getKey();
cache.put(cacheKey, computed);
log.info(">>>>>>>>>>>>>>>>>>>> refresh cache with cacheName-->【{}】,key-->【{}】finished !",cacheName,cacheKey);
}
}
}
}
6、测试
@LybGeekCacheable(cacheNames = "customUserName", key = "#username",expiredTimeSecond = 20,preLoadTimeSecond = 15)
public User getUserFromRedisByCustomAnnoWithUserName(String username){System.out.println("get user with username by custom anno:【" + username + "】");
Faker faker = Faker.instance(Locale.CHINA);
return User.builder().id(faker.idNumber().valid()).username(username).build();}
@Test
public void testCacheExpiredAndPreFreshByCustomWithUserName() throws Exception{System.out.println(userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
TimeUnit.SECONDS.sleep(5);
System.out.println("sleep 5 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
TimeUnit.SECONDS.sleep(10);
System.out.println("sleep 10 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
TimeUnit.SECONDS.sleep(5);
System.out.println("sleep 5 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
}
总结
本文次要介绍了如何基于 spring @Cacheable 扩大实现缓存主动过期工夫以及缓存行将到期主动刷新。
不晓得有没有敌人会有疑难,为啥 @Cacheable 不提供一个 ttl 属性,毕竟也不是很难。在我看来,spring 更多提供的是一个通用的标准和规范,如果定义的缓存,自身不反对 ttl,你在 @Cacheable 外面配置 ttl 就不适合了,有时候实现一个组件或者框架,思考的是不是能不能实现,而是有没有必要实现,更多是一种衡量和取舍
最初本文的实现的性能,min.jiang 博主他也有实现了一版,博文链接我贴在下方,感兴趣的敌人,能够查看一下
https://www.cnblogs.com/ASPNET2008/p/6511500.html
demo 链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-cache