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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 19:13:11