zoukankan      html  css  js  c++  java
  • SpringCache

    前言

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

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

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

    方案分析

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

    1. Web

    Web环境下的有个绝佳的数据存储位置 HttpServletRequestAttribute 。调用setAttributegetAttribute方法就能轻易地将我们的数据用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不支持能。另外简单粗暴地让方法同步也是可以的(RedisCache就是这样做的,虽然是本进程)。

    sync = false时,会组合Cache中其他的方法进行缓存的处理。逻辑较为简单清晰,自行阅读源码即可。

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

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

  • 相关阅读:
    173. Binary Search Tree Iterator
    199. Binary Tree Right Side View
    230. Kth Smallest Element in a BST
    236. Lowest Common Ancestor of a Binary Tree
    337. House Robber III
    449. Serialize and Deserialize BST
    508. Most Frequent Subtree Sum
    513. Find Bottom Left Tree Value
    129. Sum Root to Leaf Numbers
    652. Find Duplicate Subtrees
  • 原文地址:https://www.cnblogs.com/imyijie/p/11651679.html
Copyright © 2011-2022 走看看