spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間及自動(dòng)刷新功能
1.前言
用過spring cache的朋友應(yīng)該會(huì)知道,Spring Cache默認(rèn)是不支持在@Cacheable上添加過期時(shí)間的,雖然可以通過配置緩存容器時(shí)統(tǒng)一指定。形如
@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; }
但有時(shí)候我們會(huì)更習(xí)慣通過注解指定過期時(shí)間。今天我們就來聊一下如何擴(kuò)展@Cacheable實(shí)現(xiàn)緩存自動(dòng)過期以及緩存即將到期自動(dòng)刷新
2實(shí)現(xiàn)注解緩存過期前置知識(shí)
SpringCache包含兩個(gè)頂級接口,Cache和CacheManager,通過CacheManager可以去管理一堆Cache。因此我們要擴(kuò)展@Cacheable,就脫離不了對Cache和CacheManager進(jìn)行擴(kuò)展
其次要實(shí)現(xiàn)過期時(shí)間,首先是引入的緩存產(chǎn)品,他本身就要支持過期時(shí)間,比如引入的緩存為ConcurrentHashMap,他原本就是不支持過期時(shí)間,如果要擴(kuò)展,就要非常耗費(fèi)精力實(shí)現(xiàn)
3實(shí)現(xiàn)注解緩存過期
01方法一:通過自定義cacheNames方式
形如下
@Cacheable(cacheNames = "customUser#30", key = "#id")
通過#分隔,#后面部分代表過期時(shí)間(單位為秒)
實(shí)現(xiàn)邏輯步驟為:
1、自定義緩存管理器并繼承RedisCacheManager,同時(shí)重寫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、將默認(rèn)的緩存管理器改成我們自定義的緩存管理器
示例:
@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個(gè)步驟,即可實(shí)現(xiàn)緩存過期
02方法二:通過自定義派生@Cacheable注解
第一種方法的實(shí)現(xiàn)是簡單,但缺點(diǎn)是語義不直觀,因此得做好宣導(dǎo)以及wiki,不然對于新人來說,他可能都不知道cacheName用#分割是代表啥意思
方法二的實(shí)現(xiàn)邏輯步驟如下
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緩存過期時(shí)間以及緩存自動(dòng)刷新時(shí)間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事件驅(qū)動(dòng)亦可 //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初始化完成后,設(shè)置緩存過期時(shí)間,并重新初始化緩存。
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(); } }
注: 為啥要重新初始化緩存,主要是為了一開始默認(rèn)的是沒設(shè)置緩存過期,重新初始化是為了設(shè)置過期時(shí)間。為啥調(diào)用initializeCaches()這個(gè)方法,看下官方描述就知道了
/** * 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); } }
他就是在運(yùn)行的時(shí)候,可以重新初始化緩存
4、將默認(rèn)的緩存管理器改成我們自定義的緩存管理器
@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")); }
以上就是擴(kuò)展緩存過期的實(shí)現(xiàn)主要方式了,接下來我們來聊一下緩存自動(dòng)刷新
4緩存自動(dòng)刷新
一般來說,當(dāng)緩存失效時(shí),請求就會(huì)打到后端的數(shù)據(jù)庫上,此時(shí)可能就會(huì)造成緩存擊穿現(xiàn)象。因此我們在緩存即將過期時(shí)主動(dòng)刷新緩存,提高緩存的命中率,進(jìn)而提高性能。
spring4.3的@Cacheable提供了一個(gè)sync屬性。當(dāng)緩存失效后,為了避免多個(gè)請求打到數(shù)據(jù)庫,系統(tǒng)做了一個(gè)并發(fā)控制優(yōu)化,同時(shí)只有一個(gè)線程會(huì)去數(shù)據(jù)庫取數(shù)據(jù)其它線程會(huì)被阻塞
5緩存即將到期自動(dòng)刷新
1、封裝緩存注解對象CachedInvocation
/** * @description: 標(biāo)記了緩存注解的方法類信息,用于主動(dòng)刷新緩存時(shí)調(diào)用原始方法加載數(shù)據(jù) */ @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、編寫一個(gè)獲取即將到期時(shí)間參數(shù)切面,并進(jìn)行事件發(fā)布調(diào)用對象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、緩存即將到期主動(dòng)刷新緩存方法
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")); }
6總結(jié)
本文主要介紹了如何基于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及緩存即將到期自動(dòng)刷新。
不知道有沒有朋友會(huì)有疑問,為啥@Cacheable不提供一個(gè)ttl屬性,畢竟也不是很難。在我看來,spring更多提供的是一個(gè)通用的規(guī)范和標(biāo)準(zhǔn),如果定義的緩存,本身不支持ttl,你在@Cacheable里面配置ttl就不合適了,有時(shí)候?qū)崿F(xiàn)一個(gè)組件或者框架,考慮的是不是能不能實(shí)現(xiàn),而是有沒有必要實(shí)現(xiàn),更多是一種權(quán)衡和取舍
到此這篇關(guān)于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及自動(dòng)刷新的文章就介紹到這了,更多相關(guān)spring @Cacheable緩存自動(dòng)過期時(shí)間內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼
這篇文章主要介紹了Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11一文搞懂Spring中@Autowired和@Resource的區(qū)別
@Autowired?和?@Resource?都是?Spring/Spring?Boot?項(xiàng)目中,用來進(jìn)行依賴注入的注解。它們都提供了將依賴對象注入到當(dāng)前對象的功能,但二者卻有眾多不同,并且這也是常見的面試題之一,所以我們今天就來盤它2022-08-08全面詳解java代碼重構(gòu)與設(shè)計(jì)模式
這篇文章主要為大家介紹了全面詳解java代碼重構(gòu)與設(shè)計(jì)模式的全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Java中YYYY-MM-dd與yyyy-MM-dd的區(qū)別及跨年問題
YYYY-MM-dd可能會(huì)導(dǎo)致跨年周的日期被歸屬到錯(cuò)誤的年份, yyyy-MM-dd總是表示實(shí)際的日歷年份,無論日期所在的周是否跨年,本文就來介紹一下兩者的區(qū)別,感興趣的可以了解一下2024-01-01Java反射通過Getter方法獲取對象VO的屬性值過程解析
這篇文章主要介紹了Java反射通過Getter方法獲取對象VO的屬性值過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02