【问题标题】:Spring cache for a given request给定请求的 Spring 缓存
【发布时间】:2013-01-22 03:38:48
【问题描述】:

我正在使用 Spring MVC 编写一个 Web 应用程序。我有一个看起来像这样的界面:

public interface SubscriptionService
{
    public String getSubscriptionIDForUSer(String userID);
}

getSubscriptionIDForUser 实际上是通过网络调用另一个服务来获取用户的订阅详细信息。我的业务逻辑在其逻辑中的多个位置调用此方法。因此,对于给定的 HTTP 请求,我可能会多次调用此方法。所以,我想缓存这个结果,这样就不会为同一个请求进行重复的网络调用。我查看了Spring documentation,但找不到有关如何为同一请求缓存此结果的参考。不用说,如果是对同一个userID的新请求,缓存应该被认为是无效的。

我的要求如下:

  1. 对于一个HTTP请求,如果多次调用getSubscriptionIDForUser,实际方法应该只执行一次。对于所有其他调用,应返回缓存的结果。

  2. 对于不同的 HTTP 请求,我们应该重新调用并忽略缓存命中,即使方法参数完全相同。

  3. 业务逻辑可能会从不同的线程并行执行其逻辑。因此对于同一个 HTTP 请求,有可能 Thread-1 当前正在调用 getSubscriptionIDForUser 方法,并且在方法返回之前,Thread-2 也尝试使用相同的参数调用相同的方法。如果是这样,那么应该让 Thread-2 等待从 Thread-1 发出的调用返回,而不是进行另一个调用。一旦从 Thread-1 调用的方法返回,Thread-2 应该得到相同的返回值。

有什么建议吗?

更新:我的 webapp 将部署到 VIP 后面的多个主机。我最重要的要求是请求级缓存。由于每个请求都将由单个主机提供服务,因此我只需要在该主机中缓存服务调用的结果。具有相同用户 ID 的新请求不得从缓存中获取值。我浏览了文档,但找不到关于它是如何完成的参考资料。是不是我看错地方了?

【问题讨论】:

    标签: java spring caching spring-mvc


    【解决方案1】:

    我想提出另一种解决方案,它比@Dmitry 提出的解决方案要小一些。我们可以在'spring-context'工件中使用Spring提供的ConcurrentMapCacheManager,而不是实现自己的CacheManager。因此,代码将如下所示(配置):

    //add this code to any configuration class
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
    

    并且可以使用:

    @Cacheable(cacheManager = "cacheManager", cacheNames = "default")
    public SomeCachedObject getCachedObject() {
        return new SomeCachedObject();
    }
    

    【讨论】:

    • ScopedProxyMode.INTERFACES 也应该作为代理模式工作,因为调用者不太可能使用 ConcurrentMapCacheManager 类特有的任何东西,而 CacheManager 接口上没有。
    • 虽然我正在测试更多,但这个解决方案似乎对我有用。谢谢老哥!
    【解决方案2】:

    我最终得到了赫尔曼在评论中建议的解决方案:

    带有简单 HashMap 的缓存管理器类:

    public class RequestScopedCacheManager implements CacheManager {
     
        private final Map<String, Cache> cache = new HashMap<>();
    
        public RequestScopedCacheManager() {
            System.out.println("Create");
        }
    
        @Override
        public Cache getCache(String name) {
            return cache.computeIfAbsent(name, this::createCache);
        }
     
        @SuppressWarnings("WeakerAccess")
        protected Cache createCache(String name) {
            return new ConcurrentMapCache(name);
        }
     
        @Override
        public Collection<String> getCacheNames() {
            return cache.keySet();
        }
     
        public void clearCaches() {
            cache.clear();
        }
     
    }
    

    然后将其设为 RequestScoped:

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public CacheManager requestScopedCacheManager() {
        return new RequestScopedCacheManager();
    }
    

    用法:

    @Cacheable(cacheManager = "requestScopedCacheManager", cacheNames = "default")
    public YourCachedObject getCachedObject(Integer id) {
        //Your code
        return yourCachedObject;
    }
    

    更新:

    一段时间后,我发现我之前的解决方案与 Spring-actuator 不兼容。 CacheMetricsRegistrarConfiguration 试图在请求范围之外初始化请求范围缓存,导致异常。

    这是我的替代实现:

    public class RequestScopedCacheManager implements CacheManager {
    
    
    public RequestScopedCacheManager() {
    }
    
    @Override
    public Cache getCache(String name) {
        Map<String, Cache> cacheMap = getCacheMap();
        return cacheMap.computeIfAbsent(name, this::createCache);
    }
    
    protected Map<String, Cache> getCacheMap() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return new HashMap<>();
        }
        @SuppressWarnings("unchecked")
        Map<String, Cache> cacheMap = (Map<String, Cache>) requestAttributes.getAttribute(getCacheMapAttributeName(), RequestAttributes.SCOPE_REQUEST);
        if (cacheMap == null) {
            cacheMap = new HashMap<>();
            requestAttributes.setAttribute(getCacheMapAttributeName(), cacheMap, RequestAttributes.SCOPE_REQUEST);
        }
        return cacheMap;
    }
    
    protected String getCacheMapAttributeName() {
        return this.getClass().getName();
    }
    
    @SuppressWarnings("WeakerAccess")
    protected Cache createCache(String name) {
        return new ConcurrentMapCache(name);
    }
    
    @Override
    public Collection<String> getCacheNames() {
        Map<String, Cache> cacheMap = getCacheMap();
        return cacheMap.keySet();
    }
    
    public void clearCaches() {
        for (Cache cache : getCacheMap().values()) {
            cache.clear();
        }
        getCacheMap().clear();
    }
    

    }

    然后注册一个 not(!) 请求范围的 bean。缓存实现将在内部获取请求范围。

    @Bean
    public CacheManager requestScopedCacheManager() {
        return new RequestScopedCacheManager();
    }
    

    用法:

        @Cacheable(cacheManager = "requestScopedCacheManager", cacheNames = "default")
    public YourCachedObject getCachedObject(Integer id) {
        //Your code
        return yourCachedObject;
    }
    

    【讨论】:

    • 感谢分享。不幸的是,它不像@nndru 语法那么优雅。然而,它是使用 CacheMetricsRegistrarConfiguration 的唯一解决方案。顺便说一句,为什么 CacheMetrics 找不到 cacheManager bean?由于@Scope 注释,该 bean 应该在这里并由 spring 代理......也许 web/mvc 或任何东西在那一刻还没有初始化?
    • 问题是,Spring 试图在容器启动时初始化缓存管理器。此时 Trere 没有请求上下文,spring 会抛出异常。
    • 嗨@Dmitry。感谢您提供有关执行器的提示。在摔倒之前不会猜到这个麻烦。顺便说一句,为什么你在更新版本中提到不(!)注册 bean 而你仍然像以前的版本一样使用 @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 注释?我猜你忘了调整代码,但在这种情况下无法说出实现它的正确方法。一个普通的@Bean 就够了?
    • 嗨@Lemmy。你说的对。我忘了删除 Scope 注释。我已经更正了我的答案。谢谢!
    • 分布式应用基础架构的缓存是否也能工作?如何使用 EhCache 配置缓存清除功能?有人知道吗?
    【解决方案3】:

    EHCache 立即出现在脑海中,或者您甚至可以推出自己的解决方案来将结果缓存在服务层中。这里可能有十亿个缓存选项。选择取决于几个因素,例如您是否需要将值超时,或者您是否要手动清理缓存。您是否需要分布式缓存,例如您有一个分布在多个应用服务器之间的无状态 REST 应用程序。您需要能够承受崩溃或重启的强大功能。

    【讨论】:

    • 我的 webapp 将部署到 VIP 后面的多个主机上。我最重要的要求是请求级缓存。由于每个请求都将由单个主机提供服务,因此我只需要在该主机中缓存服务调用的结果。具有相同用户 ID 的新请求不得从缓存中获取值。我浏览了文档,但找不到关于它是如何完成的参考资料。可能是我看错地方了吗?
    • @SwarangaSarma 你最后用了什么解决方案?我怀疑 ehCache 是否提供基于请求范围的缓存。
    【解决方案4】:

    您可以使用 Spring Cache 注释并创建自己的 CacheManager 缓存在请求范围内。或者你可以使用我写的:https://github.com/rinoto/spring-request-cache

    【讨论】:

    • 您的 README.md 有一个警告,要求清除每个请求的缓存。难道不能简单地使 CacheManager bean 本身请求范围,以便每个请求都会获得一个新实例吗?我什至不确定在这种情况下您是否需要编写自定义实现。毕竟,您将对象存储在 ThreadLocal 中,这正是请求范围的作用。
    猜你喜欢
    • 2020-12-13
    • 2017-08-07
    • 2013-09-15
    • 2010-10-11
    • 2010-10-13
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    相关资源
    最近更新 更多