前言

用过spring cache的敌人应该会晓得,Spring Cache默认是不反对在@Cacheable上增加过期工夫的,尽管能够通过配置缓存容器时对立指定。形如

@Beanpublic 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 @Configurationpublic 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@Slf4jpublic 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@Builderpublic 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办法
@Slf4jpublic 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