Spring的@Cacheable和@Async注解

11

我需要缓存一些异步计算的结果。具体来说,为了解决这个问题,我正在尝试使用Spring 4.3缓存和异步计算功能。

例如,让我们看一下以下代码:

@Service
class AsyncService {
    @Async
    @Cacheable("users")
    CompletableFuture<User> findById(String usedId) {
        // Some code that retrieves the user relative to id userId
        return CompletableFuture.completedFuture(user);
    }
}

可能吗?我的意思是,Spring的缓存抽象是否能正确处理CompletableFuture<User>类型的对象?我知道Caffeine Cache有类似的功能,但我不确定如果适当配置后Spring是否使用它。

编辑:我对User对象本身不感兴趣,而是对代表计算的CompletableFuture感兴趣。


你试过了吗? - Kayaman
我这几天必须试一下。@Kayaman,我会告诉你我的测试结果。 - riccardo.cardin
1
好的。我一直在等待看是否有人接手这个问题。我自己也进行了一些轻度的谷歌搜索,但只发现了@Async任务中的FutureCompatibleFuture之间的区别,这在4.2中已经得到了修复。 - Kayaman
@Kayaman 我发布了我的结果的答案。希望能有所帮助。 - riccardo.cardin
6个回答

12
社区要求我做一些实验,所以我就做了。我发现答案很简单:如果在同一个方法上方放置了@Cacheable@Async,它们就无法一起工作。
明确一下,我并不是在寻找直接使缓存返回CompletableFuture拥有的对象的方法。这是不可能的,如果这样做,它将违反CompletableFuture类的异步计算契约。
正如我所说的,这两个注释不能在同一个方法上一起使用。如果你深思熟虑一下,这是显而易见的。用@Async标记也就意味着@Cacheable将整个缓存管理委托给不同的异步线程。如果CompletableFuture的值计算需要很长时间才能完成,那么Spring代理就会在那段时间之后将该值放入缓存中。
显然,有一个解决方法。解决方法利用了CompletableFuture是一个promise的事实。让我们看一下下面的代码。
@Component
public class CachedService {
    /* Dependecies resolution code */
    private final AsyncService service;

    @Cacheable(cacheNames = "ints")
    public CompletableFuture<Integer> randomIntUsingSpringAsync() throws InterruptedException {
        final CompletableFuture<Integer> promise = new CompletableFuture<>();
        // Letting an asynchronous method to complete the promise in the future
        service.performTask(promise);
        // Returning the promise immediately
        return promise;
    }
}

@Component
public class AsyncService {
    @Async
    void performTask(CompletableFuture<Integer> promise) throws InterruptedException {
        Thread.sleep(2000);
        // Completing the promise asynchronously
        promise.complete(random.nextInt(1000));
    }
}

关键在于创建一个不完整的 Promise,并立即从被标记为 @Cacheable 注解的方法中返回它。该 Promise 将由另一个拥有被标记为 @Async 注解的方法的 bean 异步完成。

作为额外奖励,我还实现了一种解决方案,它不使用 Spring 的 @Async 注解,而是直接使用 CompletableFuture 类中可用的工厂方法。

@Cacheable(cacheNames = "ints1")
public CompletableFuture<Integer> randomIntNativelyAsync() throws
        InterruptedException {
    return CompletableFuture.supplyAsync(this::getAsyncInteger, executor);
}

private Integer getAsyncInteger() {
    logger.info("Entering performTask");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return random.nextInt(1000);
}

无论如何,我在我的GitHub问题上分享了完整的解决方案,spring-cacheable-async

最后,上面是对Jira SPR-12967 的描述。

希望能有所帮助。 干杯。


我为你的尝试点赞,但你所做的看起来像是一个hack。更清晰的方法是放弃Cacheable注解并直接使用缓存管理器。或者注册一个回调函数来实现。 - Abhijit Sarkar
@Cacheable注解底层的机制非常有用。我正在使用Spring,因此我尝试按照它的理念去做事情。 - riccardo.cardin

5
根据SPR-12967,不支持ListenableFutureCompletableFuture)。

感谢您的回应。在您提供的Jira中询问了不同的问题。据我所知,在那个Jira中,用户正在请求缓存ListenableFuture的内部值。我认为只要 CompletableFuture 遵守 Object 的协议,它就可以被成功地缓存,对吗? - riccardo.cardin

2
在一个类的方法上添加@Async注解,在不同的类的方法级别上添加@Cacheable注解。然后从服务或任何不同的层调用@Async方法。这对我有用,Redis缓存和异步都能工作,大大提高了性能。

1
嗨@Rohit,欢迎来到Stack Overflow。从我的理解来看,这与我们在项目中实施的解决方案类似。但是我认为添加一些代码以说明您的解决方案,并在描述中更具体会更好。 - Didier L

1
请在@Component或@Service类级别添加注释@EnableAsync。 例如:
@Service
@Slf4j
@EnableAsync //Add it to here
public class CachingServiceImpl implements CachingService {

希望能够帮到你!


0

我尝试了以下方法,似乎可以工作。

  • 创建一个带有@Cachable的方法,执行实际的业务逻辑
  • 创建一个带有@Async的方法,调用上述@Cachable方法并返回一个CompletableFuture
  • 在主执行流中调用带有@Async的方法

示例:

public class Main {
    public void cachedAsyncData() {
        try {
            asyncFetcher.getData().get();
        } catch(Exception e){}
    }
}

public class AsyncFetcher {
    @Async
    public CompletableFuture<String> getData() {
        return CompletableFuture.completedFuture(cacheFetcher.getData());
    }
}

public class CacheFetcher {
    @Cacheable
    public String getData() {
        return "DATA";
    }
}

0

理论上,只要

  • @Cacheable 后面的 CacheManager 实现没有对缓存对象进行序列化(例如 Hazelcast 缓存),就可以工作。

  • 由于 CompletableFuture 持有状态,该状态可以通过调用 cancel() 等方法进行修改,因此所有 API 用户都不能弄乱缓存对象。否则,存在缓存对象无法在 Future 中被检索到的风险,需要进行缓存驱逐。

  • 值得验证注解后面的代理调用顺序,即 @Cacheable 代理总是在 @Async 代理之前调用吗?还是反过来?或者是取决于什么?例如,如果先调用 @Async,它会在 ForkJoinPool 中触发一个 Callable,然后再从缓存中检索其他对象。


不行,它不起作用。今天我做了一些实验。明天我会发布这些实验的结果。 - riccardo.cardin

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