Spring Cache扩展功能实现过程解析

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

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")

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

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

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

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

背景

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

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

扩展RedisCacheManagerCustomizedRedisCacheManager

继承自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<CachedInvocation>> 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) {
  //...
}

代码

可以从项目中下载。

引用

刷新缓存的思路取自于这个开源项目。https://github.com/yantrashala/spring-cache-self-refresh

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Boot 2.X整合Spring-cache(让你的网站速度飞起来)

    计算机领域有人说过一句名言:"计算机科学领域的任何问题都可以通过增加一个中间层来解决",今天我们就用Spring-cache给网站添加一层缓存,让你的网站速度飞起来. 本文目录 一.Spring Cache介绍二.缓存注解介绍三.Spring Boot+Cache实战1.pom.xml引入jar包2.启动类添加@EnableCaching注解3.配置数据库和redis连接4.配置CacheManager5.使用缓存注解6.查看缓存效果7.注意事项 一.Spring Cache介绍 Sp

  • Spring Cache手动清理Redis缓存

    这篇文章主要介绍了Spring Cache手动清理Redis缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 注册cacheRedisTemplate 将 cache 的 RedisTemplate 注册为Bean @Bean(name = "cacheRedisTemplate") public RedisTemplate cacheRedisTemplate(@Qualifier("jedisConnectionFac

  • 使用ehcache三步搞定springboot缓存的方法示例

    本次内容主要介绍基于Ehcache 3.0来快速实现Spring Boot应用程序的数据缓存功能.在Spring Boot应用程序中,我们可以通过Spring Caching来快速搞定数据缓存.接下来我们将介绍如何在三步之内搞定Spring Boot缓存. 1. 创建一个Spring Boot工程并添加Maven依赖 你所创建的Spring Boot应用程序的maven依赖文件至少应该是下面的样子: <?xml version="1.0" encoding="UTF-8

  • SpringBoot中Shiro缓存使用Redis、Ehcache的方法

    SpringBoot 中配置redis作为session 缓存器. 让shiro引用 本文是建立在你是使用这shiro基础之上的补充内容 第一种:Redis缓存,将数据存储到redis 并且开启session存入redis中. 引入pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifac

  • SpringBoot加入Guava Cache实现本地缓存代码实例

    这篇文章主要介绍了SpringBoot加入Guava Cache实现本地缓存代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在pom.xml中加入guava依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version>

  • Spring Boot整合Spring Cache及Redis过程解析

    这篇文章主要介绍了Spring Boot整合Spring Cache及Redis过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.安装redis a.由于官方是没有Windows版的,所以我们需要下载微软开发的redis,网址: https://github.com/MicrosoftArchive/redis/releases b.解压后,在redis根目录打开cmd界面,输入:redis-server.exe redis.wind

  • springboot接入cachecloud redis示例实践

    最近项目中需要接入  Redis CacheCloud,   CacheCloud是一个开源的 Redis 运维监控云平台,功能十分强大,支持Redis 实例自动部署.扩容.碎片管理.统计.监控等功能, 特别是支持单机.sentinel .cluster三种模式的自动部署,搭建redis集群一步到位轻松搞定. java项目中 接入 CacheCloud redis的方式主要有两种. 第一种就是在 CacheCloud 上创建好redis实例后将对应的IP,端口直接配置以配置形式应用到项目中,优点

  • springboot使用GuavaCache做简单缓存处理的方法

    问题背景 实际项目碰到一个上游服务商接口有10秒的查询限制(同个账号). 项目中有一个需求是要实时统计一些数据,一个应用下可能有多个相同的账号.由于服务商接口的限制,当批量查询时,可能出现同一个账号第一次查询有数据,但第二次查询无数据的情况. 解决方案 基于以上问题,提出用缓存的过期时间来解决. 这时,可用Redis和Guava Cache来解决: 当批量查询时,同一个账号第一次查询有数据则缓存并设置过期时间10s, 后续查询时直接从缓存中取,没有再从服务商查询. 最终采用Guava Cache

  • Spring Cache扩展功能实现过程解析

    两个需求缓存失效时间支持在方法的注解上指定 Spring Cache默认是不支持在@Cacheable上添加过期时间的,可以在配置缓存容器时统一指定: @Bean public CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { CustomizedRedisCacheManager cacheManager= new CustomizedRedisCacheMa

  • Spring整合MyBatis图示过程解析

    这篇文章主要介绍了Spring整合MyBatis图示过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.导入所需要的jar依赖 !--MyBatis和Spring的整合包 由MyBatis提供--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <vers

  • Spring Boot 整合 Druid过程解析

    这篇文章主要介绍了Spring Boot 整合 Druid过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概述 Druid 是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池.插件框架和 SQL 解析器组成.该项目主要是为了扩展 JDBC 的一些限制,可以让程序员实现一些特殊的需求,比如向密钥服务请求凭证.统计 SQL 信息.SQL 性能收集.SQL 注入检查.SQL 翻译等,程序员可以通过定制来实现自己需要的功能. Druid 是

  • Spring boot整合log4j2过程解析

    这篇文章主要介绍了Spring boot整合log4j2过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 以前整合过log4j2,但是今天再次整合发现都忘记了,而且也没有记下来 1.pom.xml中 (1)把spring-boot-starter-web包下面的spring-boot-starter-logging排除 <dependency> <groupId>org.springframework.boot</gr

  • SPRING IOC注入方式过程解析

    这篇文章主要介绍了SPRING IOC注入方式过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Spring IOC注入的方法主要有两种 1:设值注入 2:构造注入 简单来说一个是调用set方法设值,一个是通过构造函数设值 Spring-ioc.xml <?xml version= "1.0" encoding= "UTF-8"?> <beans xmlns= "http://ww

  • spring boot整合kafka过程解析

    这篇文章主要介绍了spring boot整合kafka过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.启动kafka 启动kafka之前一定要启动zookeeper,因为要使用kafka必须要使用zookeeper. windows环境下启动,直接使用kafka自带的zookeeper: E:\kafka_2.12-2.4.0\bin\windows zookeeper-server-start.bat ..\..\config\z

  • Spring Bean实例化实现过程解析

    这篇文章主要介绍了Spring Bean实例化实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Bean的实例化 1.构造器实例化:Spring容器通过Bean对应类中默认的无参构造方法来实例化Bean package com.itheima.instance.constructor; public class Bean1 { } <?xml version="1.0" encoding="UTF-8&quo

  • Spring cache源码深度解析

    Spring cache是一个缓存API层,封装了对多种缓存的通用操作,可以借助注解方便地为程序添加缓存功能.常见的注解有@Cacheable.@CachePut.@CacheEvict,有没有想过背后的原理是什么?楼主带着疑问,阅读完Spring cache的源码后,做一个简要总结.先说结论,核心逻辑在CacheAspectSupport类,封装了所有的缓存操作的主体逻辑,下面详细介绍. 题外话:如何阅读开源代码? 有2种方法,可以结合起来使用: 静态代码阅读:查找关键类.方法的usage之处

  • Spring cache源码深度解析

    目录 前言 题外话:如何阅读开源代码? 核心类图 源码分析(带注释解释) 1.解析注解 2.逻辑执行 总结 前言 Spring cache是一个缓存API层,封装了对多种缓存的通用操作,可以借助注解方便地为程序添加缓存功能. 常见的注解有@Cacheable.@CachePut.@CacheEvict,有没有想过背后的原理是什么?楼主带着疑问,阅读完Spring cache的源码后,做一个简要总结. 先说结论,核心逻辑在CacheAspectSupport类,封装了所有的缓存操作的主体逻辑,下面

随机推荐