代码之家  ›  专栏  ›  技术社区  ›  Garret Wilson

弹簧缓存绕过缓存方法?

  •  0
  • Garret Wilson  · 技术社区  · 2 年前

    Spring Cache发生了一些奇怪的事情;也许有人能帮我知道该找什么。

    我有两个Spring Boot应用程序:1)“web应用程序”,使用带有Thymelaf的Spring MVC;2)与数据库对话的“后端服务”。后端服务向web应用程序公开了一个RESTful接口,该接口在Thymelaf处理过程中调用(而不是使用Ajax从页面异步调用)。

    Thymeleaf -> web app -> (REST over HTTP) -> backend service -> database
    

    我将Spring Cache与 Caffeine 具有 Coffee Boots 以允许我在每个缓存的基础上配置驱逐。以下是Maven POM的一部分:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <dependency>
      <groupId>com.github.ben-manes.caffeine</groupId>
      <artifactId>caffeine</artifactId>
    </dependency>
    
    <dependency>
      <groupId>io.github.stepio.coffee-boots</groupId>
      <artifactId>coffee-boots</artifactId>
    </dependency>
    

    我使用 @EnableCaching 要在我的web应用程序配置中打开缓存:

    @Configuration
    @EnableCaching
    public class WebAppConfiguration {
      …
    

    我的web应用程序有 WebAppFooBarRepositoryImpl.getFooBars() ,在启用缓存的情况下:

    @Repository("fooBarRepository")
    public class WebAppFooBarRepositoryImpl implements WebAppFooBarRepository {
    
      …
    
      @Override
      @Cacheable("foobar")
      public Flux<FooBar> getFooBars() {
    
        System.out.println("CACHE MISS");
    
        return webClient.get().uri("backend-service").…
      }
    
      …
    

    请注意,此存储库方法记录 CACHE MISS 每次调用它时,它都会向后端发出请求。

    还请注意,我命名了存储库 fooBarRepository ,以便在Thymelaf中,我可以迭代 Flux<FooBar> 网页中的类似项目:

    <li th:each="foobar: ${@fooBarRepository.getFooBars.toIterable()}">[[${foobar}]]</li>
    

    然后后端服务具有 RestController 接受请求,如下所示(网站存储库将 Iterable Flux ):

    @GetMapping(path = "backend-service")
    public Iterable<FooBar> getFooBars() throws IOException {
    
      System.out.println("backend getting foobars");
    
      return getBackendFooBarRepository().getFooBars();
    }
    

    到目前为止,一切都是标准的、无聊的,缓存是唯一一件稍微与众不同的事情。我在web应用程序中添加此配置 application.yaml 在web层缓存foobar结果,每次缓存15分钟:

    coffee-boots:
      cache:
        spec:
          foobar: expireAfterWrite=15m
    

    一切就绪后,我加载网页并点击 F5 一次又一次地不断地重新装载。

    • 我明白了 缓存未命中 在网站日志中,每15分钟一次。这是意料之中的:web层应该缓存对 WebAppFooBarRepositoryImpl.getFooBars() ,并且只有当缓存超过15分钟时才实际进行调用。
    • 然而,每次重新加载页面时,我都会看到“后台获取foobar”,每次点击都会一秒钟多次 F5 !!

    这一定意味着 某物 每次重新加载页面时都调用后端服务。但是怎么做呢?通往后端服务的唯一途径是通过 WebAppFooBarRepositoryImpl.getFooBars() ,进行REST调用。如果 WebAppFooBarRepositoryImpl.getFooBars() 如果调用频率更高,则web应用程序将打印 缓存未命中 更频繁地添加到日志中。但该网站正在登录 缓存未命中 每15分钟一次,正如我所期望的那样,无论我多久重新加载一次页面。

    记住Spring可能正在为我的 WebAppFooBarRepositoryImpl 例如,我认为拦截调用的缓存逻辑可能会跳过日志输出,但仍然调用后端。但我从未听说过Spring代理解析代码 在…内 方法,并且只跳过其中的一部分;这毫无意义。

    目前我无法解释这种奇怪的行为。如果有人有想法,请告诉我。我希望很快它会出现在我的脑海中,我会说,“啊,我不小心做了这样那样的事”,但目前,我真的无法解释这一点。

    1 回复  |  直到 2 年前
        1
  •  1
  •   Garret Wilson    2 年前

    我希望它很快就会出现在我的脑海中,我会说,“啊,我不小心做了这样那样的事。”

    哈哈哈哈事实上,它只是突然出现在我的脑海中;我想我很清楚发生了什么。

    我认为Spring Cache没有缓存我的 FooBar s、 而是 缓存我的 Flux<FooBar> 事实上,这正是我要求它做的。这意味着它只会产生一个新的 通量<FooBar> 每15分钟。但对于在此期间的呼吁,Thymelaf 迭代缓存的 通量<FooBar> 例子 ,它对后端服务进行另一个REST调用,实际上绕过了 WebAppFooBarRepositoryImpl.getFooBars() 方法

    我想我没有仔细查看该方法返回的内容,并认为Spring知道我想要缓存什么。

    啊!

    这里唯一令人惊讶的是,我不知道如果你坚持 通量<FooBar> 来自 WebClient ,它会在你每次打电话时发出新的请求 Flux.toIterable() 。但这是有道理的,因为可迭代项通常意味着要迭代多次。

    更新: 我现在已经进行了测试,以验证这似乎确实是原因。修复方法是更改存储库,以返回可以缓存的、已经包含对象的东西(即物化容器),例如 List<FooBar> ,而不是表示将来提供对象的东西:

    @Repository("fooBarRepository")
    public class WebAppFooBarRepositoryImpl implements WebAppFooBarRepository {
    
      …
    
      @Override
      @Cacheable("foobar")
      public List<FooBar> getFooBars() {
        return webClient.get().uri("backend-service")
            ….collectList().block()
      }
    
      …
    

    与现有 网络客户端 代码,快速解决方法是添加 ….collectList().block() 到现有 通量<FooBar> 返回,如上面的片段所示。然后,我将花一些时间重新检查这个存储库如何返回信息的整个方法,现在需要考虑缓存。