Spring中@Cacheable、@CacheEvict与@Transactional的竞态条件问题求解
多线程场景下@Cacheable、@CacheEvict与@Transactional的竞态条件解决方法
问题场景
在多线程应用中使用Spring缓存注解和事务注解时出现竞态条件:
- 需求:以id为键缓存查询结果,保存新数据时驱逐对应缓存
- 现有代码:
@CacheConfig(cacheNames = CACHE_NAME) @Repository public interface MyRepository extends JpaRepository<MyTable, String> { @Cacheable MyTable getMyTable(String id); @CacheEvict(allEntries = true) MyTable saveMyTable(MyTable myTable); } @Service public class MyService { @Transactional public MyTable createMyTable(String id) { return myRepo.saveMyTable(new MyTable(id)); } } public void method(String id) { MyTable t = myRepo.getMyTable(id); //...1 if (t == null) { synchronized (this) { t = myRepo.getMyTable(id); //...2 if (t == null) { t = myService.createMyTable(id); //...3 } } } // do work on t }
- 竞态触发:线程T1执行步骤3时,
@CacheEvict先清空了缓存,但数据库事务还未提交;此时线程T2执行步骤1,缓存未命中后查询数据库得到null并缓存该值。T1提交事务退出同步块后,T2执行步骤2时因缓存了null,直接跳过数据库查询,继续创建同id数据,最终抛出重复id异常。
可行解决办法
1. 让缓存驱逐与事务提交绑定
利用@CacheEvict的afterCommit属性,设置为true,让缓存驱逐操作在事务提交完成后执行,避免缓存先清但数据未落地的情况:
@CacheEvict(allEntries = true, afterCommit = true) // 事务提交后再驱逐缓存 MyTable saveMyTable(MyTable myTable);
这样T1的缓存清空操作会等数据库事务提交成功后才执行,T2查询时要么命中旧缓存,要么查数据库能拿到T1刚提交的新数据,不会出现缓存null的情况。
2. 禁止缓存null结果
修改@Cacheable,添加unless = "#result == null",让查询返回null时不缓存,确保后续线程进入同步块后会重新查询数据库:
@Cacheable(unless = "#result == null") // 不缓存null值 MyTable getMyTable(String id);
即使T2在T1提交前查询到null,也不会缓存这个结果,当T2执行步骤2时会再次查询数据库,此时T1的事务已经提交,就能拿到新创建的数据,避免重复创建。
3. 缩小缓存驱逐粒度
当前用allEntries = true会清空整个缓存,粒度太粗容易引发竞态。改成按id精准驱逐缓存,只清除当前保存数据对应的缓存条目:
@CacheEvict(key = "#myTable.id", afterCommit = true) // 仅驱逐当前id的缓存 MyTable saveMyTable(MyTable myTable);
这样既满足缓存更新需求,又减少了对其他缓存条目的影响,降低竞态发生概率。
4. 替换本地锁为分布式锁(多实例部署场景)
如果是多服务实例部署,本地synchronized只能保证单JVM内的线程安全,需要用分布式锁(比如Redis锁)来确保同一id的创建操作全局唯一:
public void method(String id) { MyTable t = myRepo.getMyTable(id); if (t == null) { // 用id作为锁键,获取分布式锁 RLock lock = redissonClient.getLock("my-table-lock:" + id); try { lock.lock(); t = myRepo.getMyTable(id); if (t == null) { t = myService.createMyTable(id); } } finally { lock.unlock(); } } // do work on t }
结合前面的缓存配置,彻底避免多实例下的竞态问题。
内容的提问来源于stack exchange,提问作者user1589188
相关产品推荐
相关产品推荐

