Spring Cache扩展:注解失效时间+主动刷新缓存

*:first-child {
margin-top: 0 !important;
}

body>*:last-child {
margin-bottom: 0 !important;
}

/* BLOCKS
=============================================================================*/

p, blockquote, ul, ol, dl, table, pre {
margin: 15px 0;
}

/* HEADERS
=============================================================================*/

h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
}

h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
font-size: inherit;
}

h1 {
font-size: 28px;
color: #000;
}

h2 {
font-size: 24px;
border-bottom: 1px solid #ccc;
color: #000;
}

h3 {
font-size: 18px;
}

h4 {
font-size: 16px;
}

h5 {
font-size: 14px;
}

h6 {
color: #777;
font-size: 14px;
}

body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
margin-top: 0;
padding-top: 0;
}

a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0;
}

h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
margin-top: 10px;
}

/* LINKS
=============================================================================*/

a {
color: #4183C4;
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

/* LISTS
=============================================================================*/

ul, ol {
padding-left: 30px;
}

ul li > :first-child,
ol li > :first-child,
ul li ul:first-of-type,
ol li ol:first-of-type,
ul li ol:first-of-type,
ol li ul:first-of-type {
margin-top: 0px;
}

ul ul, ul ol, ol ol, ol ul {
margin-bottom: 0;
}

dl {
padding: 0;
}

dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px;
}

dl dt:first-child {
padding: 0;
}

dl dt>:first-child {
margin-top: 0px;
}

dl dt>:last-child {
margin-bottom: 0px;
}

dl dd {
margin: 0 0 15px;
padding: 0 15px;
}

dl dd>:first-child {
margin-top: 0px;
}

dl dd>:last-child {
margin-bottom: 0px;
}

/* CODE
=============================================================================*/

pre, code, tt {
font-size: 12px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}

code, tt {
margin: 0 0px;
padding: 0px 0px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px;
}

pre>code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}

pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
}

pre code, pre tt {
background-color: transparent;
border: none;
}

kbd {
-moz-border-bottom-colors: none;
-moz-border-left-colors: none;
-moz-border-right-colors: none;
-moz-border-top-colors: none;
background-color: #DDDDDD;
background-image: linear-gradient(#F1F1F1, #DDDDDD);
background-repeat: repeat-x;
border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
border-image: none;
border-radius: 2px 2px 2px 2px;
border-style: solid;
border-width: 1px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
line-height: 10px;
padding: 1px 4px;
}

/* QUOTES
=============================================================================*/

blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777;
}

blockquote>:first-child {
margin-top: 0px;
}

blockquote>:last-child {
margin-bottom: 0px;
}

/* HORIZONTAL RULES
=============================================================================*/

hr {
clear: both;
margin: 15px 0;
height: 0px;
overflow: hidden;
border: none;
background: transparent;
border-bottom: 4px solid #ddd;
padding: 0;
}

/* IMAGES
=============================================================================*/

img {
max-width: 100%
}
-->

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为前提来实现上面两个需求,其它类型的缓存原理应该是相同的。

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

扩展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<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) {
    //...
}

代码

可以从我的个人项目中下载。spring cache code

引用

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

时间: 03-06

Spring Cache扩展:注解失效时间+主动刷新缓存的相关文章

如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚

摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读文档.查看源码,然后又很快忘记.始终不能融汇贯通.本文主要基于Spring Cache扩展为例,介绍如何进行高效的源码阅读. SpringCache的介绍 为什么以Spring Cache为例呢,原因有两个 Spring框架是web开发最常用的框架,值得开发者去阅读代码,吸收思想 缓存是企业级应用开

Spring Cache 自定义注解

1.在使用spring cache注解如cacheable.cacheevict.cacheput过程中有一些问题: 比如,我们在查到一个list后,可以将list缓存到一个键对应的区域里:当新增.修改.删除一个元素的时候,其实我们 需要的是只将cache的list里的元素变动就可以了,但因为只有一个键,没法做到只更改一个元素,只能整个list重新加载, 对性能还是有一定的影响: 2.对spring扩展,增加自定义cache, @MyCacheDelete:当删除元素后,从cache里取出该ke

Spring Cache常用注解【转】

转自博客:https://www.cnblogs.com/kingsonfu/p/10409596.html 原文博主:傻不拉几猫 作参考储备用 1.@CacheConfig 主要用于配置该类中会用到的一些共用的缓存配置.示例: @CacheConfig(cacheNames = "users") public interface UserService {...} 配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable

springboot redis-cache 自动刷新缓存

这篇文章是对上一篇 spring-data-redis-cache 的使用 的一个补充,上文说到 spring-data-redis-cache 虽然比较强悍,但还是有些不足的,它是一个通用的解决方案,但对于企业级的项目,住住需要解决更多的问题,常见的问题有 缓存预热(项目启动时加载缓存) 缓存穿透(空值直接穿过缓存) 缓存雪崩(大量缓存在同一时刻过期) 缓存更新(查询到的数据为旧数据问题) 缓存降级 redis 缓存时,redis 内存用量问题 本文解决的问题 增强 spring-data-r

Spring Cache For Redis

一.概述 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息都是立即可用的. 常用的缓存数据库: Redis   使用内存存储(in-memory)的非关系数据库,字符串.列表.集合.散列表.有序集合,每种数据类型都有自己的专属命令.另外还有批量操作(bulk operation)和不完全(partial)的事务支持 .发布与订阅.主从复制(master/slave replication).持久化.脚本(存储过程,stored procedure). 效率比ehcac

JAVA 框架 Spring Cache For Redis.

一.概述 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息都是立即可用的. 常用的缓存数据库: Redis   使用内存存储(in-memory)的非关系数据库,字符串.列表.集合.散列表.有序集合,每种数据类型都有自己的专属命令.另外还有批量操作(bulk operation)和不完全(partial)的事务支持 .发布与订阅.主从复制(master/slave replication).持久化.脚本(存储过程,stored procedure). 效率比ehcac

SpringBoot系列:Spring Boot集成Spring Cache,使用RedisCache

前面的章节,讲解了Spring Boot集成Spring Cache,Spring Cache已经完成了多种Cache的实现,包括EhCache.RedisCache.ConcurrentMapCache等. 这一节我们来看看Spring Cache使用RedisCache. 一.RedisCache使用演示 Redis是一个key-value存储系统,在web应用上被广泛应用,这里就不对其过多描述了. 本章节示例是在Spring Boot集成Spring Cache的源码基础上进行改造.源码地

注释驱动的 Spring cache 缓存介绍--转载

概述 Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果. Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存

注释驱动的 Spring cache 缓存介绍

概述 Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果. Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存