You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cacheable:单条与批量查询的缓存对象复用问题及优化

Spring缓存抽象(CaffeineCache)的缓存键值问题

我正在使用Spring的缓存抽象(CaffeineCache实现),我的JpaRepository中有两个方法:

Article findByArticleId(final Long articleId);
List<Article> findByArticleIdIn(Set<Long> articleIds);

我为它们添加缓存注解如下:

@Cacheable(value = "article_cache", key = "#articleId")
Article findByArticleId(final Long articleId);

@Cacheable(value = "article_cache", key = "#articleIds")
List<Article> findByArticleIdIn(Set<Long> articleIds);

请问:Spring是否能智能地仅缓存单个Article对象,还是会生成不同类型的键值对?当两个方法被调用且包含部分相同ID时,缓存会出现如下不理想的情况:

{
   "[1,2,3,4,5]": [Article@1, Article@2,..],
   "[2,3,4]": [Article@2, Article@3,..],
   "1": Article@1,
   "2": Article@2,
   ...
}

还是会呈现我期望的如下状态:

{
   "1": Article@1,
   "2": Article@2,
   "3": Article@3,
   "4": Article@4,
   ...
}

由于Article对象体积较大,若为第一种情况,如何在不实现自定义缓存的前提下达成第二种效果?


回答

一、Spring缓存的默认行为

Spring缓存抽象不会自动拆分批量查询的结果做单对象缓存,默认会生成你提到的第一种不理想的缓存结构:

  • 调用findByArticleId(1)时,以"1"为键缓存单个Article对象
  • 调用findByArticleIdIn([1,2,3])时,以"[1,2,3]"为键缓存整个List<Article>集合
    两者的缓存键完全独立,不会自动复用或拆分批量结果到单对象缓存中。

二、不自定义缓存的优化方案

要实现只缓存单个Article对象的目标,可以通过以下两种方式改造,无需自定义缓存实现:

1. Service层封装逻辑,手动复用缓存

在Service层对批量查询做封装,先从缓存取已有数据,只查未缓存的ID,再把结果存入缓存:

// Service层示例
@Cacheable(value = "article_cache", key = "#articleId")
public Article getArticleById(Long articleId) {
    return articleRepository.findByArticleId(articleId);
}

public List<Article> getArticlesByIds(Set<Long> articleIds) {
    // 先从缓存获取已存在的对象
    Map<Long, Article> cachedArticles = articleIds.stream()
        .collect(Collectors.toMap(
            id -> id,
            id -> cacheManager.getCache("article_cache").get(id, Article.class),
            (a, b) -> a
        ));
    
    // 筛选出未缓存的ID
    Set<Long> uncachedIds = articleIds.stream()
        .filter(id -> cachedArticles.get(id) == null)
        .collect(Collectors.toSet());
    
    // 批量查询未缓存的数据,并存入缓存
    if (!uncachedIds.isEmpty()) {
        List<Article> dbArticles = articleRepository.findByArticleIdIn(uncachedIds);
        dbArticles.forEach(article -> cacheManager.getCache("article_cache").put(article.getArticleId(), article));
        cachedArticles.putAll(dbArticles.stream().collect(Collectors.toMap(Article::getArticleId, a -> a)));
    }
    
    // 按原始ID顺序返回结果
    return articleIds.stream()
        .map(cachedArticles::get)
        .collect(Collectors.toList());
}

这种方式的核心是尽量复用已有缓存,减少数据库查询,同时保证所有缓存都是单个Article对象,后续单查或批量查都能直接复用。

2. 用@CachePut拆分批量结果到单对象缓存

如果不想写太多Service逻辑,可以修改Repository的批量查询注解,用SpEL表达式把每个返回对象单独存入缓存:

// 修改Repository的批量查询方法
@CachePut(value = "article_cache", key = "#result.![articleId]")
List<Article> findByArticleIdIn(Set<Long> articleIds);

注意:#result.![articleId]是SpEL语法,会遍历返回的List<Article>,把每个对象以自身articleId为键存入缓存。但这种方式每次调用批量查询都会执行数据库查询,不会先读缓存,适合数据更新频繁、批量查询场景少的情况。


内容的提问来源于stack exchange,提问作者Jerome

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 09:58:18