前言
在 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, 接口定义如下
- public interface CacheManager {
- Cache getCache(String name);
- Collection<String> getCacheNames();
- }
可以看到其实只需要实现 Cache 接口就行了. 在上一篇文章中提到的 SimpleCacheManager, 它的 Cache 实现 ConcurrentMapCache 内部的存储是依赖 ConcurrentMap<Object, Object>. 我们的实现跟它非常类似, 最主要的不同是我们需要使用 ThreadLocal<Map<Object, Object>> 下面给出几处关键的实现, 其他部分简单看下 ConcurrentMapCache 就能明白.
1 extends
我们选择不直接继承 Cache 而是 AbstractValueAdaptingCache, 其被大多数缓存实现所继承, 它的作用主要是包装 value 值以区分是没有命中缓存还是缓存的 null 值.
- 2 store
- private final ThreadLocal<Map<Object, Object>> store = ThreadLocal.withInitial(() -> new HashMap<>(128));
我们的缓存数据存储的地方, ThreadLocal 保证缓存只会存在于这一个线程中. 同时又因为只有一个线程能够访问, 我们简单地使用 HashMap 即可.
- 3 get
- 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 实现即可.
- @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 时, 只会调用如下代码
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 值传递功能.
来源: https://www.cnblogs.com/imyijie/p/11651679.html