【问题标题】:Spring Caching not working for findAll methodSpring缓存不适用于findAll方法
【发布时间】:2018-05-27 22:32:00
【问题描述】:

我最近开始着手缓存方法的结果。我正在使用@Cacheable 和@CachePut 来实现所需的功能。

但不知何故,保存操作并未更新 findAll 方法的缓存。下面是相同的代码sn-p:

@RestController
@RequestMapping(path = "/test/v1")
@CacheConfig(cacheNames = "persons")
public class CacheDemoController {

    @Autowired
    private PersonRepository personRepository;

    @Cacheable
    @RequestMapping(method = RequestMethod.GET, path="/persons/{id}")
    public Person getPerson(@PathVariable(name = "id") long id) {
        return this.personRepository.findById(id);
    }

    @Cacheable
    @RequestMapping(method = RequestMethod.GET, path="/persons")
    public List<Person> findAll() {
        return this.personRepository.findAll();
    }

    @CachePut
    @RequestMapping(method = RequestMethod.POST, path="/save")
    public Person savePerson(@RequestBody Person person) {
        return this.personRepository.save(person);
    }
}

对于 findAll 方法的第一次调用,它会将结果存储在“persons”缓存中,并且对于所有后续调用,即使在两者之间执行了 save() 操作,它也会返回相同的结果。

我对缓存很陌生,所以任何关于这方面的建议都会有很大帮助。

谢谢!

【问题讨论】:

    标签: caching spring-cache


    【解决方案1】:

    所以,关于你的 UC 和查看上面的代码,我想到了一些事情。

    1. 首先,我不喜欢在应用程序的 UI 或数据层启用缓存的用户,尽管它在数据层(例如 DAO 或 Repos)更有意义。缓存,如事务管理、安全等,是服务级别的问题,因此属于服务层 IMO,您的应用程序由以下部分组成:[Web|Mobile|CLI]+ UI -> Service -> DAO(又名 Repo) .在服务层中启用缓存的优点是在您的应用程序/系统架构中更可重用。例如,除了 Web 之外,还为移动应用程序客户端提供服务。您的 Web 层控制器不一定与处理移动应用客户端的控制器相同。

    2. 我鼓励您阅读Spring's Cache Abstraction 上的核心Spring 框架参考文档 中的章节。仅供参考,Spring 的缓存抽象,就像 TX 管理一样,深深植根于 Spring's AOP support。但是,为了您的目的,让我们将您的 Spring Web MVC 控制器(即CacheDemoController)分解一下,了解正在发生的事情。

    所以,您有一个 findAll() 方法,您正在为其缓存结果。

    警告:另外,我通常不建议您缓存Repository.findAll() 调用的结果,尤其是在生产环境中!虽然在给定有限的数据集的情况下,这在本地可能工作得很好,但 CrudRepository.findAll() method 返回 all 会导致该特定数据存储(例如 RDBMS 中的 Person 表)中的数据结构默认情况下对象/数据类型(例如Person),除非您使用分页或对返回的结果集进行一些限制。说到缓存,总是想着对相对不频繁的数据变化进行高度重用;这些都是很好的缓存候选者。

    鉴于您的 Controller 的 findAll() 方法具有 NO 方法参数,Spring 将确定用于缓存 findAll() 方法的返回值的“默认”键(即List&lt;Person)。

    提示:有关详细信息,请参阅 Spring 的“Default Key Generation”文档。

    注意:在 Spring 中,与一般的缓存一样,Key/Value 存储(如 java.util.Map)是 Spring 的 Cache 概念的主要实现.但是,并非所有“缓存提供程序”都是平等的(例如,Redis 与 java.util.concurrent.ConcurrentHashMap)。

    调用findAll()Controller方法后,你的缓存会有...

    KEY    | VALUE
    ------------------------
    abc123 | List of People
    

    注意:缓存不会将列表中的每个 Person 单独存储为单独的缓存条目。这不是 Spring 的缓存抽象 中方法级缓存的工作方式,至少默认情况下不是这样。但是,它是possible

    然后,假设接下来会调用 Controller 的可缓存 getPerson(id:long) 方法。嗯,这个方法包含一个参数,Person's ID。当控制器getPerson(..) 方法被调用并且Spring 试图在缓存。例如,假设使用controller.getPerson(1) 调用该方法。除非缓存中不存在键为 1 的缓存条目,即使 Person (1) 在映射到键 abc123 的列表中也是如此。因此,Spring 不会在列表中找到 Person1 并将其返回,因此,此操作会导致缓存未命中。当方法返回值时(ID 为 1 的Person)将被缓存。但是,缓存现在看起来像这样......

    KEY    | VALUE
    ------------------------
    abc123 | List of People
    1      | Person(1)
    

    最后,用户调用控制器的savePerson(:Person) 方法。同样,savePerson(:Person) 控制器方法的参数值用作键(即“Person”对象)。假设该方法是这样调用的,controller.savePerson(person(1))。好吧,CachePut 在方法返回时发生,因此 Person 1 的现有缓存条目不会更新,因为“键”不同,因此创建了一个新的缓存条目,您的缓存再次看起来像这样.. .

    KEY       | VALUE
    ---------------------------
    abc123    | List of People
    1         | Person(1)
    Person(1) | Person(1)
    

    这些都不是你想要或打算发生的。

    那么,你如何解决这个问题。好吧,正如我在上面的警告中提到的,您可能不应该缓存从操作返回的整个值集合。而且,即使您这样做了,您也需要扩展 Spring 的缓存基础架构 OOTB 以处理 Collection 返回类型,以根据某个键将 Collection 的元素分解为单独的缓存条目。这关系密切。

    不过,您可以在 getPerson(id:long)savePerson(:Person) 控制器方法之间添加更好的协调性。基本上,您需要更具体地了解savePerson(:Person) 方法的密钥。幸运的是,Spring 允许您通过提供自定义 KeyGenerator 实现或简单地使用 SpEL 来“指定”密钥。同样,see the docs 了解更多详情。

    所以你的例子可以这样修改......

    @CachePut(key = "#result.id"
    @RequestMapping(method = RequestMethod.POST, path="/save")
    public Person savePerson(@RequestBody Person person) {
        return this.personRepository.save(person);
    }
    

    注意@CachePut 注释,其中key 属性包含SpEL 表达式。在这种情况下,我指出这个 Controller savePerson(:Person) 方法的缓存“key”应该是返回值(即“#result”)或 Person 对象的 ID,从而匹配 Controller getPerson(id:long) 方法的 key,这然后将更新Person 的单个缓存条目,键控在Person's ID...

    KEY       | VALUE
    ---------------------------
    abc123    | List of People
    1         | Person(1)
    

    尽管如此,这不会处理 findAll() 方法,但它适用于 getPerson(id)savePerson(:Person)。同样,请参阅我的 answers 到关于 Collection 值作为 Spring 缓存基础结构中的返回类型以及如何正确处理它们的帖子。但小心点!将整个值集合缓存为单个缓存条目可能会对应用程序的内存占用造成严重破坏,从而导致 OOME。在这种情况下,您肯定需要“调整”底层缓存提供程序(驱逐、过期、压缩等),然后再将大量数据放入缓存中,特别是在 UI 层,其中可能同时发生数千个请求,然后“并发”也成为一个因素!请参阅 sync capabilities 上的 Spring 文档。

    无论如何,希望这有助于您理解缓存,尤其是 Spring 以及一般的缓存。

    干杯, -约翰

    【讨论】:

    • 谢谢约翰。我用这篇文章来实现缓存,效果很好。这很有帮助。
    猜你喜欢
    • 2021-01-25
    • 2017-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-10
    • 2019-05-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多