You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 08:55:12