深入浅出spring原理及实战「开发实战系列」spring-cache扩展自定义(注解失效时间+主动刷新缓存)(代码片段)

洛神灬殇 洛神灬殇     2022-12-16     557

关键词:

缓存失效时间支持在方法的注解上指定

Spring Cache默认是不支持在@Cacheable上添加过期时间的,可以在配置缓存容器时统一指定:

@Bean
public CacheManager cacheManager(
        @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) 
    CustomizedRedisCacheManager cacheManager= new CustomizedRedisCacheManager(redisTemplate);
    cacheManager.setDefaultExpiration(60);
    Map<String,Long> expiresMap=new HashMap<>();
    expiresMap.put("Product",5L);
    cacheManager.setExpires(expiresMap);
    return cacheManager;

想这样配置过期时间,焦点在value的格式上Product#5#2,详情下面会详细说明。

@Cacheable(value = "Product#5#2",key ="#id")

上面两种各有利弊,并不是说哪一种一定要比另外一种强,根据自己项目的实际情况选择。

在缓存即将过期时主动刷新缓存

一般缓存失效后,会有一些请求会打到后端的数据库上,这段时间的访问性能肯定是比有缓存的情况要差很多。所以期望在缓存即将过期的某一时间点后台主动去更新缓存以确保前端请求的缓存命中率,示意图如下:

Spring 4.3提供了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞

背景

Spring Cache +Redis为前提来实现上面两个需求,其它类型的缓存原理应该是相同的。

本文内容未在生产环境验证过,也许有不妥的地方,请多多指出。

扩展RedisCacheManager

CustomizedRedisCacheManager

继承自RedisCacheManager,定义两个辅助性的属性:

    /**
     * 缓存参数的分隔符
     * 数组元素0=缓存的名称
     * 数组元素1=缓存过期时间TTL
     * 数组元素2=缓存在多少秒开始主动失效来强制刷新
     */
    private String separator = "#";

    /**
     * 缓存主动在失效前强制刷新缓存的时间
     * 单位:秒
     */
    private long preloadSecondTime=0;

注解配置失效时间简单的方法就是在容器名称上动动手脚,通过解析特定格式的名称来变向实现失效时间的获取。

  • 比如第一个#后面的5可以定义为失效时间,第二个#后面的2是刷新缓存的时间,只需要重写getCache:

  • 解析配置的value值,分别计算出真正的缓存名称,失效时间以及缓存刷新的时间。

调用构造函数返回缓存对象

@Override
public Cache getCache(String name) 
    String[] cacheParams=name.split(this.getSeparator());
    String cacheName = cacheParams[0];
    if(StringUtils.isBlank(cacheName))
        return null;
    
    Long expirationSecondTime = this.computeExpiration(cacheName);
    if(cacheParams.length>1) 
        expirationSecondTime=Long.parseLong(cacheParams[1]);
        this.setDefaultExpiration(expirationSecondTime);
    
    if(cacheParams.length>2) 
        this.setPreloadSecondTime(Long.parseLong(cacheParams[2]));
    
    Cache cache = super.getCache(cacheName);
    if(null==cache)
        return cache;
    
    logger.info("expirationSecondTime:"+expirationSecondTime);
    CustomizedRedisCache redisCache= new CustomizedRedisCache(
            cacheName,
            (this.isUsePrefix() ? this.getCachePrefix().
			 prefix(cacheName) : null),
            this.getRedisOperations(),
            expirationSecondTime,
            preloadSecondTime);
    return redisCache;

CustomizedRedisCache

主要是实现缓存即将过期时能够主动触发缓存更新,核心是下面这个get方法

在获取到缓存后再次取缓存剩余的时间,如果时间小于我们配置的刷新时间就手动刷新缓存。为了不影响get的性能,启用后台线程去完成缓存的刷新

public ValueWrapper get(Object key) 
    ValueWrapper valueWrapper= super.get(key);
    if(null!=valueWrapper)
        Long ttl= this.redisOperations.getExpire(key);
        if(null!=ttl && ttl<=this.preloadSecondTime)
            logger.info("key: ttl: preloadSecondTime:
						",key,ttl,preloadSecondTime);
            ThreadTaskHelper.run(new Runnable() 
                @Override
                public void run() 
                    //重新加载数据
                    logger.info("refresh key:",key);
                    CustomizedRedisCache.this.getCacheSupport().
				    refreshCacheByKey(CustomizedRedisCache.
								   super.getName(),key.toString());
                
            );
        
    
    return valueWrapper;

  • ThreadTaskHelper是个帮助类,但需要考虑重复请求问题,及相同的数据在并发过程中只允许刷新一次,这块还没有完善就不贴代码了。

  • @Cacheable,并记录执行方法信息上面提到的缓存获取时,会根据配置的刷新时间来判断是否需要刷新数据,当符合条件时会触发数据刷新。

  • 但它需要知道执行什么方法以及更新哪些数据,所以就有了下面这些类。

CacheSupport

刷新缓存接口,可刷新整个容器的缓存也可以只刷新指定键的缓存。

public interface CacheSupport 

	/**
	 * 刷新容器中所有值
	 * @param cacheName
     */
	void refreshCache(String cacheName);

	/**
	 * 按容器以及指定键更新缓存
	 * @param cacheName
	 * @param cacheKey
     */
	void refreshCacheByKey(String cacheName,String cacheKey);

InvocationRegistry

执行方法注册接口,能够在适当的地方主动调用方法执行来完成缓存的更新。

public interface InvocationRegistry 

	void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set<String> cacheNames);

CachedInvocation

执行方法信息类,这个比较简单,就是满足方法执行的所有信息即可。

public final class CachedInvocation 
    private Object key;
    private final Object targetBean;
    private final Method targetMethod;
    private Object[] arguments;
    public CachedInvocation(Object key, Object targetBean, Method 
							targetMethod, Object[] arguments) 
        this.key = key;
        this.targetBean = targetBean;
        this.targetMethod = targetMethod;
        if (arguments != null && arguments.length != 0) 
            this.arguments = Arrays.copyOf(arguments, arguments.length);
        
    

CacheSupportImpl

这个类主要实现上面定义的缓存刷新接口以及执行方法注册接口

刷新缓存

获取cacheManager用来操作缓存:

@Autowired
private CacheManager cacheManager;

实现缓存刷新接口方法:

@Override
public void refreshCache(String cacheName) 
	this.refreshCacheByKey(cacheName,null);


@Override
public void refreshCacheByKey(String cacheName, String cacheKey) 
	if (cacheToInvocationsMap.get(cacheName) != null) 
		for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) 
			if(!StringUtils.isBlank(cacheKey)&&invocation.getKey().toString().equals(cacheKey)) 
				refreshCache(invocation, cacheName);
			
		
	

反射来调用方法:

private Object invoke(CachedInvocation invocation)
			throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException 
	final MethodInvoker invoker = new MethodInvoker();
	invoker.setTargetObject(invocation.getTargetBean());
	invoker.setArguments(invocation.getArguments());
	invoker.setTargetMethod(invocation.getTargetMethod().getName());
	invoker.prepare();
	return invoker.invoke();

缓存刷新最后实际执行是这个方法,通过invoke函数获取到最新的数据,然后通过cacheManager来完成缓存的更新操作。

private void refreshCache(CachedInvocation invocation, String cacheName) 

	boolean invocationSuccess;
	Object computed = null;
	try 
		computed = invoke(invocation);
		invocationSuccess = true;
	 catch (Exception ex) 
		invocationSuccess = false;
	
	if (invocationSuccess) 
		if (cacheToInvocationsMap.get(cacheName) != null) 
			cacheManager.getCache(cacheName).put(invocation.getKey(), computed);
		
	

执行方法信息注册

定义一个Map用来存储执行方法的信息:

private Map<String, Set> cacheToInvocationsMap;
实现执行方法信息接口,构造执行方法对象然后存储到Map中。

@Override
public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments, Set<String> annotatedCacheNames) 

	StringBuilder sb = new StringBuilder();
	for (Object obj : arguments) 
		sb.append(obj.toString());
	

	Object key = sb.toString();

	final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments);
	for (final String cacheName : annotatedCacheNames) 
		String[] cacheParams=cacheName.split("#");
		String realCacheName = cacheParams[0];
		if(!cacheToInvocationsMap.containsKey(realCacheName)) 
			this.initialize();
		
		cacheToInvocationsMap.get(realCacheName).add(invocation);
	

CachingAnnotationsAspect

@Cacheable方法信息并完成注册,将使用了缓存的方法的执行信息存储到Map中,key是缓存容器的名称,value是不同参数的方法执行实例,核心方法就是registerInvocation。

@Around("pointcut()")
public Object registerInvocation(ProceedingJoinPoint joinPoint) throws Throwable
	Method method = this.getSpecificmethod(joinPoint);
	List<Cacheable> annotations=this.getMethodAnnotations(method,Cacheable.class);
	Set<String> cacheSet = new HashSet<String>();
	for (Cacheable cacheables : annotations) 
		cacheSet.addAll(Arrays.asList(cacheables.value()));
	
	cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method, joinPoint.getArgs(), cacheSet);
	return joinPoint.proceed();

客户端调用

指定5秒后过期,并且在缓存存活3秒后如果请求命中,会在后台启动线程重新从数据库中获取数据来完成缓存的更新。理论上前端不会存在缓存不命中的情况,当然如果正好最后两秒没有请求那也会出现缓存失效的情况。

@Cacheable(value = "Product#5#2",key ="#id")
public Product getById(Long id) 
    //...

深入浅出spring原理及实战「开发实战系列」手把手教你将@schedule任务调度升级为分布式调度@distributeschedule(代码片段)

背景介绍很多小伙伴们都跟我留言说过一个类似的问题,就是针对于任务调度框架而言的选取,很多公司都会采用任务调度框架的鼻祖Quartz,那么我们来梳理以下Java领域的任务调度框架吧。Java领域的定时任务的框架... 查看详情

深入浅出spring原理及实战「开发实战系列」springsecurity技术实战之通过注解表达式控制方法权限(代码片段)

SpringSecurity权限控制机制SpringSecurity中可以通过表达式控制方法权限,其中有四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行... 查看详情

深入浅出spring原理及实战「开发实战系列」采用protostuff和kryo高性能序列化框架实现redistemplate的序列化组件(代码片段)

序列化序列化可以简单理解为对象–>字节的过程,同理,反序列化则是相反的过程。为什么需要序列化?因为网络传输只认字节。所以互信的过程依赖于序列化。网络传输的性能等诸多因素,通常会支持多种序... 查看详情

深入浅出spring原理及实战「开发实战系列」oauth2的技术体系架构和开发概览

背景介绍主要实现OAuth2的三种授权模式:密码模式、客户端模式和授权码模式,包括展示授权服务器、资源服务器、客户端等几种角色的交互,以及JWT的整合。并且每个实例都提供两个代码版本:一个是基于旧的S... 查看详情

深入浅出spring原理及实战「开发实战系列」springsecurity与jwt实现权限管控以及登录认证指南(代码片段)

SpringSecurity介绍SpringSecurity是一个用于Java企业级应用程序的安全框架,主要包含用户认证和用户授权两个方面,相比较Shiro而言,Security功能更加的强大,它可以很容易地扩展以满足更多安全控制方面的需求,... 查看详情

深入浅出springcloud原理及实战「springcloud-alibaba系列」微服务模式搭建系统基础架构实战指南及版本规划踩坑分析

前提介绍SpringCloud-Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用服务的必需组件,方便开发者通过SpringCloud编程模型轻松使用这些组件来开发分布式应用服务。依托SpringCloudAlibaba,您只需要... 查看详情

深入浅出spring原理及实战「源码原理实战」从底层角度去分析研究propertysourcesplaceholderconfigurer的原理及实战注入机制

Spring提供配置解析功能主要有一下xml文件占位符解析和Java的属性@Value的占位符解析配置这两种场景进行分析和实现解析,如下面两种案例。xml文件的占位符解析配置<beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"init-method="i... 查看详情

深入浅出dubbo3原理及实战「技术大纲」深入浅出并发实战课程系列及技术指南

Dubbo3开题简介如开篇所述,Dubbo提供了构建云原生微服务业务的一站式解决方案,可以使用Dubbo快速定义并发布微服务组件,同时基于Dubbo开箱即用的丰富特性及超强的扩展能力,构建运维整个微服务体系所需的各... 查看详情

深入浅出sentinel原理及实战「基础实战专题」零基础实现服务流量控制实战开发指南

你若要喜爱你自己的价值,你就得给世界创造价值。Sentinel的组成部分Sentinel主要由以下两个部分组成。Sentinel核心库(Java客户端):Sentinel的核心库不依赖任何框架或库,能够运行于Java8及以上的版本的运行时环境中,同时对Spri... 查看详情

深入浅出sentinel原理及实战「基础实战专题」零基础探索分析sentinel控制台开发指南

Sentinel控制台Sentinel提供了一个轻量级的开源控制台SentinelDashboard,它提供了机器发现与健康情况管理、监控(单机和集群)、规则管理与推送等多种功能。Sentinel控制台提供的功能如下查看机器列表以及健康情况:Sentnel控制台能... 查看详情

深入浅出spring原理及实战「原理分析专题」从零开始教你springel表达式使用和功能分析讲解指南(上篇)(代码片段)

SpringEL表达式语言,这种语言jsp中学到的el,但是在整个spring之中其表达式语言要更加的复杂,而且支持度更加的广泛,最重要的是他可以进行方法的调用,对象的实例化,集合操作等等,但是唯一的难点就是:代码太复杂了,表达式太复杂... 查看详情

精华推荐|深入浅出rocketmq原理及实战「底层源码挖掘系列」透彻剖析贯穿rocketmq的消费者端的运行核心的流程(上篇)

精华推荐|【深入浅出RocketMQ原理及实战】「底层源码挖掘系列」透彻剖析贯穿RocketMQ的消费者端的运行核心的流程上篇:分析对应总体消费流程的判断和校验以及限流控制和回调等处理流程分析下篇:分析基于上篇的总体流程的... 查看详情

深入浅出springcloud原理及实战「netflix系列之hystrix」针对于限流熔断组件hystrix的基本参数和实现原理介绍分析(代码片段)

...此度过糟糕的一生。[温馨提示]承接第一篇文章🏹【深入浅出SpringCloud原理及实战】「Netflix系列之Hystrix」针对于限流熔断组件Hystrix的基本参数和实现原理介绍分析在这里推荐给大家martinfowler的熔断器介绍和权威指南,有兴趣... 查看详情

深入浅出springcloud原理及实战「netflix系列之hystrix」针对于限流熔断组件hystrix的超时机制的原理和实现分析(代码片段)

...此度过糟糕的一生。[温馨提示]承接第一篇文章🏹【深入浅出SpringCloud原理及实战】「Netflix系列之Hystrix」针对于限流熔断组件Hystrix的基本参数和实现原理介绍分析在这里推荐给大家martinfowler的熔断器介绍和权威指南,有兴趣... 查看详情

深入浅出seata原理及实战「入门基础专题」探索seata服务的at模式下的分布式开发实战指南

承接上文上一篇文章说到了Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。那么接下来我们将要针对于AT模式下进行分布式事务开发的原理进行介绍以及实战。SeataAT模式在AT、TCC、SAGA和XA这四种... 查看详情

精华推荐|深入浅出rocketmq原理及实战「底层原理挖掘系列」透彻剖析贯穿rocketmq的存储系统的实现原理和持久化机制

RocketMQ的发展历史RocketMQ是一个统一消息引擎、轻量级数据处理平台。RocketMQ是一款阿里巴巴开源的消息中间件。2016年11月28日,阿里巴巴向广西党性培训Apache软件基金会捐赠RocketMQ,成为Apache孵化项目。2017年9月25日,Apache宣布Rock... 查看详情

精华推荐|深入浅出rocketmq原理及实战「底层原理挖掘系列」透彻剖析贯穿rocketmq的存储系统的实现原理和持久化机制

RocketMQ的发展历史RocketMQ是一个统一消息引擎、轻量级数据处理平台。RocketMQ是一款阿里巴巴开源的消息中间件。2016年11月28日,阿里巴巴向广西党性培训Apache软件基金会捐赠RocketMQ,成为Apache孵化项目。2017年9月25日,A... 查看详情

深入浅出spring原理及实战「源码调试分析」结合datasourceregister深入分析importbeandefinitionregistrar的源码运作流程

每日一句人的一生中不可能会一帆风顺,总会遇到一些挫折,当你对生活失去了信心的时候,仔细的看一看、好好回想一下你所遇到的最美好的事情吧,那会让你感觉到生活的美好。注入案例代码如何通过实现SpringBoot框架带有... 查看详情