Spring Cache:如何避免缓存对象在持久化前引发数据不一致?
缓存与事务一致性问题的最佳实践
问题核心:直接修改从缓存获取的User对象引用后,若事务因异常回滚,数据库数据未变更,但缓存中的对象属性已被修改,导致数据不一致。以下是无需对象克隆的解决方案:
方案1:事务启动时清除对应缓存
在createAppUser方法上添加@CacheEvict注解,确保事务开始时就清除目标邮箱的缓存条目:
@Override @Transactional(isolation = Isolation.SERIALIZABLE) @CacheEvict(value = UserRepository.USER_CACHE, key = "#registrationRequest.email") public void createAppUser(RegistrationAppRequestDto registrationRequest) throws EmailSendingException { // 此时getUserByEmail会从数据库加载最新托管对象,而非缓存 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); }
事务内修改的是数据库加载的对象,若回滚,数据库数据不变,缓存无脏数据,下次查询将重新从数据库拉取正确数据。
方案2:事务回滚时同步清理缓存
注册事务同步器,当检测到事务回滚时主动清除对应缓存:
@Service public class UserServiceImpl implements UserService, UserDetailsService { private final UserRepository userRepository; private final CacheManager cacheManager; // 注入CacheManager // 构造方法注入 public UserServiceImpl(UserRepository userRepository, CacheManager cacheManager) { this.userRepository = userRepository; this.cacheManager = cacheManager; } @Override @Transactional(isolation = Isolation.SERIALIZABLE) public void createAppUser(RegistrationAppRequestDto registrationRequest) throws EmailSendingException { String email = registrationRequest.getEmail(); User user = getUserByEmail(email); 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); } // 注册事务同步器 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCompletion(int status) { if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { Cache cache = cacheManager.getCache(UserRepository.USER_CACHE); if (cache != null) { cache.evict(email); } } } }); mailSender.sendWelcomeMessage(user); userRepository.save(user); } }
该方案仅在事务回滚时清理缓存,避免不必要的缓存失效,同时修正缓存脏数据问题。
方案3:缓存更新与事务提交绑定
使用TransactionAwareCacheManagerProxy包装缓存管理器,确保@CachePut仅在事务成功提交后执行缓存更新:
@Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager simpleCacheManager = new SimpleCacheManager(); simpleCacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache(UserRepository.USER_CACHE))); // 包装为事务感知的缓存管理器 return new TransactionAwareCacheManagerProxy(simpleCacheManager); } }
配置后,UserRepository中的@CachePut会延迟到事务提交后执行,若事务回滚,缓存不会被更新。结合方案1可彻底避免缓存脏数据。
方案4:前置高风险操作
将邮件发送等易失败操作前置,确认成功后再修改用户对象并执行持久化:
@Override @Transactional(isolation = Isolation.SERIALIZABLE) public void createAppUser(RegistrationAppRequestDto registrationRequest) throws EmailSendingException { String email = registrationRequest.getEmail(); // 先获取用户对象发送邮件,不修改属性 User tempUser = getUserByEmail(email); mailSender.sendWelcomeMessage(tempUser); // 邮件发送成功后,再修改对象并持久化 User user = getUserByEmail(email); 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); } userRepository.save(user); }
此方案避免了修改对象后回滚的问题,但需注意:若邮件发送成功后数据库操作失败,会出现邮件已发送但用户未更新的业务不一致,需根据场景权衡。
内容的提问来源于stack exchange,提问作者SnejOK
相关产品推荐
相关产品推荐

