Spring @Cacheable 和@Async 注解

Spring @Cacheable and @Async annotation

我需要缓存一些异步计算的结果。具体来说,为了克服这个问题,我正在尝试使用 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 感兴趣。

根据 SPR-12967,不支持 ListenableFuture (CompletableFuture)。

理论上只要

就可以用
  • @Cacheable 背后的 CacheManager 实现没有序列化缓存的对象(就像 Hazelcast 支持的缓存)

  • 因为 CompletableFuture 持有一个状态,可以通过调用例如修改cancel() 方法,重要的是 API 的所有用户都不会乱用缓存的对象。否则,可能存在无法再检索 Future 中的缓存对象的风险,并且需要缓存逐出

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

社区让我做一些实验,所以我做了。我发现我的问题的答案很简单:如果 @Cacheable@Async 放在同一个方法之上,它们就不能一起工作。

明确地说,我并不是在寻求一种方法来直接使缓存 return 成为 CompletableFuture 拥有的对象。这是不可能的,否则会破坏classclass.

的异步计算的契约。

正如我所说,这两个注释不能在同一个方法上一起工作。如果你仔细想想,这是显而易见的。用@Async标记也是@Cacheable的意思是将整个缓存管理委托给不同的异步线程。如果 CompletableFuture 的值的计算需要很长时间才能完成,缓存中的值将在该时间之后由 Spring Proxy 放置。

显然,有一个解决方法。解决方法使用 CompletableFuture 是一个 承诺 这一事实。让我们看看下面的代码。

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

诀窍是创建一个不完整的承诺,并立即从标有 @Cacheable 注释的方法中 return 它。该承诺将由另一个拥有标有 @Async 注释的方法的 bean 异步完成。

作为奖励,我还实现了一个不使用 Spring @Async 注释的解决方案,但它使用 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);
}

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

最后,上面是对JiraSPR-12967所指内容的长篇描述

希望对您有所帮助。 干杯。

在一个 class 中的方法上添加 @Async 注释,在另一个 class 中的方法级别添加 @Cacheable 注释。

然后从服务或任何不同的层调用 @Async 方法。

它对我有用,包括 Redis 缓存和异步,极大地提高了性能。

我尝试了以下方法,它似乎有效。

  • 使用 @Cachable 创建一个执行实际业务逻辑的方法
  • 使用 @Async 创建一个方法,它调用上面的 @Cachable 方法和 returns 一个 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";
    }
}

请在@Component 或@Serice class 级别添加注解@EnableAsync。 Exp:

@Service
@Slf4j
@EnableAsync //Add it to here
public class CachingServiceImpl implements CachingService {

希望能帮到你!