带有项目/实体集合的 Spring Cache

Posted

技术标签:

【中文标题】带有项目/实体集合的 Spring Cache【英文标题】:Spring Cache with collection of items/entities 【发布时间】:2017-11-15 16:14:16 【问题描述】:

我正在使用 Spring Cache,我传入一个键集合,返回一个实体列表。我想让缓存框架了解返回列表中的每个元素都将使用相应的代码进行缓存。目前,似乎键是整个列表,如果我在后续调用中缺少键,它会尝试再次重新加载整个集合。

@Override
@Cacheable(value = "countries")
public List<Country> getAll(List<String>codes) 
    return countryDao.findAllInCodes(codes);

另一种可能性是返回是一个映射,同样我希望缓存足够智能,只查询以前从未查询过的项目,也可以用它的键缓存每个项目。

@Override
@Cacheable(value = "countries")
public Map<String,Country> getAllByCode(List<String>codes) 
    return countryDao.findAllInCodes(codes);

假设国家类如下所示:

class Country
  String code; 
  String fullName;
  long id;

... // getters setters constructurs etc.. 

Spring Cache 可以做到这一点吗?

【问题讨论】:

为什么不使用二级JPA?与JPA或Cache库作为EhCache相比,Spring缓存确实很无助且容易出错。 @davidxxx 据我了解,Spring Cache 只是一个抽象,EhCache 可以是下面的实现。不同之处在于它无需编写注释之外的任何缓存逻辑。我将如何使用 JPA 编写上面的代码? @Charbel 你有什么解决办法吗,我也有同样的问题 @VishalKawade 不,我没有,在某些时候我以为我写了一个扩展并将其提交给 Spring Source,但一直没有时间去做 【参考方案1】:

事实上,即使使用 Spring's Caching Abstraction,它也是可能的,但不是开箱即用 (OOTB)。本质上,您必须自定义 Spring 的 缓存基础设施(下文进一步解释)

默认Spring 的 缓存基础设施使用整个@Cacheable 方法参数作为缓存“键”,如here 所述。当然,您也可以使用 SpEL 表达式 或自定义 KeyGenerator 实现来自定义密钥分辨率,如 here 所述。

不过,不会参数参数的集合或数组以及@Cacheable方法的返回值分解为单独的缓存条目(即基于数组/集合或映射的键/值对)。

为此,您需要 Spring 的 CacheManager(取决于您的缓存策略/提供者)和 Cache 接口的自定义实现。

注意:具有讽刺意味的是,这将是我第三次回答几乎相同的问题,首先是here,然后是here,现在是这里,:-)。总之……

我已经更新/清理了my example(有点)这个帖子。

注意my example extends and customizes ConcurrentMapCacheManagerSpring 框架 本身中提供。

理论上,您可以扩展/自定义任何 CacheManager 实现,例如 Spring Data Redis 中的 Redis、here (source) 或 Pivotal GemFire 的 CacheManagerSpring Data GemFire 中,here (source)。 Pivotal GemFire 的开源版本是 Apache Geode,其中有一个对应的 Spring Data Geode 项目,(source for CacheManager in Spring Data Geode,与 SD GemFire 基本相同)。当然,您可以将此技术应用于其他缓存提供程序... Hazelcast、Ehcache 等。

然而,这项工作的真正核心是由 Spring 的 Cache 接口的 custom implementation(或者更具体地说,base class)处理的。

无论如何,希望通过my example,您将能够弄清楚您需要在应用程序中执行哪些操作才能满足应用程序的缓存要求。

此外,您可以应用相同的方法来处理Maps,但我将把它留给您作为练习,;-)。

希望这会有所帮助!

干杯, 约翰

【讨论】:

谢谢@John,我会看看这个并尝试根据我的需要实现它! 感谢@John,如果我理解正确的话,如果我们要这样做,我们需要为我们可能使用的每一个缓存提供程序(Redis,ehCache)自定义实现 Spring 的 CacheManager?跨度> 基本上,扩展每个缓存提供程序CacheManagerCache 实现(例如Redis 或ehCache 等)是我的建议。但是,我也可以想象,可以使用 AOP 引入更常见的解决方案,或者甚至只是一个简单的可重用 CacheManager/Cache 装饰器实现,包装现有的缓存提供程序实现(即 Redis 或 ehCache)来处理集合的可缓存元素。继续... 虽然,这种方法效率较低,因为(例如)通用的CollectionHandlingCache 实现(将包装RedisCache),由(例如)CollectionHandlingCacheManager(包装RedisCacheManager),将遍历由@Cacheable 方法返回的元素集合,然后为集合中的每个元素调用Cache.put(key, value)(或者Cache.putIfAbsent(key, value))。这比缓存提供程序首先处理元素集合的效率要低。继续... 嗨,我们如何实现部分缓存未命中?例如如果fatorial(1,2,3) 并且只有 1 和 3 不在缓存中,我们希望它实际上只计算 1 和 3。我想不出如何修改示例代码的方法。谢谢!【参考方案2】:

使用@CachePut 和辅助方法,您可以像这样非常简单地实现它:

public List<Country> getAllByCode(List<String>codes) 
    return countryDao.findAllInCodes(codes);


public void preloadCache(List<String>codes) 
    List<Country> allCountries = getAllByCode(codes);
    for (Country country : allCountries) 
        cacheCountry(country);
    


@CachePut
public Country cacheCountry(Country country) 
    return country;

注意

这只会将值添加到缓存中,而不会删除旧值。在添加新值之前,您可以轻松地进行缓存逐出

选项 2

有一个建议让它像这样工作:

@CollectionCacheable
public List<Country> getAllByCode(List<String>codes)     

见:

https://github.com/spring-projects/spring-framework/issues/23221 https://github.com/neiser/spring-collection-cacheable

如果您不耐烦,请从 GitHub 获取代码并本地集成

【讨论】:

那么在调用 finder 时这会如何命中缓存?【参考方案3】:

我发现两种原生解决方法可以使用复杂的集合值作为缓存键。 第一种方法是使用计算字符串作为缓存键:

    @Cacheable(value = "Words", key = "#root.methodName, #a1", unless = "#result == null")
    //or
    @Cacheable(value = "Words", key = "#root.methodName, #p1", unless = "#result == null")
    //or
    @Cacheable(value = "Words", key = "#root.methodName, #precomputedString", unless = "#result == null")
    public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) 

    

为了调用这个方法服务如下:

//use your own complex object collection to string mapping as a second parammeter  
 service.findWords(request.getParameterMap().values(),request.getParameterMap()
                        .values()
                        .stream()
                    .map(strings -> Arrays.stream(strings)
                            .collect(Collectors.joining(",")))
                    .collect(Collectors.joining(","));)

以及第二种方法(我喜欢的形式):

@Cacheable(value = "Edges", key = "#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())", unless = "#result == null")
    public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) 
        
        

其中 package.relationalDatabase.utils.Functions getSpringCacheKey 是自己创建的函数,如下:

public static String getSpringCacheKey(Object o) throws JsonProcessingException 

        ObjectMapper objectMapper = new ObjectMapper();
        boolean isSpringEntity = o.getClass().getAnnotation(javax.persistence.Entity.class) != null;
        if (isSpringEntity) 
            return objectMapper.writerWithView(JSONViews.Simple.class).writeValueAsString(o);
         else 
            return objectMapper.writeValueAsString(o);

        

注意 I:此方法允许将本机密钥缓存表示法与自定义包装器结合使用。与 Spring 缓存的 keyGenerator 属性不同,该属性不允许键注释(它们是互斥的)并且需要创建 CustomKeyGenerator

@Cacheable(value = "Edges", unless = "#result == null", keyGenerator = "CustomKeyGenerator")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey)            
            
////////
public class CustomKeyGenerator implements KeyGenerator 
    Object generate(Object target, Method method, Object... params)


并为每个复杂的集合键创建一个返回包装器。例如:

@Override
public Object generate(Object target, Method method, Object... params) 
    
    
    if(params[0] instanceof Collection)
    //do something 
    if(params[0] instanceof Map)
    //do something 
    if(params[0] instanceof HttpServletRequest)
    //do something      

因此,建议的方法允许:

//note #request.getParameterMap().values()
@Cacheable(value = "Edges", key = "#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())"
    
//note #request.getParameterMap().keySet()
@Cacheable(value = "Edges", key = "#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().keySet())"

无需为每个集合更新方法。

注意二:这种方法允许对 spring 实体使用 jackson 视图,但在某些情况下需要 @JsonIgnoreProperties("hibernateLazyInitializer") 注释。

最后这个方法的spring cache的trace结果如下:

计算出的缓存键 '[findWords, [[""],["0"],[""],[""],[""],[""],["巴西"],["on"],["false"]] ]' 为了 操作生成器[public java.util.List package.relationalDatabase.services.myClass.find(javax.servlet.http.HttpServletRequest)] 缓存=[myClass] |键='#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())' |密钥生成器='' |缓存管理器='' |缓存解析器='' |条件='' |除非='#result == null' |同步='假'

另一方面,建议使用字符串散列函数来压缩生成的键值。

【讨论】:

【参考方案4】:

为什么不将您的列表缓存为字符串?

@Cacheable(value = "my-cache-bucket:my-id-parameters", key = "#id, #parameters")
getMy(UUID id, String parameters)  ... 

像这样使用:

getMy(randomUUID(), parametersList.toString());

您的缓存键如下所示:

"my-cache-bucket:my-id-parameters::3ce42cd9-99d4-1d6e-a657-832b4a982c72,[parameterValue1,parameter-value2]

【讨论】:

以上是关于带有项目/实体集合的 Spring Cache的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Hibernate 在内部保存带有集合的实体?

如何使用 Spring Data 中相关实体的集合?

带有来自本机查询的参数的 Spring 实体动态计算字段

如何过滤嵌套集合实体框架对象?

Spring Boot - 不同数据库的相同存储库和相同实体

无法使用带有 InhertanceType.JOINED 的 Spring JPA 持久化实体