我想马上用一个例子来说明。有这样一个存储库:
@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提供程序的缓存,例如Hibernate的L2缓存,可能是一种解决方案,但是现在您已经将(可配置的、有条件的和可选的)缓存关注点从应用程序转移到持久性提供程序,这可能不是在所有情况下都需要的,也不是可移植的。
例如,当您不使用JPA和数据库作为持久性提供者/后备存储,而是使用另一个存储(如Cassandra)作为您的记录系统(SOR)来支持某些服务时,会发生什么?或者,也许您正在从应用程序服务类调用[基于rest的]Web服务(例如Google Maps/Geocoding API) ?然后什么?
1您将遇到的问题,特别是当您只是使用"simple"Spring [Boot]提供的缓存提供程序(也在Spring框架的缓存抽象文档中描述),是对缓存返回的对象的引用是存储在缓存中的实际对象。
类似于:
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();
更糟糕的情况是,被缓存的对象的(改变的)状态是equals
和hashCode
方法的一部分(通常在执行缓存操作时使用:get(key)
,put(key, value)
等),这将影响映射,并影响任何基于Map
的缓存实现。
当从缓存中读取或存储缓存值(对象)时,大多数缓存提供程序都会提供一个设置,例如copy-on-read
(语义)。这在基于本地的缓存中特别有用(用于&;Near Caching&;),它可以补充基于客户端/服务器的缓存(例如Redis),以减少访问时的网络跳数,特别是对于不经常更改的数据。
如果缓存解决方案是纯客户端/服务器(没有本地缓存,只是从客户端代理到服务器),这是大多数生产拓扑安排(给定缓存数据通常是"共享的"),那么修改缓存的恐惧是零,因为访问/从服务器访问的值(例如Redis)无论如何都需要反/序列化。
如果你使用的是Spring Boot的"Simple"(ConcurrentMap
支持的)Cache
实现,也许只是为了(基于配置文件的)测试目的,然后您甚至可以配置"simple";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,特别是在嵌入式容量(Near Caching的一种形式)中,您将使用CacheConfig.setStoreByValue(boolean)
。
同样,Spring [Boot]支持的许多健壮的缓存提供程序,实现了JCacheAPI,也可以作为JCache缓存提供程序(Spring和Spring Boot对此提供了出色的支持,在这里)提供类似的配置。
这应该不影响任何应用程序代码,只影响您的配置,并且它不应该依赖于应用程序层(例如:持久性)。