知識庫 / Spring RSS 訂閱

使用 Spring 實現兩級緩存

Spring
HongKong
4
11:17 AM · Dec 06 ,2025

1. 概述

緩存數據意味着我們的應用程序不必訪問較慢的存儲層,從而提高其性能和響應速度。我們可以使用內存實現庫(如 Caffeine)來實現緩存。

雖然這樣做可以提高數據檢索的性能,但如果應用程序部署在多個副本集中,則緩存在不同實例之間不會共享。為了解決這個問題,我們可以引入一個可供所有實例訪問的分佈式緩存層。

在本教程中,我們將學習如何使用 Spring 實現雙層緩存機制。我們將使用 Spring 的緩存支持來實現這兩種層,並演示當本地緩存層發生緩存未命中時,如何調用分佈式緩存層。

2. 在 Spring Boot 中示例應用程序

讓我們假設我們需要構建一個簡單的應用程序,該應用程序調用數據庫以檢索一些數據。

2.1. Maven 依賴

首先,添加 spring-boot-starter-web 依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.5</version>
</dependency>

2.2. 實現 Spring 服務

我們將實現一個從倉庫獲取數據的 Spring 服務。

首先,讓我們對 客户 類進行建模:

public class Customer implements Serializable {
    private String id;
    private String name;
    private String email;
    // standard getters and setters
}

然後我們來實現 CustomerService 類和 getCustomer 方法:

@Service
public class CustomerService {
    
    private final CustomerRepository customerRepository;

    public Customer getCustomer(String id) {
        return customerRepository.getCustomerById(id);
    }
}

最後,讓我們定義 CustomerRepository 接口:

public interface CustomerRepository extends CrudRepository<Customer, String> {
}

現在我們將實現兩級緩存。

3. 實現第一層緩存

我們將利用 Spring 的緩存支持和 Caffeine 庫來實現第一層緩存。

3.1. 咖啡因依賴

請包含以下依賴項:spring-boot-starter-cachecaffeine

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>3.1.5</version/
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

3.2. 啓用咖啡因緩存

要啓用咖啡因緩存,我們需要添加一些與緩存相關的配置。

首先,我們在 CacheConfig 類中添加 @EnableCaching 註解,幷包含一些 Caffeine 緩存配置:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CaffeineCache caffeineCacheConfig() {
        return new CaffeineCache("customerCache", Caffeine.newBuilder()
          .expireAfterWrite(Duration.ofMinutes(1))
          .initialCapacity(1)
          .maximumSize(2000)
          .build());
    }
}

接下來,我們將使用 CaffeineCacheManager Bean 以及 SimpleCacheManager 類,並設置緩存配置:

@Bean
public CacheManager caffeineCacheManager(CaffeineCache caffeineCache) {
    SimpleCacheManager manager = new SimpleCacheManager();
    manager.setCaches(Arrays.asList(caffeineCache));
    return manager;
}

3.3. 添加 @Cacheable 註解

為了啓用上述緩存機制,我們需要在 getCustomer 方法中添加 @Cacheable 註解:

@Cacheable(cacheNames = "customerCache", cacheManager = "caffeineCacheManager")
public Customer getCustomer(String id) {
}

正如之前討論的,此方案在單實例部署環境中效果良好,但在應用程序同時運行多個副本時效果不佳

4. 實現二級緩存

我們將使用 Redis 服務器來實現二級緩存。當然,我們也可以使用其他分佈式緩存,例如 Memcached。 該層級緩存將對我們應用程序的所有副本可訪問

4.1. Redis 依賴

添加 spring-boot-starter-redis 依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.1.5</version>
</dependency>

4.2. 啓用 Redis 緩存

我們需要將 Redis 緩存相關的配置添加到應用程序中,以啓用它。

首先,讓我們使用以下幾項屬性配置 RedisCacheConfiguration Bean:

@Bean
public RedisCacheConfiguration cacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
      .entryTtl(Duration.ofMinutes(5))
      .disableCachingNullValues()
      .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}

然後,我們使用 RedisCacheManager 類啓用 CacheManager

@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory, RedisCacheConfiguration cacheConfiguration) {
    return RedisCacheManager.RedisCacheManagerBuilder
      .fromConnectionFactory(connectionFactory)
      .withCacheConfiguration("customerCache", cacheConfiguration)
      .build();
}

4.3. 包含 @Caching a@Cacheable 註解

我們將使用 @Caching@Cacheable 註解,在 getCustomer 方法中添加第二個緩存:

@Caching(cacheable = {
  @Cacheable(cacheNames = "customerCache", cacheManager = "caffeineCacheManager"),
  @Cacheable(cacheNames = "customerCache", cacheManager = "redisCacheManager")
})
public Customer getCustomer(String id) {
}

我們應該注意的是,Spring 將從第一個可用的緩存中獲取緩存對象如果兩個緩存管理器都失效,它將運行實際的方法。

5. 實現集成測試

為了驗證我們的配置,我們將實施一些集成測試並驗證兩個緩存。

首先,我們將創建一個集成測試,使用嵌入式 Redis 服務器來驗證這兩個緩存:

@Test
void givenCustomerIsPresent_whenGetCustomerCalled_thenReturnCustomerAndCacheIt() {
    String CUSTOMER_ID = "100";
    Customer customer = new Customer(CUSTOMER_ID, "test", "[email protected]");
    given(customerRepository.findById(CUSTOMER_ID))
      .willReturn(customer);
    
    Customer customerCacheMiss = customerService.getCustomer(CUSTOMER_ID);
    
    assertThat(customerCacheMiss).isEqualTo(customer);
    verify(customerRepository, times(1)).findById(CUSTOMER_ID);
    assertThat(caffeineCacheManager.getCache("customerCache").get(CUSTOMER_ID).get()).isEqualTo(customer);
    assertThat(redisCacheManager.getCache("customerCache").get(CUSTOMER_ID).get()).isEqualTo(customer);
}

我們將運行上述測試用例,並發現它能夠正常工作。

接下來,讓我們設想一個場景,其中第一級緩存數據因過期而被清除,然後嘗試獲取相同的客户。 此時,應該在 Redis 的第二級緩存中發生緩存命中。 對於相同的客户,任何進一步的緩存命中都應該在第一級緩存中。

讓我們實現上述測試場景,以檢查本地緩存過期後兩級緩存的狀態。

@Test
void givenCustomerIsPresent_whenGetCustomerCalledTwiceAndFirstCacheExpired_thenReturnCustomerAndCacheIt() throws InterruptedException {
    String CUSTOMER_ID = "102";
    Customer customer = new Customer(CUSTOMER_ID, "test", "[email protected]");
    given(customerRepository.findById(CUSTOMER_ID))
      .willReturn(customer);

    Customer customerCacheMiss = customerService.getCustomer(CUSTOMER_ID);
    TimeUnit.SECONDS.sleep(3);
    Customer customerCacheHit = customerService.getCustomer(CUSTOMER_ID);

    verify(customerRepository, times(1)).findById(CUSTOMER_ID);
    assertThat(customerCacheMiss).isEqualTo(customer);
    assertThat(customerCacheHit).isEqualTo(customer);
    assertThat(caffeineCacheManager.getCache("customerCache").get(CUSTOMER_ID).get()).isEqualTo(customer);
    assertThat(redisCacheManager.getCache("customerCache").get(CUSTOMER_ID).get()).isEqualTo(customer);
}

當我們運行上述測試時,我們會看到Caffeine緩存對象上出現意外的斷言錯誤

org.opentest4j.AssertionFailedError: 
expected: Customer(id=102, name=test, [email protected])
but was: null
...
at com.baeldung.caching.twolevelcaching.CustomerServiceCachingIntegrationTest.
givenCustomerIsPresent_whenGetCustomerCalledTwiceAndFirstCacheExpired_thenReturnCustomerAndCacheIt(CustomerServiceCachingIntegrationTest.java:91)

從以上日誌來看,客户對象在清理後並未出現在 Caffeine 緩存中,即使再次調用相同的邏輯,也不會從第二個緩存中恢復。對於該用例而言,這種情況並不理想,因為當第一個緩存過期時,它永遠不會被更新,直到第二個緩存也過期,從而給 Redis 緩存帶來額外的負載。

需要注意的是,Spring 不會管理多個緩存之間的數據,即使這些緩存被聲明為同一個方法

這告訴我們,每次訪問時都需要更新第一級緩存

6. 實現自定義 CacheInterceptor

為了更新第一個緩存,我們需要實現一個自定義緩存攔截器,以便在訪問緩存時進行攔截。

我們將添加一個攔截器來檢查當前緩存類是否為 Redis 類型,如果本地緩存不存在,則可以更新緩存值。

讓我們通過重寫 doGet 方法來實現一個自定義 CacheInterceptor

public class CustomerCacheInterceptor extends CacheInterceptor {

    private final CacheManager caffeineCacheManager;

    @Override
    protected Cache.ValueWrapper doGet(Cache cache, Object key) {
        Cache.ValueWrapper existingCacheValue = super.doGet(cache, key);
      
        if (existingCacheValue != null && cache.getClass() == RedisCache.class) {
            Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
            if (caffeineCache != null) {
                caffeineCache.putIfAbsent(key, existingCacheValue.get());
            }
        }

        return existingCacheValue;
    }
}

我們還需要註冊 CustomerCacheInterceptor Bean 以啓用它:

@Bean
public CacheInterceptor cacheInterceptor(CacheManager caffeineCacheManager, CacheOperationSource cacheOperationSource) {
    CacheInterceptor interceptor = new CustomerCacheInterceptor(caffeineCacheManager);
    interceptor.setCacheOperationSources(cacheOperationSource);
    return interceptor;
}

@Bean
public CacheOperationSource cacheOperationSource() {
    return new AnnotationCacheOperationSource();
}

需要注意的是,自定義攔截器會在 Spring 代理方法內部調用獲取緩存方法時進行攔截。

我們將重新運行集成測試,並確認上述測試用例通過。

7. 結論

在本文中,我們學習瞭如何使用 Spring 的緩存支持,結合 Caffeine 和 Redis 實現兩級緩存。我們還演示瞭如何使用自定義緩存攔截器實現對第一級 Caffeine 緩存的更新。

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

發佈 評論

Some HTML is okay.