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


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
27天前
|
XML Java 数据格式
SpringBoot入门(8) - 开发中还有哪些常用注解
SpringBoot入门(8) - 开发中还有哪些常用注解
48 0
|
1月前
|
XML JSON Java
SpringBoot必须掌握的常用注解!
SpringBoot必须掌握的常用注解!
58 4
SpringBoot必须掌握的常用注解!
|
12天前
|
前端开发 Java Spring
探索Spring MVC:@Controller注解的全面解析
在Spring MVC框架中,`@Controller`注解是构建Web应用程序的基石之一。它不仅简化了控制器的定义,还提供了一种优雅的方式来处理HTTP请求。本文将全面解析`@Controller`注解,包括其定义、用法、以及在Spring MVC中的作用。
29 2
|
1月前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
67 5
|
1月前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
43 1
|
1月前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
18 0
|
7月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
1月前
|
存储 缓存 Java
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
133 2
|
7月前
|
缓存 Java 数据库
优化您的Spring应用程序:缓存注解的精要指南
优化您的Spring应用程序:缓存注解的精要指南
112 0
|
7月前
|
缓存 NoSQL Java
Spring Cache之本地缓存注解@Cacheable,@CachePut,@CacheEvict使用
SpringCache不支持灵活的缓存时间和集群,适合数据量小的单机服务或对一致性要求不高的场景。`@EnableCaching`启用缓存。`@Cacheable`用于缓存方法返回值,`value`指定缓存名称,`key`定义缓存键,可按SpEL编写,`unless`决定是否不缓存空值。当在类上使用时,类内所有方法都支持缓存。`@CachePut`每次执行方法后都会更新缓存,而`@CacheEvict`用于清除缓存,支持按键清除或全部清除。Spring Cache结合Redis可支持集群环境。
411 6