Spring Boot中@Cacheable与@Transactional联合使用时无法正常工作

4
我不能分享实际代码,因为公司政策的限制,但以下是方法结构的示例。
因此,在此示例中,我希望当类A中抛出异常时,清除Class B中方法上的缓存。
注意:我无法将缓存移动到Class A,因此这不是可行的解决方案。
我尝试阅读所有在线答案和帖子来使其工作,但无法弄清楚。
请提出建议。A
我已在application.properties中设置了以下属性。
spring.cache.enabled=true
spring.cache.jcache.config=classpath:cache/ehcache.xml

@EnableCaching
@EnableTransactionManagement
    Main Class{


@Autowired
CacheManager cacheManager

@PostConstruct
void postConstruct(){
(JCacheCacheManager)cachemanager).setTransactionAware(true);

}
}

@Service
Class A{

@Autowired
B b;

@Transactional
public List<Data> getAllBusinessData(){

List<Data> dataList = b.getDataFromSystem("key");

//TestCode to test cache clears if exception thrown here
throw new RuntimeException("test");

}
}

@Service
Class B{

@Cacheable("cacheName")
public List<Data> getDataFromSystem(String key){

client call code here

return dataList;

}

}

添加你已经尝试过的解决方案会非常有帮助。 - dekkard
请尝试开发一个关于此(repo)的最小示例,因为我不明白为什么您的代码不能正常工作。 - Jonathan JOhx
为了测试目的,我已经在A类方法中添加了一个throw new RuntimeException,每当从B类返回结果不是null时。如上面的例子所示。 - Mukul Goel
@MukulGoel 在这种情况下应该期望什么样的响应? - Kumaresh Babu N S
@KumareshBabuNS 在哪种情况下呢?当我在A类中抛出新的运行时异常时? 预期的响应应该是:什么都没有被缓存,当我再次从Swagger进行相同的调用时,A类会调用B类并且B类尝试再次访问外部API来获取响应。从而证明在事务内抛出异常时,没有任何内容被缓存。 - Mukul Goel
显示剩余4条评论
1个回答

4

还有其他方法,但以下方法可能是有效的解决方案。

第一步将是定义一个自定义异常,以便稍后能够适当地处理它。此异常将接收缓存的名称和要清除的键等参数。例如:

public class CauseOfEvictionException extends RuntimeException {

  public CauseOfEvictionException(String message, String cacheName, String cacheKey) {
    super(message);
    
    this.cacheName = cacheName;
    this.cacheKey = cacheKey;
  }

  // getters and setters omitted for brevity
}

在您的示例中,您的B类将引发此异常:

@Service
Class A{

  @Autowired
  B b;

  @Transactional
  public List<Data> getAllBusinessData(){
 
    List<Data> dataList = b.getDataFromSystem("key");

    // Sorry, because in a certain sense you need to be aware of the cache
    // name here. Probably it could be improved
    throw new CauseOfEvictionException("test", "cacheName", "key");

  }
}

现在,我们需要一种处理这种异常的方法。
独立于该方法之外,想法是负责处理异常的代码将访问配置的CacheManager并触发缓存清除。
因为您正在使用Spring Boot,处理它的简单方法是通过扩展ResponseEntityExceptionHandler来提供适当的@ExceptionHandler。请参考更多信息我在相关SO问题中提供的答案这篇很棒的文章
总之,请考虑以下示例:
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
  
  @Autowired
  private CacheManager cacheManager;

  @ExceptionHandler(CauseOfEvictionException.class)
  public ResponseEntity<Object> handleCauseOfEvictionException(
    CauseOfEvictionException e) {
    this.cacheManager.getCache(e.getCacheName()).evict(e.getCacheKey());

    // handle the exception and provide the necessary response as you wish
    return ...;
  }
}

重要的是要意识到,当处理由多个参数默认情况下(请考虑阅读此文)组成的键时,实际缓存键将被包装为SimpleKey类的实例,该实例包含所有这些参数。

请注意,此默认行为可以通过使用SpEL或提供自己的缓存KeyGenerator进行某种程度的定制化。参考这里,这是框架提供的默认实现SimpleKeyGenerator
考虑问题,可能的解决方案也可以使用某种AOP。思路如下。
首先,定义某种辅助注释。该注释将有助于确定应该建议哪些方法。例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EvictCacheOnError {
}

下一步是定义处理实际缓存逐出过程的方面。假设您只需要通知Spring管理的bean,为了简单起见,我们可以使用Spring AOP。您可以使用@Around@AfterThrowing方面。考虑以下示例:
@Aspect
@Component
public class EvictCacheOnErrorAspect {

  @Autowired
  private CacheManager cacheManager;

  @Around("@annotation(your.pkg.EvictCacheOnError)")
  public void evictCacheOnError(ProceedingJoinPoint pjp) {
    try {
      Object retVal = pjp.proceed();
      return retVal;
    } catch (CauseOfEvictionException e) {
      this.cacheManager.getCache(
          e.getCacheName()).evict(e.getCacheKey()
      );

      // rethrow 
      throw e;
    }    
  }
}

最后一步是注释应用行为的方法:
@Service
Class A{

  @Autowired
  B b;

  @Transactional
  @EvictCacheOnError
  public List<Data> getAllBusinessData(){

    List<Data> dataList = b.getDataFromSystem("key");

    throw new CauseOfEvictionException("test", "cacheName", "key");
  }
}

您甚至可以尝试泛化这个想法,通过在EvictCacheOnError注释中提供所有必要的信息来执行缓存清除:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EvictCacheOnError {
    String cacheName();
    int[] cacheKeyArgsIndexes();
}

以下是:

@Aspect
@Component
public class EvictCacheOnErrorAspect {

  @Autowired
  private CacheManager cacheManager;

  @Autowired
  private KeyGenerator keyGenerator;

  @Around("@annotation(your.pkg.EvictCacheOnError)")
  // You can inject the annotation right here if you want to
  public void evictCacheOnError(ProceedingJoinPoint pjp) {
    try {
      Object retVal = pjp.proceed();
      return retVal;
    } catch (Throwable t) {
      // Assuming only is applied on methods
      MethodSignature signature = (MethodSignature) pjp.getSignature();
      Method method = signature.getMethod();
      // Obtain a reference to the EvictCacheOnError annotation
      EvictCacheOnError evictCacheOnError = method.getAnnotation(EvictCacheOnError.class);
      // Compute cache key: some safety checks are imperative here,
      // please, excuse the simplicity of the implementation
      int[] cacheKeyArgsIndexes = evictCacheOnError.cacheKeyArgsIndexes();
      Object[] args = pjp.getArgs();
      List<Object> cacheKeyArgsList = new ArrayList<>(cacheKeyArgsIndexes.length);
      for (int i=0; i < cacheKeyArgsIndexes.length; i++) {
        cacheKeyArgsList.add(args[cacheKeyArgsIndexes[i]]);
      }
      
      Object[] cacheKeyArgs = new Object[cacheKeyArgsList.size()];
      cacheKeyArgsList.toArray(cacheKeyArgs);

      Object target = pjp.getTarget();

      Object cacheKey = this.keyGenerator.generate(target, method, cacheKeyArgs);

      // Perform actual eviction
      String cacheName = evictCacheOnError.cacheName();
      this.cacheManager.getCache(cacheName).evict(cacheKey);

      // rethrow: be careful here if using in it with transactions
      // Spring will per default only rollback unchecked exceptions
      throw new RuntimeException(t);
    }    
  }
}

这种解决方案取决于实际的方法参数,如果缓存键是基于方法体内获得的中间结果,则可能不适用。

感谢您抽出时间来回答。我正在尝试您的解决方案。在我的用例中,B类中的方法有3个参数。您能否建议如何创建'key'对象,该方法接受单个对象,不确定如何将3个参数转换为缓存键。这个方法中的this.cacheManager.getCache(e.getCacheName()).evict(keyObject);怎么用呢? - Mukul Goel
不用在意上面的内容了。我发现Spring在使用@cacheable框架时使用SimpleKey simpleKey = new SimpleKey(Object... keyElements)。我现在正在尝试这个。 - Mukul Goel
抱歉回复晚了。@MukulGoel,不用谢。很高兴看到你能够根据自己的需求创建缓存键。希望提供的解决方案对你有所帮助。 - jccampanero
谢谢您提供的解决方案。它确实按预期工作。为了完整起见,您能否更新答案,包括对于带有多个参数的cacheable注释方法,我们需要提供SimpleKey实例的概念。 这里列出了关键场景。 https://www.logicbig.com/tutorials/spring-framework/spring-integration/cache-key-generation.html - Mukul Goel
1
非常感谢您的反馈@MukulGoel。 很高兴听到解决方案按预期工作。 是的,我当然已经根据您提供的信息和一些附加信息更新了答案。 我希望它也能起到帮助作用。 - jccampanero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接