知識庫 / Spring WebFlux RSS 訂閱

Spring Webflux 和 @Cacheable 註解

Reactive,Spring WebFlux
HongKong
7
12:28 PM · Dec 06 ,2025

1. 引言

本文將解釋 Spring WebFlux 如何與 <em @Cacheable</em> 註解交互。首先,我們將探討一些常見問題以及如何避免它們。接下來,我們將介紹可用的解決方法。最後,如往常一樣,我們將提供代碼示例。

2. <em @Cacheable 與反應式類型

本文檔內容相對較新。在撰寫本文時,<em @Cacheable 與反應式框架之間的無縫集成尚不完善。 主要問題在於,目前沒有非阻塞緩存實現(JSR-107緩存API是阻塞式的)。 僅Redis提供了反應式驅動程序。

儘管我們之前提到的問題,我們仍然可以在服務方法上使用 <em @Cacheable。 這會導致我們封裝的對象(<em Mono 或 <em Flux)被緩存,但不會緩存我們方法本身的實際結果。

2.1. 項目設置

讓我們通過一個測試案例來演示。在測試之前,我們需要設置我們的項目。我們將創建一個簡單的 Spring WebFlux 項目,並使用 reactive MongoDB 驅動器。我們不會將 MongoDB 運行為單獨的進程,而是使用 Testcontainers。

我們的測試類將被標註為 @SpringBootTest,並且將包含:

final static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));

@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
    mongoDBContainer.start();
    registry.add("spring.data.mongodb.uri",  mongoDBContainer::getReplicaSetUrl);
}

這些行將啓動 MongoDB 實例並將 URI 傳遞給 SpringBoot 以自動配置 Mongo 存儲庫。

對於此測試,我們將創建一個 ItemService 類,該類包含 savegetItem 方法:

@Service
public class ItemService {

    private final ItemRepository repository;

    public ItemService(ItemRepository repository) {
        this.repository = repository;
    }
    @Cacheable("items")
    public Mono<Item> getItem(String id){
        return repository.findById(id);
    }
    public Mono<Item> save(Item item){
        return repository.save(item);
    }
}

<em >application.properties</em > 中,我們設置了緩存和倉庫的日誌器,以便我們能夠監控測試過程中的情況:

logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.cache=TRACE

2.2. 初步測試

完成設置後,我們可以運行測試並分析結果:

@Test
public void givenItem_whenGetItemIsCalled_thenMonoIsCached() {
    Mono<Item> glass = itemService.save(new Item("glass", 1.00));

    String id = glass.block().get_id();

    Mono<Item> mono = itemService.getItem(id);
    Item item = mono.block();

    assertThat(item).isNotNull();
    assertThat(item.getName()).isEqualTo("glass");
    assertThat(item.getPrice()).isEqualTo(1.00);

    Mono<Item> mono2 = itemService.getItem(id);
    Item item2 = mono2.block();

    assertThat(item2).isNotNull();
    assertThat(item2.getName()).isEqualTo("glass");
    assertThat(item2.getPrice()).isEqualTo(1.00);
}

在控制枱中,我們可以看到以下輸出(為了簡潔,僅顯示了必要的部分):

Inserting Document containing fields: [name, price, _class] in collection: item...
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '618817a52bffe4526c60f6c0' in cache(s) [items]
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "618817a52bffe4526c60f6c0"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item...
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '618817a52bffe4526c60f6c0' found in cache 'items'
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item

在第一行,我們看到我們的 insert 方法。隨後,當 getItem 被調用時,Spring 會檢查緩存中是否存在該項,但未找到,因此會訪問 MongoDB 以檢索該記錄。在第二次調用 getItem 的時候,Spring 再次檢查緩存並找到該鍵的條目,但仍然會訪問 MongoDB 以檢索該記錄。

之所以發生這種情況,是因為 Spring 緩存了 getItem 方法的結果,該結果是 Mono 包裝器對象。但是,對於結果本身,它仍然需要從數據庫中檢索記錄。

在後續部分,我們將提供此問題的解決方法。

3. 緩存 Mono/Flux 的結果

MonoFlux 內置了緩存機制,我們可以利用它作為一種臨時解決方案。正如我們之前所説,@Cacheable 緩存的是包裝對象,而藉助內置緩存,我們可以創建一個指向實際服務方法結果的引用。

@Cacheable("items")
public Mono<Item> getItem_withCache(String id) {
    return repository.findById(id).cache();
}

讓我們用這個新的服務方法運行上章的測試。輸出結果如下所示:

Inserting Document containing fields: [name, price, _class] in collection: item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '6189242609a72e0bacae1787' in cache(s) [items]
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "6189242609a72e0bacae1787"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item
findOne using query: { "_id" : { "$oid" : "6189242609a72e0bacae1787"}} fields: {} in db.collection: test.item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '6189242609a72e0bacae1787' found in cache 'items'

我們幾乎可以得到相似的輸出。但這次,當項目的緩存中存在時,不再進行額外的數據庫查找。 採用此解決方案,當緩存過期時存在潛在問題。 由於我們使用了緩存層級,因此我們需要為兩個緩存設置適當的過期時間。 經驗法則如下: Flux 緩存的 TTL 應該比 @Cacheable 更長。

4. 使用 Caffeine

由於 Reactor 3 添加項將在下一版本中被棄用(從 3.6.0 開始),我們將僅使用 Caffeine 來展示緩存的實現。對於此示例,我們將配置 Caffeine 緩存:

public ItemService(ItemRepository repository) {
    this.repository = repository;
    this.cache = Caffeine.newBuilder().build(this::getItem_withAddons);
}

ItemService 構造函數中,我們使用最小配置初始化 Caffeine 緩存,並在新的服務方法中使用該緩存:

@Cacheable("items")
public Mono<Item> getItem_withCaffeine(String id) {
    return cache.asMap().computeIfAbsent(id, k -> repository.findById(id).cast(Item.class)); 
}

當我們重新運行之前的測試時,我們會得到與先前示例中相似的輸出。

5. 結論

在本文中,我們探討了 Spring WebFlux 如何與 @Cacheable 交互。此外,我們還描述了它們的使用方法以及一些常見問題。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.