在SpringCache缓存初探中咱们研究了如何利用spring cache已有的几种实现快速地知足咱们对于缓存的需求。这一次咱们有了新的更个性化的需求,想在一个请求的生命周期里实现缓存。html
需求背景是:一次数据的组装须要调用多个方法,然而在这多个方法里又会调用同一个IO接口,此时多浪费了一次IO的资源。首先想到的解决方案是将此次IO接口提出来调用,而后将结果做为参数传递到多个方法中,可是这样一来,每一个调用这些方法的地方都得添加额外的代码。那么第二个方案就是,咱们仍是分别调用,只不过将这个结果缓存起来,就像咱们以前作的那样。java
这时候问题来了,这个数据结果咱们但愿尽量实时,即便只缓存了一秒,致使在不一样的请求里用了同一份数据也不太好,又或者缓存效率很是低下,可能就这个请求会查几回。看来不得不本身实现一个只保持在一次请求过程当中的缓存了。node
要将数据缓存在一次请求周期内,那咱们先得区分是什么环境下的请求,以分析咱们如何存储数据。git
Web环境下的有个绝佳的数据存储位置 HttpServletRequest
的Attribute
。调用setAttribute
和getAttribute
方法就能轻易地将咱们的数据用key-value的形式存储在请求上,并且每次请求都自动拥有一个干净的Request
。想要获取到HttpServletRequest
也很是简单,在web请求中随时随地调用((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
便可。github
我司所使用的rpc框架是基于finagle自研的,对外提供服务时使用线程池进行处理请求,即对于一次完整的请求,会使用同一个线程进行处理。首先想到的办法仍是改动这个rpc框架服务端,增长一个能够对外暴露的、能够key-value存储的请求上下文。为了能在方便的地方获取到这个请求上下文,得将其存储在ThreadLocal
中。 web
综合这两种环境考虑,咱们最好仍是实现一个统一的方案以减小维护和开发成本。Spring的RequestContextHolder.getRequestAttributes()
其实也是使用ThreadLocal
来实现的,那咱们能够统一将数据存到ThreadLocal<Map<Object,Object>>
,本身来维护缓存的清理。spring
存储位置有了,接下来实现SpringCache思路就比较清晰了。 windows
要实现SpringCache须要一个CacheManager,接口定义以下缓存
xxxxxxxxxx
public interface CacheManager {
Cache getCache(String name);
Collection<String> getCacheNames();
}
能够看到其实只须要实现Cache接口就好了。 在上一篇文章中提到的SimpleCacheManager
,它的Cache实现ConcurrentMapCache
内部的存储是依赖ConcurrentMap<Object, Object>
。咱们的实现跟它很是相似,最主要的不一样是咱们须要使用ThreadLocal<Map<Object, Object>>
下面给出几处关键的实现,其余部分简单看下ConcurrentMapCache
就能明白。app
咱们选择不直接继承Cache而是AbstractValueAdaptingCache
,其被大多数缓存实现所继承,它的做用主要是包装value值以区分是没有命中缓存仍是缓存的null值。
xxxxxxxxxx
private final ThreadLocal<Map<Object, Object>> store = ThreadLocal.withInitial(() -> new HashMap<>(128));
咱们的缓存数据存储的地方,ThreadLocal
保证缓存只会存在于这一个线程中。同时又由于只有一个线程可以访问,咱们简单地使用HashMap
便可。
xxxxxxxxxx
public <T> T get(Object key, Callable<T> valueLoader) {
return (T) fromStoreValue(this.store.get().computeIfAbsent(key, r -> {
try {
return toStoreValue(valueLoader.call());
} catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
}));
}
至此咱们即将大功告成,只差一个步骤,ThreadLocal
的清理:使用AOP
实现便可。
xxxxxxxxxx
"bean(server)") (
public void clearThreadCache() {
threadCacheManager.clear();
}
记得将Cache的clear
方法经过咱们自定义的CacheManager
暴露出来。同时也要确保切面能覆盖每一个请求的结束。
从以上一个简单的ThreadLocalCacheManager
实现,咱们对CacheManager
又有了更多的理解。
同时可能也会有更多的疑问。
再回顾Spring Cache为咱们提供的@Cacheable
中的sync
的注释,它提到此功能的做用是: 同步化对被注解方法的调用,使得多个线程试图调用此方法时,只有一个线程可以成功调用,其余线程直接取此次调用的返回值。同时也提到这仅仅只是个hint
,是否真的能成仍是要看缓存提供者。
咱们找到Spring Cache处理缓存调用的关键方法org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.reflect.Method, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts)
(spring-context-5.1.5.RELEASE)
通过分析,当sync = true
时, 只会调用以下代码
xxxxxxxxxx
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))))
即咱们上文实现的T get(Object key, Callable<T> valueLoader)
方法,回头一看一切都清晰了。 只要咱们的this.store.get().computeIfAbsent
是同步的,那这个sync = true
就起做用了。 固然咱们这里使用的HashMap
不支持,可是咱们若是换成ConcurrentMap
就可以实现同步化的功能。另外简单粗暴地让方法同步也是能够的(RedisCache就是这样作的)。
当sync = false
时,会组合Cache中其余的方法进行缓存的处理。逻辑较为简单清晰,自行阅读源码便可。
异步操做分两种状况,直接建立线程或者使用线程池。对于第一种状况咱们能够简单地使用java.lang.InheritableThreadLocal
来替代ThreadLocal
,建立的子进程会天然而然地共享父进程的InheritableThreadLocal
;第二种状况就相对比较复杂了,建议能够参考 alibaba/transmittable-thread-local ,它实现了线程池下的ThreadLocal
值传递功能。