Spring Data:如何在事务方法中维护缓存一致性?
Spring内置缓存是否存在这些问题?
是的,Spring内置缓存框架(基于@Cacheable/@CachePut/@CacheEvict)同样存在你提到的两类一致性问题:
Write-through策略的事务回滚问题
默认情况下,@CachePut的缓存更新操作会在目标方法执行完成后立即执行,而Spring事务的提交是在方法执行完毕后的AOP切面环节完成。如果缓存切面的执行优先级高于事务切面,就会出现缓存已更新但数据库事务后续回滚的情况,导致缓存中存在未落地到数据库的脏数据。缓存失效的并发脏读问题
使用@CacheEvict时,默认也是在方法执行完成后立即失效缓存。此时数据库事务尚未提交,其他并发请求调用getUserById时,会从数据库读取旧版本数据并写入缓存,后续事务提交后,缓存中依然是过时数据,引发一致性问题。
实际应用中的规避方案
1. 调整缓存与事务的切面执行顺序
通过设置切面优先级,让事务切面先执行,缓存切面在事务提交后再执行,确保缓存操作仅在事务成功提交后生效。
在配置类中指定切面顺序:
@Configuration @EnableTransactionManagement(order = 1) // 事务切面优先级更高(order值越小优先级越高) @EnableCaching(order = 2) // 缓存切面在事务切面之后执行 public class CacheTransactionConfig { // 其他配置 }
这样@CachePut和@CacheEvict都会在事务提交后执行,从根源上避免了事务回滚导致的缓存脏数据,以及缓存失效后并发读取旧数据的问题。
2. 手动绑定缓存操作到事务提交事件
如果需要更精细的控制,可以通过TransactionSynchronizationManager手动注册事务同步器,在事务成功提交后再执行缓存更新或失效操作。
示例:Write-through策略(事务提交后更新缓存)
@Service public class UserService { @Autowired private UserRepository userRepo; @Autowired private CacheManager cacheManager; @Transactional(readOnly = true) @Cacheable(value = "users", key = "#userId") public User getUserById(String userId) { return userRepo.findById(userId).orElse(null); } @Transactional(readOnly = false) public User saveUser(User user){ User savedUser = userRepo.save(user); // 注册事务同步器,仅在事务提交后更新缓存 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { Cache userCache = cacheManager.getCache("users"); if (userCache != null) { userCache.put(savedUser.getId(), savedUser); } } }); return savedUser; } }
示例:缓存失效策略(事务提交后失效缓存)
@Transactional(readOnly = false) public User saveUser(User user){ User savedUser = userRepo.save(user); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { Cache userCache = cacheManager.getCache("users"); if (userCache != null) { userCache.evict(savedUser.getId()); } } }); return savedUser; }
3. 利用读写锁缩小并发窗口
在高并发场景下,可以通过读写锁控制对缓存和数据库的访问:
- 执行更新操作时,加写锁,阻塞所有读请求,直到事务提交并完成缓存更新/失效。
- 执行读取操作时,加读锁,允许多个读请求并发执行,但会被写锁阻塞。
这种方式能有效减少缓存失效到事务提交之间的并发窗口,降低脏数据写入缓存的概率。
4. 缓存版本校验
在缓存数据时带上版本号(或时间戳),读取缓存时对比数据库中的版本号:
- 如果缓存版本与数据库版本一致,直接返回缓存数据。
- 如果版本不一致,更新缓存并返回最新数据。
这种方式可以在出现脏数据时自动修正,适合对一致性要求较高但能容忍短暂不一致的场景。
5. 强一致性场景的特殊处理
如果业务要求绝对的缓存与数据库一致性,建议在关键操作时直接绕过缓存读取数据库,或者使用分布式锁严格控制读写顺序,确保更新操作完全完成后才允许读取。
内容的提问来源于stack exchange,提问作者Martin Häusler

