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
相关产品推荐
相关产品推荐

