我在典型的多线程应用程序中使用
@Cacheable
、@CacheEvict
和 @Transactional
,但面临着竞争状况,我不知道解决它的好方法。我现在唯一有效的解决方案是根本不使用缓存,但这是不可取的。想看看SO社区能否有更好的解决方案。这是一个示例设置:
@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
}
因此,目标是使用 id 作为键来缓存获取值,然后在保存新值时逐出所有缓存的值(即所有 id)。
当线程 (T1) 处于
3
中间时,就会发生竞争条件,其中缓存已被逐出,但在更改提交到数据库之前,此时另一个线程 (T2) 调用 1
,它将缓存未命中,然后从数据库检查,数据库肯定会返回 null,并且 null 会为该 id 缓存。
当 T1 提交并退出同步块时,T2 在
2
再次进行第二次检查,通常没有缓存,这会告诉 T2 数据库中已经有该 id 的条目,但是不需要执行任何操作,由于上面的竞争条件,缓存的 null 值返回而没有实际检查数据库,并且 T2 继续创建具有相同 id 的新条目,抛出异常。
潜在地,我认为如果缓存逐出和事务提交是原子完成的,那么它应该没有错误,但不确定
@CacheEvict
和@Transactional
如何相互依赖。谢谢你。
您遇到的问题是同时使用缓存和数据库作为持久存储的系统中缓存一致性问题的典型示例。由于数据库的事务性保证与缓存的非事务性本质之间的差异而出现了挑战。
您所观察到的竞争条件确实可以通过确保在事务提交时自动发生缓存逐出来缓解。实现此目的的一种方法是使用事务缓存管理。
Spring 通过
@Cacheable
、@CachePut
和 @CacheEvict
注解提供此功能,这些注解可以与 @Transactional
结合使用。但是,为了使其按预期工作,您需要调整您的设置:
在保存数据的事务方法中使用
@CachePut
而不是 @Cacheable
。这确保了只有在事务成功提交后才更新缓存。
确保
@CacheEvict
处的缓存逐出与事务的成功提交相关联。如果您的缓存提供程序和事务管理器配置正确,@CacheEvict
应该仅在提交事务后逐出该条目。
以下是更新服务以解决竞争条件的方法:
@CacheConfig(cacheNames = CACHE_NAME)
@Repository
public interface MyRepository extends JpaRepository<MyTable, String> {
// Removed @Cacheable here to avoid caching null values
MyTable getMyTable(String id);
@CacheEvict(allEntries = true)
MyTable saveMyTable(MyTable myTable);
}
@Service
public class MyService {
@Transactional
public MyTable createMyTable(String id) {
MyTable t = myRepo.getMyTable(id); // Check the database within the transaction
if (t == null) {
t = myRepo.saveMyTable(new MyTable(id)); // Save and evict the cache after successful commit
}
return t;
}
}
public void method(String id) {
MyTable t = myService.createMyTable(id); // Always use the service method
// do work on t
}
在此设置中,
createMyTable
是一种事务方法,它可以从数据库中获取数据,也可以创建一条新记录(如果不存在)。缓存逐出与成功的保存操作相关联 @CacheEvict
。
请注意,通过直接使用
createMyTable
方法,您可以确保对 MyTable
的任何读取或写入都是在事务内完成,这有助于保持缓存和数据库之间的一致性。
请记住,此方法假设您的缓存管理器支持事务同步。如果您的缓存管理器不支持事务同步,您可能需要显式管理缓存一致性。
最后,请记住,在多线程应用程序中使用
synchronized
,特别是在集群或分布式环境中,可能会导致可扩展性问题。相反,您应该更喜欢 JPA 提供的事务管理和乐观锁定机制,它们可以更有效地处理并发更改。