Spring声明式基于注解的缓存(3-精进篇)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 目录一、序言二、如何自定义过期时间三、解决方案1、CacheManger的作用2、CacheResolver的作用四、代码示例1、自定义缓存相关注解(1) @TTLCacheable注解(2) @TTLCachePut注解2、自定义CacheResolver3、自定义CacheManager4、开启声明式缓存配置类五、测试用例1、 测试服务类2、 带过期时间的缓存操作3、 带过期时间的更新操作六、结语

目录

一、序言

二、如何自定义过期时间

三、解决方案

1、CacheManger的作用

2、CacheResolver的作用

四、代码示例

1、自定义缓存相关注解

(1) @TTLCacheable注解

(2) @TTLCachePut注解

2、自定义CacheResolver

3、自定义CacheManager

4、开启声明式缓存配置类

五、测试用例

1、 测试服务类

2、 带过期时间的缓存操作

3、 带过期时间的更新操作

六、结语

一、序言


在上一节 Spring声明式基于注解的缓存(2-实践篇)中给出了一些声明式基于注解的缓存实际使用案例。在这一节中,我们会通过自定义CacheResolverRedisCacheManager还有Cache相关注解来实现带过期时间的缓存方案。

二、如何自定义过期时间


在实例化RedisCacheManager时,我们可以指定key过期的entryTtl属性,如下:

@EnableCaching
@Configuration
public class RedisCacheConfig {
  private static final String KEY_SEPERATOR = ":";
  /**
   * 自定义CacheManager,具体配置参考{@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration}
   * @param redisConnectionFactory 自动配置会注入
   * @return
   */
  @Bean(name = "redisCacheManager")
  public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisSerializer<String> keySerializer = new StringRedisSerializer();
    RedisSerializer<Object> valueSerializer = new GenericJackson2JsonRedisSerializer();
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
      .serializeKeysWith(SerializationPair.fromSerializer(keySerializer))
      .serializeValuesWith(SerializationPair.fromSerializer(valueSerializer))
      .computePrefixWith(key -> key.concat(KEY_SEPERATOR))
      .entryTtl(Duration.ofSeconds(1));
    return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfig).build();
  }
}


备注:这种方式有一个很明显的缺点,所有key都会共享配置,比如这里会设置所有key的过期时间都为1秒。

三、解决方案



1、CacheManger的作用

在Spring声明式基于注解的缓存(1-理论篇)中我们了解到CacheManager主要有两个方法。一个是根据指定缓存名获取Cache实例,还有一个是获取所有缓存名称的。

public interface CacheManager {
  /**
   * Get the cache associated with the given name.
   * <p>Note that the cache may be lazily created at runtime if the
   * native provider supports it.
   * @param name the cache identifier (must not be {@code null})
   * @return the associated cache, or {@code null} if such a cache
   * does not exist or could be not created
   */
  @Nullable
  Cache getCache(String name);
  /**
   * Get a collection of the cache names known by this manager.
   * @return the names of all caches known by the cache manager
   */
  Collection<String> getCacheNames();
}

让我们看看RedisCacheManagerCacheManager的实现,实际上中间还继承了两个抽象类,如下:5faf3120103147afad9eb4bd5408b303.png其中getCache()方法的实现逻辑主要在AbstractCacheManager中,如下:

@Override
@Nullable
public Cache getCache(String name) {
  // Quick check for existing cache...
  Cache cache = this.cacheMap.get(name);
  if (cache != null) {
    return cache;
  }
  // The provider may support on-demand cache creation...
  Cache missingCache = getMissingCache(name);
  if (missingCache != null) {
    // Fully synchronize now for missing cache registration
    synchronized (this.cacheMap) {
      cache = this.cacheMap.get(name);
      if (cache == null) {
        cache = decorateCache(missingCache);
        this.cacheMap.put(name, cache);
        updateCacheNames(name);
      }
    }
  }
  return cache;
}


有经验的同学在看到decorateCache方法时绝对会眼前一亮,见名知意,这个方法就是用来装饰根据指定缓存名称获取到的缓存实例的,这个方法也正是交给子类来实现。(Ps:这里用到的是模板方法模式)


而decorateCache方法实际上是由AbstractTransactionSupportingCacheManager来实现的,该抽象类在装饰缓存时会附加事务的支持,比如在事务提交之后缓存,如下:

@Override
protected Cache decorateCache(Cache cache) {
  return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
}


2、CacheResolver的作用


@FunctionalInterface
public interface CacheResolver {
  /**
   * Return the cache(s) to use for the specified invocation.
   * @param context the context of the particular invocation
   * @return the cache(s) to use (never {@code null})
   * @throws IllegalStateException if cache resolution failed
   */
  Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}

该接口有个抽象类实现AbstractCacheResolver,对resolveCaches的实现如下:

@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
  Collection<String> cacheNames = getCacheNames(context);
  if (cacheNames == null) {
    return Collections.emptyList();
  }
  Collection<Cache> result = new ArrayList<>(cacheNames.size());
  for (String cacheName : cacheNames) {
    Cache cache = getCacheManager().getCache(cacheName);
    if (cache == null) {
      throw new IllegalArgumentException("Cannot find cache named '" +
          cacheName + "' for " + context.getOperation());
    }
    result.add(cache);
  }
  return result;
}


四、代码示例


1、自定义缓存相关注解

Spring中缓存相关注解同样可以作为元注解,这里我们自定义了@TTLCacheable@TTLCachePut两个注解,并且指定了名为ttlCacheResolver的缓存解析器实例。

(1) @TTLCacheable注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(cacheResolver = "ttlCacheResolver")
public @interface TTLCacheable {
  @AliasFor(annotation = Cacheable.class)
  String[] value() default {};
  @AliasFor(annotation = Cacheable.class)
  String[] cacheNames() default {};
  @AliasFor(annotation = Cacheable.class)
  String key() default "";
  @AliasFor(annotation = Cacheable.class)
  String keyGenerator() default "";
  @AliasFor(annotation = Cacheable.class)
  String cacheManager() default "";
  @AliasFor(annotation = Cacheable.class)
  String condition() default "";
  @AliasFor(annotation = Cacheable.class)
  String unless() default "";
  @AliasFor(annotation = Cacheable.class)
  boolean sync() default false;
  /**
   * time to live
   */
  long ttl() default 0L;
  /**
   * 时间单位
   * @return
   */
  TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}


(2) @TTLCachePut注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@CachePut(cacheResolver = "ttlCacheResolver")
public @interface TTLCachePut {
  @AliasFor(annotation = CachePut.class)
  String[] value() default {};
  @AliasFor(annotation = CachePut.class)
  String[] cacheNames() default {};
  @AliasFor(annotation = CachePut.class)
  String key() default "";
  @AliasFor(annotation = CachePut.class)
  String keyGenerator() default "";
  @AliasFor(annotation = CachePut.class)
  String cacheManager() default "";
  @AliasFor(annotation = CachePut.class)
  String condition() default "";
  @AliasFor(annotation = CachePut.class)
  String unless() default "";
  /**
   * time to live
   */
  long ttl() default 0L;
  /**
   * 时间单位
   * @return
   */
  TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

2、自定义CacheResolver

这里我们直接继承SimpleCacheResolver,在解析缓存时根据注解中的过期时间配置动态给CacheManager传值,然后再调用AbstractCacheResolverresolveCaches方法进行实际的缓存解析操作。

public class TTLCacheResolver extends SimpleCacheResolver {
  public TTLCacheResolver() {
  }
  public TTLCacheResolver(CacheManager cacheManager) {
    super(cacheManager);
  }
  @Override
  public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
    TTLCacheable ttlCacheable = context.getMethod().getAnnotation(TTLCacheable.class);
    TTLCachePut ttlCachePut = context.getMethod().getAnnotation(TTLCachePut.class);
    CacheManager cacheManager = super.getCacheManager();
    if (cacheManager instanceof TTLRedisCacheManager) {
      TTLRedisCacheManager ttlRedisCacheManager = (TTLRedisCacheManager) cacheManager;
      Optional.ofNullable(ttlCacheable).ifPresent(cacheable -> {
        ttlRedisCacheManager.setTtl(cacheable.ttl());
        ttlRedisCacheManager.setTimeUnit(cacheable.timeUnit());
      });
      Optional.ofNullable(ttlCachePut).ifPresent(cachePut -> {
        ttlRedisCacheManager.setTtl(cachePut.ttl());
        ttlRedisCacheManager.setTimeUnit(cachePut.timeUnit());
      });
    }
    return super.resolveCaches(context);
  }
}

3、自定义CacheManager

这里我们直接重写了RedisCacheManager

public class TTLRedisCacheManager extends RedisCacheManager {
  /**
   * 过期时间,具体见{@link com.netease.cache.distrubuted.redis.integration.custom.annotation.TTLCacheable}
   * 中的ttl说明
   */
  private long ttl;
  /**
   * 时间单位
   */
  private TimeUnit timeUnit;
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
    super(cacheWriter, defaultCacheConfiguration);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    String... initialCacheNames) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    boolean allowInFlightCacheCreation, String... initialCacheNames) {
    super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
  }
  public void setTtl(long ttl) {
    this.ttl = ttl;
  }
  public void setTimeUnit(TimeUnit timeUnit) {
    this.timeUnit = timeUnit;
  }
  /**
   * CacheResolver调用CacheManager的getCache方法后会调用该方法进行装饰,这里我们可以给Cache加上过期时间
   * @param cache
   * @return
   */
  @Override
  protected Cache decorateCache(Cache cache) {
    RedisCache redisCache = (RedisCache) cache;
    RedisCacheConfiguration config = redisCache.getCacheConfiguration().entryTtl(resolveExpiryTime(ttl, timeUnit));
    return super.decorateCache(super.createRedisCache(redisCache.getName(), config));
  }
  private Duration resolveExpiryTime(long timeToLive, TimeUnit timeUnit) {
    return Duration.ofMillis(timeUnit.toMillis(timeToLive));
  }
}


4、开启声明式缓存配置类

@EnableCaching
@Configuration
public class TTLRedisCacheConfig {
  private static final String KEY_SEPERATOR = ":";
  @Bean(name = "ttlRedisCacheManager")
  public TTLRedisCacheManager ttlRedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisSerializer<String> keySerializer = new StringRedisSerializer();
    RedisSerializer<Object> valueSerializer = new GenericJackson2JsonRedisSerializer();
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();
    cacheConfig = cacheConfig.serializeKeysWith(SerializationPair.fromSerializer(keySerializer));
    cacheConfig = cacheConfig.serializeValuesWith(SerializationPair.fromSerializer(valueSerializer));
    cacheConfig = cacheConfig.computePrefixWith(key -> "ttl" + KEY_SEPERATOR + key + KEY_SEPERATOR);
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    return new TTLRedisCacheManager(redisCacheWriter, cacheConfig);
  }
  @Bean(name = "ttlCacheResolver")
  public TTLCacheResolver ttlCacheResolver(TTLRedisCacheManager ttlRedisCacheManager) {
    return new TTLCacheResolver(ttlRedisCacheManager);
  }
}

备注:这里我们用自定义的TTLCacheManagerTTLCacheResolver初始化配置即可,缓存key的名称指定了前缀ttl:

五、测试用例


1、 测试服务类

@Service
public class TTLSpringCacheService {
  @TTLCacheable(cacheNames = "student-cache", key = "#stuNo", ttl = 200, timeUnit = TimeUnit.SECONDS)
  public StudentDO getStudentWithTTL(int stuNo, String stuName) {
    StudentDO student = new StudentDO(stuNo, stuName);
    System.out.println("模拟从数据库中读取...");
    return student;
  }
  @TTLCachePut(cacheNames = "student-cache", key = "#student.stuNo", ttl = 1, timeUnit = TimeUnit.MINUTES)
  public StudentDO updateStudent(StudentDO student) {
    System.out.println("数据库进行了更新,检查缓存是否一致");
    return student;
  }
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class TTLSpringCacheIntegrationTest {
  @Autowired
  private TTLSpringCacheService ttlSpringCacheService;
  @Test
  public void getStudentWithTTLTest() {
    StudentDO studentDO = ttlSpringCacheService.getStudentWithTTL(1, "Nick");
    System.out.println(studentDO);
  }
  @Test
  public void updateStudentWithTTLTest() {
    StudentDO studentDO = ttlSpringCacheService.updateStudent(new StudentDO(1, "Licky"));
    System.out.println(studentDO);
  }
}


2、 带过期时间的缓存操作

调用getStudentWithTTLTest方法,这里我们指定了缓存的过期时间为200秒,查看Redis中key对应的值,如下:bd07cc85db66451285072eb177c3180b.png

3、 带过期时间的更新操作

调用updateStudentWithTTLTest方法,更新时我们指定了缓存的过期时间为1分钟,查看Redis中key对应的值,如下:4eac4ee422e244dcb99bd87815779a70.png

六、结语


Spring基于注解的缓存抽象就到这里啦,Spring源码还是比较清晰易懂的,见名知意。除了自定义方案,阿里爸爸也有一个缓存抽象解决方案,叫做 jetcache。


它是Spring缓存抽象的增强版,提供的特性有带过期时间的缓存、二级缓存、自动缓存更新、代码操作缓存实例等,有兴趣的同学可以去体验一下。


相关文章
|
6天前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
127 4
|
15天前
|
存储 缓存 Java
Spring中@Cacheable、@CacheEvict以及其他缓存相关注解的实用介绍
缓存是提升应用性能的重要技术,Spring框架提供了丰富的缓存注解,如`@Cacheable`、`@CacheEvict`等,帮助开发者简化缓存管理。本文介绍了如何在Spring中配置缓存管理器,使用缓存注解优化数据访问,并探讨了缓存的最佳实践,以提升系统响应速度与可扩展性。
123 0
Spring中@Cacheable、@CacheEvict以及其他缓存相关注解的实用介绍
|
2月前
|
缓存 Java 数据库连接
怎么使用注解开启二级缓存,注解应该放在那里?
在 MyBatis 中,使用 `@CacheNamespace` 注解可开启二级缓存,该注解应添加在 Mapper 接口上。通过配置 `eviction`、`flushInterval`、`size` 等参数,可以控制缓存行为。此外,实体类需实现 `Serializable` 接口以确保缓存正常工作。
95 1
|
2月前
|
存储 缓存 NoSQL
Spring Cache缓存框架
Spring Cache是Spring体系下的标准化缓存框架,支持多种缓存(如Redis、EhCache、Caffeine),可独立或组合使用。其优势包括平滑迁移、注解与编程两种使用方式,以及高度解耦和灵活管理。通过动态代理实现缓存操作,适用于不同业务场景。
295 0
|
4月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
175 32
|
3月前
|
Java 测试技术 数据库
说一说 SpringBoot 整合 Junit5 常用注解
我是小假 期待与你的下一次相遇 ~
|
6月前
|
Java Spring
Spring Boot的核心注解是哪个?他由哪几个注解组成的?
Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 : ● @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能; ● @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项 ● @ComponentScan:Spring组件扫描
|
5月前
|
人工智能 缓存 自然语言处理
保姆级Spring AI 注解式开发教程,你肯定想不到还能这么玩!
这是一份详尽的 Spring AI 注解式开发教程,涵盖从环境配置到高级功能的全流程。Spring AI 是 Spring 框架中的一个模块,支持 NLP、CV 等 AI 任务。通过注解(如自定义 `@AiPrompt`)与 AOP 切面技术,简化了 AI 服务集成,实现业务逻辑与 AI 基础设施解耦。教程包含创建项目、配置文件、流式响应处理、缓存优化及多任务并行执行等内容,助你快速构建高效、可维护的 AI 应用。
|
6月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
516 0
|
Java API Spring
Spring容器如何使用一个注解来指定一个类型为配置类型
Spring容器如何使用一个注解来指定一个类型为配置类型
175 0