当前位置: 首页 > news >正文

Spring声明式基于注解的缓存(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秒。



三、解决方案

我们的需求是每个key都能自定义过期时间,如果能从原生@Cacheable等注解中获取过期时间,然后再动态注入到RedisCacheConfiguration中,其实就能实现我们想要的需求。

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的实现,实际上中间还继承了两个抽象类,如下:
在这里插入图片描述
其中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);
}

其实我们也可以重写decorateCache()方法,然后让Cache实例支持带过期时间,我们需要做的就是从缓存相关注解中拿到过期时间配置,再重新对缓存进行实例化,而这一步需要通过CacheResolver来实现。

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;
}

这里可以看到会通过CacheManager根据缓存名去获取缓存实例,我们可以通过给CacheManager实例传递相关过期时间的配置,在CacheManager创建Cache实例时根据时间配置进行初始化。



四、代码示例

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;
}

备注:这两个注解我都加了ttltimeUnit两个属性,当ttl为0时代表key永久不过期。

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对应的值,如下:
在这里插入图片描述

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

调用updateStudentWithTTLTest方法,更新时我们指定了缓存的过期时间为1分钟,查看Redis中key对应的值,如下:
在这里插入图片描述



六、结语

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

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

在这里插入图片描述

相关文章:

  • 怎么入门网络安全,学这两类证书就够了NISP或CISP
  • 探究MYSQL之索引
  • Linux环境搭建与登陆
  • WEB自动化测试(5)—— Cypress-元素交互
  • 图片速览 Deep Clustering for Unsupervised Learning of Visual Features
  • HCIA网络基础9-VRP文件系统管理
  • springboot整合mycat实现读写分离
  • iOS 16 SwiftUI 4.0 列表(List)项分隔线变短的原因及解决
  • 创邻科技入选Gartner全球《图数据库管理系统市场指南》代表厂商
  • OpenHarmony如何控制屏幕亮度
  • jenkins 2.346.1 从git拉取后自动构建部署springboot maven项目
  • vue搭建项目、创建登录页面和后台交互之引入axios
  • MySQL-存储过程-函数-触发器-游标
  • JVM入门教程
  • python毕业设计项目源码选题(20)教室图书馆座位预约系统毕业设计毕设作品开题报告开题答辩PPT
  • @angular/forms 源码解析之双向绑定
  • avalon2.2的VM生成过程
  • CSS魔法堂:Absolute Positioning就这个样
  • css属性的继承、初识值、计算值、当前值、应用值
  • Django 博客开发教程 8 - 博客文章详情页
  • echarts花样作死的坑
  • Laravel 中的一个后期静态绑定
  • tweak 支持第三方库
  • webgl (原生)基础入门指南【一】
  • 从PHP迁移至Golang - 基础篇
  • 分类模型——Logistics Regression
  • 记录:CentOS7.2配置LNMP环境记录
  • 入口文件开始,分析Vue源码实现
  • 一文看透浏览器架构
  • Linux权限管理(week1_day5)--技术流ken
  • 国内唯一,阿里云入选全球区块链云服务报告,领先AWS、Google ...
  • 教程:使用iPhone相机和openCV来完成3D重建(第一部分) ...
  • $(function(){})与(function($){....})(jQuery)的区别
  • $.each()与$(selector).each()
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (草履虫都可以看懂的)PyQt子窗口向主窗口传递参数,主窗口接收子窗口信号、参数。
  • (多级缓存)缓存同步
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (转)Google的Objective-C编码规范
  • (转)jdk与jre的区别
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • .NET 常见的偏门问题
  • .net 后台导出excel ,word
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .sdf和.msp文件读取
  • .考试倒计时43天!来提分啦!
  • @RequestMapping-占位符映射
  • @RequestParam @RequestBody @PathVariable 等参数绑定注解详解
  • [20180224]expdp query 写法问题.txt
  • [bbk5179]第66集 第7章 - 数据库的维护 03
  • [BT]BUUCTF刷题第4天(3.22)
  • [BZOJ 4034][HAOI2015]T2 [树链剖分]
  • [C#]OpenCvSharp使用帧差法或者三帧差法检测移动物体
  • [C++] 统计程序耗时