聊聊如何基於spring @Cacheable擴充套件實現快取自動過期時間以及即將到期自動重新整理
前言
用過spring cache的朋友應該會知道,Spring Cache預設是不支援在@Cacheable上新增過期時間的,雖然可以通過配置快取容器時統一指定。形如
java
@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方式
形如下
java
@Cacheable(cacheNames = "customUser#30", key = "#id")
通過#分隔,#後面部分代表過期時間(單位為秒)
實現邏輯步驟為:
1、自定義快取管理器並繼承RedisCacheManager,同時重寫createRedisCache方法
示例:
```java 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、將預設的快取管理器改成我們自定義的快取管理器
示例:
```java @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
```java @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
```java 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初始化完成後,設定快取過期時間,並重新初始化快取
```java 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()這個方法,看下官方描述就知道了
```java /* * Initialize the static configuration of caches. *
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、將預設的快取管理器改成我們自定義的快取管理器
```java
@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、測試
```java @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();
}
```
```java @Test public void testCacheExpiredAndPreFreshByCustom() throws Exception{ System.out.println(userService.getUserFromRedisByCustomAnno("1"));
}
``` 以上就是擴充套件快取過期的實現主要方式了,接下來我們來聊一下快取自動重新整理
快取自動重新整理
一般來說,當快取失效時,請求就會打到後端的資料庫上,此時可能就會造成快取擊穿現象。因此我們在快取即將過期時主動重新整理快取,提高快取的命中率,進而提高效能。
spring4.3的@Cacheable提供了一個sync屬性。當快取失效後,為了避免多個請求打到資料庫,系統做了一個併發控制優化,同時只有一個執行緒會去資料庫取資料其它執行緒會被阻塞
快取即將到期自動重新整理實現步驟
1、封裝快取註解物件CachedInvocation
```java /* * @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
```java @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
示例 ```java public class CustomizedRedisCacheManager extends RedisCacheManager implements BeanFactoryAware {
//@Subscribe
@EventListener
private void doWithCachedInvocationEvent(CachedInvocation cachedInvocation){
this.cachedInvocation = cachedInvocation;
}
```
4、自定義cache並重寫get方法
```java @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、快取即將到期主動重新整理快取方法
```java 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、測試
```java @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();
}
```
```java @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 博主他也有實現了一版,博文連結我貼在下方,感興趣的朋友,可以檢視一下
http://www.cnblogs.com/ASPNET2008/p/6511500.html
demo連結
http://github.com/lyb-geek/springboot-learning/tree/master/springboot-cache
- 聊聊如何基於spring @Cacheable擴充套件實現快取自動過期時間以及即將到期自動重新整理
- 聊聊基於docker部署的mysql如何進行資料恢復
- 聊聊如何驗證線上的版本是符合預期的版本
- 聊聊如何讓你的業務程式碼具有可擴充套件性
- spring事務失效的幾種場景以及原因
- 聊聊自定義SPI如何與sentinel整合實現熔斷限流
- 聊聊如何實現一個支援鍵值對的SPI
- 聊聊springboot專案如何實現自定義actuator端點
- 聊聊使用lombok @Builder踩到的坑
- 笑話
- 聊聊如何自定義實現maven外掛
- 聊聊在idea dubug模式下,動態代理類出現的null現象
- 聊聊基於jdk實現的spi如何與spring整合實現依賴注入
- 笑話
- 聊聊springcloud專案同時存在多個註冊中心客戶端採坑記
- feign請求返回值反序列LocalDateTime異常記錄
- 笑話
- 笑話
- 笑話
- 笑話