Spring缓存与项目/实体集合

43

我正在使用Spring Cache,在那里我传递一个键的集合,然后返回一个实体列表。我希望缓存框架能够理解返回列表中的每个元素都应该与相应的代码一起缓存。目前,似乎整个列表是关键字,如果在后续调用中缺少一个键,则会尝试重新加载整个集合。

@Override
@Cacheable(value = "countries")
public List<Country> getAll(List<String>codes) {
    return countryDao.findAllInCodes(codes);
}
另一个可能性是返回一个映射表,同样我希望缓存足够智能,只查询以前从未查询过的项,并用其键缓存每个项。

另一个可能的情况是返回一个 Map(映射表)。同样地,我希望这个缓存可以足够智能,只查询那些之前从未被查询过的项目,并把每个项目和它的键都缓存起来。

@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可以实现这个吗?


1
为什么不使用JPA的第二级缓存?与JPA或缓存库(如EhCache)相比,Spring缓存在缓存方面真的很无助且容易出错。 - davidxxx
1
据我所知,Spring Cache只是一个抽象层,EhCache可以作为其实现。区别在于它消除了除注释之外的任何缓存逻辑编写的需求。如果使用JPA,我该如何编写上面的代码呢? - Charbel
1
@Charbel,你有解决这个问题的办法吗?我也遇到了同样的问题。 - Vishal Kawade
@VishalKawade 没有,有一段时间我想编写一个扩展程序并提交给 Spring Source,但从未开始。 - Charbel
4个回答

33
实际上,即使使用Spring的缓存抽象,但这并非开箱即用(OOTB)。基本上,您必须自定义Spring的缓存基础结构(下面将进一步解释)。 默认情况下Spring的缓存基础结构将使用整个@Cacheable方法参数作为缓存“键”,如此处所述。当然,您还可以使用SpEL表达式或自定义KeyGenerator实现来自定义键解析,如此处所述。

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

为此,您需要一个自定义的SpringCacheManager实现(取决于您的缓存策略/提供程序)和Cache接口。

注意:具有讽刺意味的是,这将是我第三次回答几乎相同的问题,首先是here,然后是here,现在是here,:-)。无论如何...

我已经更新/清理了我的示例(稍微)为这篇文章。
请注意,我的示例扩展并自定义Spring Framework中提供的ConcurrentMapCacheManager
理论上,您可以扩展/定制任何CacheManager实现,比如Spring Data Redis中的Redis(这里)源代码),或Spring Data GemFire中的Pivotal GemFire CacheManager(这里)源代码)。Pivotal GemFire的开源版本是Apache Geode,它有一个相应的Spring Data Geode项目(缓存管理器的源代码),与SD GemFire基本相同。当然,您也可以将此技术应用于其他缓存提供程序... Hazelcast、Ehcache等。
然而,实际工作的核心由Spring缓存接口的自定义实现(或更具体地说,基类)处理。

无论如何,希望从我的示例中,您能够找出在应用程序中满足缓存需求所需做的事情。

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

希望这可以帮到你!

祝好, 约翰


谢谢@John,我会看一下并尝试根据我的需求实现它! - Charbel
谢谢@John,如果我理解正确的话,如果我们这样做,我们需要为每个缓存提供程序(Redis、ehCache)定制Spring的CacheManager实现? - alchn
基本上,我提议扩展每个缓存提供程序的CacheManagerCache实现(例如Redis或ehCache等)。但是,我也可以想象使用AOP引入更常见的解决方案,甚至只需使用一个简单可重用的CacheManager/Cache装饰器实现来包装现有的缓存提供程序实现(即Redis或ehCache),以处理可缓存元素的集合。继续... - John Blum
1
总之,如果可能的话,在某些使用情况下,通过扩展利用底层缓存提供程序的优势比委派更好。当然,选择权在你手中,这只是一个思路。如果您需要更具体的示例,我可以稍后找时间演示一下,以便说明我的意思是否不太清楚。请告诉我。希望这有所帮助。干杯! - John Blum
2
你好,我们如何实现部分缓存未命中?例如,如果 fatorial({1,2,3}) 只有 1 和 3 不在缓存中,我们希望它只计算 1 和 3。我无法想出如何修改你的示例代码。谢谢! - ch271828n
显示剩余2条评论

4
使用 @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) {    

请查看:

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


2
调用查找程序时,这个东西怎么可能会命中缓存呢? - Stefano L

0
我找到了两种本地解决方案来使用复杂的集合值作为缓存键。
第一种方法是使用计算字符串作为缓存键:
    @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 parameter  
 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);

        }
}

注意1:此方法允许将本地键缓存符号与自定义包装器组合使用。与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())}"

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

注意2: 此方法允许在Spring实体中使用Jackson视图,但在某些情况下需要@JsonIgnoreProperties({"hibernateLazyInitializer"})注释。

最后,此方法的Spring缓存跟踪结果如下:

计算缓存密钥'[findWords, [[""],["0"],[""],[""],[""],[""],["brazil"],["on"],["false"]]]'用于 操作Builder[public java.util.List package.relationalDatabase.services.myClass.find(javax.servlet.http.HttpServletRequest)] caches=[myClass] | key='{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='#result == null' | sync='false'

另一方面,建议使用字符串哈希函数来压缩结果键值。

为避免在JAR包生命周期中出现T()函数的问题,最好创建一个bean:

@Bean
    KeySerializationComponent keySerializationComponent() {
        return new KeySerializationComponent();
    }

并使用以下方式调用:

@Cacheable(value = "Document", key = "{#root.methodName,#size,@keySerializationComponent.getSpringCacheKey(#ids)}", unless = "#result == null")

-1
为什么不将您的列表缓存为字符串?
@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]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接