Spring 在方法中更新读取值时缓存更新

Spring cache updates when read value updated in method

我正面临 spring 缓存问题,如下所述。检查是否有人遇到类似问题或任何可用的解决方法。我正在为简单用例使用通用缓存。

Spring 下面的缓存代码从数据库读取并缓存:

    @Cacheable("all-items")
    public List<ItemData> getAllItems() {
       //DB fetch here
    }

另一种方法是从缓存中读取:

   public List<ItemData> getFilterData(String filter) {
      List<ItemData> items = templateItemRepo.getAllItems();
      // more code here to apply filter
      staticItems.forEach(x -> {
            items.removeIf(i -> i.getKey().equals(x)); //after this line, cache gets updated instead of maintaining its original data
      });
   }

在方法中本地修改从缓存中读取的项目后,令人惊讶的是缓存也得到了更新。

您希望在这种情况下发生什么?

您是否期望 ListItemData 个对象(在本例中为缓存数据)不会被修改?

如果没有你的确切配置(和代码),肯定地说是不准确的,但根据你的描述,很容易推测这里发生了什么。

首先,您说您正在使用“通用缓存”。好吧,在 Spring 中,当您没有显式声明和配置特定的缓存提供程序时,例如 Redis、Hazelcast、Apache Geode (GemFire) 或其他方式(请参阅 here or here, and specifically, here), then what Spring gives you is a ConcurrentMap cache implementation, by default (a ConcurrentHashMap to be precise; see here),其中存储缓存值in-memory,在与您的 Spring [Boot] 应用程序相同的 JVM 进程中。

其次,假设 templateItemRepo 指的是包含您的 @Cacheable getAllItems() 方法的存储库类型,那么第一次 getAllItems() 是从 Spring 容器,比如你的类型包含 getFilterData(:String) 方法,@Cacheable getAllItems() Repository 方法将被执行并且 returned List of ItemData 对象将被缓存在一个简单的键下。

NOTE: A simple key is used in this case since the @Cacheable getAllItems() Repository method has no parameters. See documentation for more details.

此后,每次后续调用 @Cacheable getAllItems() Repository 方法将始终 return 相同 ,缓存 List给定底层 Cache 实现的 ItemData 个对象是 ConcurrentMap.

例如,您的“all-items”Cache 包含以下单个缓存条目 (key/value):

<simple-key> : List<ItemData>

@Cacheable getAllItems() 存储库方法的所有后续调用始终使用相同的简单键,并且始终 return 对 List 的完全相同的对象引用=11=]存储在“all-items”Cache.

使用 Redis、Hazelcast 或 Apache Geode 等缓存提供程序可能会导致不同的行为,因为这些数据存储以不同方式处理数据。

例如,Apache Geode 能够配置 copy-on-read 语义,意思是当值(例如 List of ItemData)从“all-items”Cache 中读取 (cache.get(key)),您得到的是 List 及其所有 [=] 的真实(深度)“副本” 11=] 个对象,即使“all-items”Cache 是本地的,in-memory 与您的 Spring [Boot] 应用程序相同的 JVM 进程。

如果您的缓存提供程序用于 client/server 拓扑,那么 List 及其内容(ItemData 对象)将有必要在客户端和服务器之间序列化,并且再一次,您将得到的是 List 的“副本”(假设 ItemData 类型在某种程度上是可序列化的)。

每个缓存提供商将在这方面提供不同的功能,而其他可能显着影响数据的因素是逐出和过期策略。

如果您想在简单的“通用”缓存安排中实现与缓存提供程序类似的行为,那么您需要手动制作 List 的 [深] 副本。

当然,您可以从 Spring 的缓存抽象中创建 Adapter (Wrapper) or Decorator implementations of the primary Cache (Javadoc) and CacheManager (Javadoc) 接口,它可以为您处理此复制。 Wrapper 类型将包装来自实际缓存提供程序的实际 CacheCacheManager 接口,即使使用 ConcurrentMap 支持的简单 Cache 实现也是如此。

但是,在使用复杂的缓存提供程序时,最好让提供程序处理复制、序列化等操作。不过,对于测试简单的情况,上述方法可能很有用。