【问题标题】:How to avoid changing object in cache before saving Spring Cache如何避免在保存 Spring Cache 之前更改缓存中的对象
【发布时间】:2023-02-25 09:47:08
【问题描述】:

我想马上用一个例子来展示。有这样一个存储库:

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

String USER_CACHE = "users";

@Override
@CachePut(value = USER_CACHE, key = "#user.email", unless = "#result == null")
<S extends User> S save(S user);

@Override
@CacheEvict(value = USER_CACHE, key = "#user.email")
void delete(User user);

@Cacheable(value = USER_CACHE, key = "#email", unless = "#result == null")
User findByEmailIgnoreCase(String email);

}

并且有这样一种服务,可以保存用户的更改,并向邮件发送确认码:

@Service
public class UserServiceImpl implements UserService, UserDetailsService {

private final UserRepository userRepository;

@Override
public User getUserByEmail(String email) {
    return userRepository.findByEmailIgnoreCase(email);
}

@Override
@Transactional(isolation = Isolation.SERIALIZABLE)
public void createAppUser(RegistrationAppRequestDto registrationRequest) throws EmailSendingException {
    User user = getUserByEmail(registrationRequest.getEmail());

    user.setPassword(registrationRequest.getPassword());
    user.setApp(true);
    user.setActivated(false);
    user.setActivationCode(UUID.randomUUID().toString());
    user.setLastVisit(LocalDateTime.now());

    if (Strings.isEmpty(user.getImg())) {
        user.setImg(DEFAULT_IMG);
    }

    mailSender.sendWelcomeMessage(user);    
    userRepository.save(user);
 }
}

问题是如果出现错误(例如,当向邮件发送消息时),对该用户所做的更改将保留在缓存中,并且这些更改不会进入数据库(这是正确的).有处理这种情况的做法吗?或者,我可以使用对象克隆,但我认为这是一种不好的做法。我将不胜感激任何帮助。

【问题讨论】:

  • 如果您使用的是 JPA,请不要使用 Spring Cache,请为您的 JPA 提供程序(可能是 Hibernate)使用缓存集成,它将为您管理所有这些。如果您真的想使用 Spring Cache(在这种情况下我不推荐),这将是最佳实践并且会更好地集成,请使用将与事务保持一致的事务感知缓存管理器。
  • @M.Deinum 谢谢,我研究了休眠缓存的工作原理,它确实看起来像一个解决方案

标签: java spring caching spring-data-jpa spring-cache


【解决方案1】:

使用 JPA 提供者的缓存,例如 Hibernate 的 L2 缓存,可能是一种解决方案,但现在您已经将(可配置的、有条件的和可选的)缓存问题从应用程序转移到持久性提供者,这可能不是在所有情况下都可取或可移植的.

例如,当您不使用 JPA 和数据库作为持久性提供程序/后备存储,而是使用其他存储(如 Cassandra)作为您的记录系统 (SOR) 来支持某些服务时,会发生什么情况?或者,也许您正在从您的应用程序服务类调用 [基于 REST 的] Web 服务(例如 Google 地图/地理编码 API)?然后怎样呢?

您将遇到的问题之一,尤其是如果您只是使用 Spring [Boot] 提供的 "Simple" caching provider(在 Spring Framework 的缓存抽象文档中也是 described),是对缓存返回的对象的引用是缓存中存储的实际对象。

这类似于:

Map<String, User> users = new HashMap<>();

// populate Map, for example:
users.put("jonDoe", User.as("Jon Doe").active());

assertThat(users.get("jonDoe").isActive()).isTrue();

// then...
User jonDoe = users.get("jonDoe")

jonDoe.setActive(false);

assertThat(users.get("jonDoe").isActive()).isFalse();

更糟糕的是,缓存对象的(更改的)状态是 equalshashCode 方法的一部分(缓存在执行缓存操作时经常使用:get(key)put(key, value) 等),这将影响映射,并影响任何基于 Map 的缓存实现。

大多数缓存提供程序提供一个设置,例如copy-on-read(语义),当缓存值(对象)从缓存中读取或存储到缓存中时。这在基于本地的缓存中特别有用(用于“近缓存"),它可以补充基于客户端/服务器的缓存(例如 Redis)以减少访问时的网络跳数,特别是对于不经常更改的数据。

如果缓存解决方案是纯客户端/服务器(没有本地缓存​​,只是从客户端代理到服务器),这是大多数生产拓扑安排(给定缓存数据通常是“共享的”),那么修改缓存的恐惧是nil 因为从服务器(例如 Redis)访问/访问的值无论如何都需要反序列化。

如果您正在使用 Spring Boot 的“简单”(ConcurrentMap-backed)Cache 实现,也许只是为了(基于配置文件的)测试目的,那么您甚至可以配置“简单”Cache以在读取时返回复制的值:

@Configuration
@EnableCaching
class CustomCachingConfiguration {

  @Bean
  CacheManagerCustomizer cacheManagerCustomizer() {

    return cacheManager -> {
      if (cacheManager instanceof ConcurrentMapCacheManager concurrentMapCacheManager) {
        concurrentMapCacheManager.setStoreByValue(true);
      }
    };
  }

此配置特定于缓存提供程序。

使用 Apache Geode (VMware GemFire),您可以使用 GemFireCache.copyOnRead(boolean) 配置缓存。当然,在非本地/非嵌入式、仅代理、客户端/服务器拓扑中,这不是必需的。

使用 Hazelcast,特别是在嵌入式容量(一种形式近缓存),你会使用CacheConfig.setStoreByValue(boolean)

同样,Spring [Boot] 支持的许多健壮的缓存提供程序实现了缓存API,并且可以作为缓存缓存提供程序(Spring 和 Spring Boot 提供出色的 supporthere)也提供类似的配置。

这不应该影响您的任何应用程序代码,只会影响您的配置,并且它不应该依赖于应用程序层(例如:持久性)。

【讨论】:

    猜你喜欢
    • 2011-02-17
    • 1970-01-01
    • 2019-11-03
    • 2019-08-08
    • 2014-05-30
    • 2010-09-10
    • 1970-01-01
    • 1970-01-01
    • 2012-08-19
    相关资源
    最近更新 更多