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
Spring Cache扩展:注解失效时间+主动刷新缓存的更多相关文章
- Spring Cache扩展:注解失效时间+主动刷新缓存(二)
		
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
 - 以Spring Cache扩展为例介绍如何进行高效的源码的阅读
		
摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...
 - 如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚
		
摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...
 - Spring Cache 自定义注解
		
1.在使用spring cache注解如cacheable.cacheevict.cacheput过程中有一些问题: 比如,我们在查到一个list后,可以将list缓存到一个键对应的区域里:当新增.修 ...
 - spring cache常用注解使用
		
1.@CacheConfig 主要用于配置该类中会用到的一些共用的缓存配置.示例: @CacheConfig(cacheNames = "users") public interf ...
 - spring cache会默认使用redis作为缓存吗?
		
web项目中,只需要配置 redis 的IP,端口,用户名和密码就可以使用redis作为缓存了,不需要在在java 代码中配置redisConfig,redisConfig只是作为缓存配置的辅助,比如 ...
 - 一个缓存使用案例:Spring Cache VS Caffeine 原生 API
		
最近在学习本地缓存发现,在 Spring 技术栈的开发中,既可以使用 Spring Cache 的注解形式操作缓存,也可用各种缓存方案的原生 API.那么是否 Spring 官方提供的就是最合适的方案 ...
 - 【开源项目系列】如何基于 Spring Cache 实现多级缓存(同时整合本地缓存 Ehcache 和分布式缓存 Redis)
		
一.缓存 当系统的并发量上来了,如果我们频繁地去访问数据库,那么会使数据库的压力不断增大,在高峰时甚至可以出现数据库崩溃的现象.所以一般我们会使用缓存来解决这个数据库并发访问问题,用户访问进来,会先从 ...
 - springboot redis-cache 自动刷新缓存
		
这篇文章是对上一篇 spring-data-redis-cache 的使用 的一个补充,上文说到 spring-data-redis-cache 虽然比较强悍,但还是有些不足的,它是一个通用的解决方案 ...
 
随机推荐
- JavaScript 开发工具webstrom使用指南
			
本文给大家推荐了一款非常热门的javascript开发工具webstrom,着重介绍了webstrom的特色功能.设置技巧.使用心得以及快捷键汇总,非常的全面. 看到网上一篇介绍webstrom的文章 ...
 - 插入排序法-java案例详解
			
/** * 功能:插入排序法 * 基本思想:把n个待排序的元素看成一个有序和无序表,开始时有序表中只包含一个元素, * 无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码 ...
 - php常用图片处理类
			
<?php /** * 已知问题:1.在图片缩放功能中,使用imagecreatetruecolor函数创建画布,并使用透明处理算法,但PNG格式的图片无法透明.用imagecreate函数创建 ...
 - AnsiIO
			
1.文件数据内容,元数据内容 i节点ls -l err.txt-rw-rw-r-- 1 csgec csgec 50 Jun 23 11:19 err.txt-:普通文件(文件类型)rw-:属主用户拥 ...
 - --@angularJS--指令与控制器之间的交互demo
			
1.index.html: <!DOCTYPE HTML><html ng-app="app"><head> <title>c ...
 - jQuery 鼠标滚轮插件 jquery.mousewheel.js
			
jQuery Mousewheel Plugin,用于添加跨浏览器的鼠标滚轮支持.mousewheel事件的处理函数有一点小小的变化,它除了第一个参数event 外,还接收到第二个参数delta.通过 ...
 - 清除delphi 控件DBgrid 的记录
			
http://blog.csdn.net/windhaunting/article/details/4751560 1.TTable(DBGrid1.DataSource.DataSet).Empty ...
 - Google Analytics之增强型电子商务报告
			
虽然Google Analytics很多年以前就提供了电子商务报告的功能,但对于电子商务网站来说,这个报告缺失的东西还太多.而Google Analytics即将推出的增强型电子商务报告有望弥补这一短 ...
 - Java多线程(学习篇)
			
Java多线程:(学习篇) 1.什么是线程 2.线程状态 3.线程中断 4.线程交互 5.同步机制 6.锁机制 7.堵塞队列与堵塞栈 8.条件变量.原子量.线程池等 9.线性安全类和Callable与 ...
 - leetcode难度及面试频率
			
转载自:LeetCode Question Difficulty Distribution 1 Two Sum 2 5 array sort set ...