SpringCache – 请求级别缓存的简易实现

前言

SpringCache缓存初探

中我们研究了如何利用spring cache已有的几种实现快速地满足我们对于缓存的需求。这一次我们有了新的更个性化的需求,想在一个请求的生命周期里实现缓存。

需求背景是:一次数据的组装需要调用多个方法,然而在这多个方法里又会调用同一个IO接口,此时多浪费了一次IO的资源。首先想到的解决方案是将这次IO接口提出来调用,然后将结果作为参数传递到多个方法中,但是这样一来,每个调用这些方法的地方都得添加额外的代码。那么第二个方案就是,我们还是分别调用,只不过将这个结果缓存起来,就像我们之前做的那样。

这时候问题来了,这个数据结果我们希望尽可能实时,即使只缓存了一秒,导致在不同的请求里用了同一份数据也不太好。看来不得不自己实现一个只保持在一次请求过程中的缓存了。

方案分析

要将数据缓存在一次请求周期内,那我们先得区分是什么环境下的请求,以分析我们如何存储数据。

1. Web

Web环境下的有个绝佳的数据存储位置
HttpServletRequest

Attribute
。调用
setAttribute

getAttribute
方法就能轻易地将我们的数据用key-value的形式存储在请求上,而且每次请求都自动拥有一个干净的
Request
。想要获取到
HttpServletRequest
也非常简单,在web请求中随时随地调用
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
即可。

2. RPC框架

我司所使用的rpc框架是基于finagle自研的,对外提供服务时使用线程池进行处理请求,即对于一次完整的请求,会使用同一个线程进行处理。首先想到的办法还是改动这个rpc框架服务端,增加一个可以对外暴露的、可以key-value存储的请求上下文。为了能在方便的地方获取到这个请求上下文,得将其存储在
ThreadLocal
中。

综合这两种环境考虑,我们最好还是实现一个统一的方案以减少维护和开发成本。Spring的
RequestContextHolder.getRequestAttributes()
其实也是使用
ThreadLocal
来实现的,那我们可以统一将数据存到
ThreadLocal<Map<Object,Object>>
,自己来维护缓存的清理。

存储位置有了,接下来实现SpringCache思路就比较清晰了。

实现SpringCache

要实现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
就能明白。

1 extends

xxxxxxxxxx
class ThreadCache extends AbstractValueAdaptingCache

AbstractValueAdaptingCache
被大多数缓存实现所继承,它的作用主要是包装value值以区分是没有命中缓存还是缓存的null值。

2 store

xxxxxxxxxx
private final ThreadLocal<Map<Object, Object>> store = ThreadLocal.withInitial(() -> new HashMap<>(128));

我们的缓存数据存储的地方,
ThreadLocal
保证缓存只会存在于这一个线程中。同时又因为只有一个线程能够访问,我们简单地使用
HashMap
即可。

3 lookup

xxxxxxxxxx
protected Object lookup(Object key) {
    return  this.store.get().get(key);
}

直接获取值

4 get

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);     
        }  
     }));
 }

非常类似于
Map

computeIfAbsent

5 put

xxxxxxxxxx
public void put(Object key, Object value) {
    this.store.get().put(key, toStoreValue(value));
}

6 putIfAbsent

xxxxxxxxxx
public ValueWrapper putIfAbsent(Object key, Object value) {
    return toValueWrapper(this.store.get().putIfAbsent(key, toStoreValue(value)));
}

7 evict

xxxxxxxxxx
public void evict(Object key) {
    this.store.get().remove(key);
}

8 clear

xxxxxxxxxx
public void clear() {
    this.store.get().clear();
}

至此我们即将大功告成,只差一个步骤,
ThreadLocal
的清理:使用
AOP
实现即可。

xxxxxxxxxx
    @After("bean(server)")
    public void clearThreadCache() {
        threadCacheManager.clear();
    }

记得将Cache的
clear
方法通过我们自定义的
CacheManager
暴露出来。同时也要确保切面能覆盖每个请求的结束。

总结与扩展

从以上一个简单的
ThreadLocalCacheManager
实现,我们对
CacheManager
又有了更多的理解。

同时可能也会有更多的疑问。

1. 我们实现的这些方法,从方法名和逻辑上看起来都很简单,那他们是如何配合使用的?跟@Cacheable上的sync又有什么关系呢?

再回顾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中其他的方法进行缓存的处理。逻辑较为简单清晰,自行阅读源码即可。

2. 用ThreadLocal严格来说实现的只是线程内的缓存,万一一次请求中有异步操作怎么办?

异步操作分两种情况,直接创建线程或者使用线程池。对于第一种情况我们可以简单地使用
java.lang.InheritableThreadLocal
来替代
ThreadLocal
,创建的子进程会自然而然地共享父进程的
InheritableThreadLocal
;第二种情况就相对比较复杂了,建议可以参考

alibaba/transmittable-thread-local

,它实现了线程池下的
ThreadLocal
值传递功能。

原文 

http://www.cnblogs.com/imyijie/p/11651679.html

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » SpringCache – 请求级别缓存的简易实现

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址