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-cache 和 caffeine。
<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 緩存的更新。