10

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

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

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

我的要求如下:

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

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

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

任何指针?

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

4

4 回答 4

7

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

//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();
}
于 2020-01-14T14:26:32.383 回答
3

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

具有简单 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;
}
于 2019-11-05T20:17:20.577 回答
1

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

于 2013-02-06T21:29:48.107 回答
1

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

于 2015-04-03T16:53:44.890 回答