问题
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. thecancel()
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 theFuture
could not be retrieved anymore, and a cache eviction would be necessaryIt'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 aCallable
inside aForkJoinPool
, just to then retrieve the other object from the cache.
来源:https://stackoverflow.com/questions/47160563/spring-cacheable-and-async-annotation