给定请求的 Spring 缓存

Swa*_*rma 8 java spring caching spring-mvc

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

public interface SubscriptionService
{
    public String getSubscriptionIDForUSer(String userID);
}
Run Code Online (Sandbox Code Playgroud)

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 的新请求不得从缓存中获取值。我已经浏览了文档,但找不到关于它是如何完成的参考。可能是我看错了地方?

nnd*_*dru 16

我想提出另一种解决方案,该解决方案比@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();
}
Run Code Online (Sandbox Code Playgroud)

并可用于:

@Cacheable(cacheManager = "cacheManager", cacheNames = "default")
public SomeCachedObject getCachedObject() {
    return new SomeCachedObject();
}
Run Code Online (Sandbox Code Playgroud)


Dmi*_*try 7

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

具有简单 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();
    }
 
}
Run Code Online (Sandbox Code Playgroud)

然后将其设置为RequestScoped:

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

用法:

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

更新:

一段时间后,我发现我之前的解决方案与 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();
}
Run Code Online (Sandbox Code Playgroud)

}

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

@Bean
public CacheManager requestScopedCacheManager() {
    return new RequestScopedCacheManager();
}
Run Code Online (Sandbox Code Playgroud)

用法:

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


Cod*_*imp 1

你立刻就会想到EHCache ,或者你甚至可以推出自己的解决方案来将结果缓存在服务层中。这里可能有十亿种缓存选项。选择取决于几个因素,例如您是否需要超时值,或者您是否要手动清理缓存。您是否需要分布式缓存,例如您有一个分布在多个应用程序服务器之间的无状态 REST 应用程序。您需要一些坚固耐用的东西,可以在崩溃或重新启动时幸存下来。