Spring @Cacheable and @Async annotation

谁说胖子不能爱 提交于 2020-08-06 09:15:10

问题


I have the need to cache some the results of some asynchronous computations. In detail, to overcome this issue, I am trying to use Spring 4.3 cache and asynchronous computation features.

As an example, let's take the following code:

@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);
    }
}

Is it possible? I mean, will the caching abstraction of Spring handle correctly the objects of type CompletableFuture<User>? I know that Caffeine Cache has something like that, but I can't understand if Spring uses it if properly configured.

EDIT: I am not interested in the User object itself, but in the CompletableFuture that represents the computation.


回答1:


As per SPR-12967, ListenableFuture (CompletableFuture) are not supported.




回答2:


The community asks me to do some experiments, so I made them. I found that the answer to my question is simple: @Cacheable and @Async do not work together if they are placed above the same method.

Just to be clear, I was not asking a way to make the cache to return directly the object owned by a CompletableFuture. This is impossible and if it isn't so, it would break the contract of asynchronous computation of the CompletableFuture class.

As I said, the two annotations do not work together on the same method. If you think about it, it is obvious. Marking with @Async a method that is also @Cacheable means to delegate the whole cache management to different asynchronous threads. If the computation of the value of the CompletableFuture will take a long time to complete, the value in the cache will be placed after that time by Spring Proxy.

Obviously, there is a workaround. The workaround uses the fact the CompletableFuture are promises. Let's have a look at the code below.

@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));
    }
}

The trick is to create an incomplete promise and return it immediately from the method that is marked with the @Cacheable annotation. The promise will be completed asynchronously by another bean, that owns the method marked with the @Async annotation.

As a bonus, I implemented also a solution that does not use the Spring @Async annotation, but it uses directly the factory methods available in CompletableFuture class.

@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);
}

Anyway, I shared the complete solution to the problem on my GitHub, spring-cacheable-async.

Finally, the above is a long description of what the Jira SPR-12967 refers to.

Hope it helps. Cheers.




回答3:


Add @Async annotation on methods in one class and @Cacheable annotation at method level in a different class.

Then invoke @Async method from a service or any different layer.

It worked for me, both Redis cache and Async, which improved the performance drastically.




回答4:


In theory, it would work as long as

  • the implementation of CacheManager behind the @Cacheable is not serializing the cached objects (like a cache backed by Hazelcast)

  • Since the CompletableFuture holds a state, which can be modified by calling e.g. the cancel() method, it's important that all the users of the API won't mess around with the cached object. Otherwise, there might be the risk that the cached object inside the Future could not be retrieved anymore, and a cache eviction would be necessary

  • It's worth to verify in which order the proxies behind the annotations are called. i.e. is the @Cacheable proxy called always before the @Async one? Or the other way around? Or it depends? For example, if the @Async is called before, it will fire a Callable inside a ForkJoinPool, just to then retrieve the other object from the cache.



来源:https://stackoverflow.com/questions/47160563/spring-cacheable-and-async-annotation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!