5

SpringBoot中实现两级缓存

 1 month ago
source link: https://www.jdon.com/73033.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SpringBoot中实现两级缓存

缓存数据意味着我们的应用程序不必访问速度较慢的存储层,从而提高其性能和响应能力。我们可以使用任何内存实现库(例如Caffeine )来实现缓存。

虽然这样做提高了数据检索的性能,但如果应用程序部署到多个副本集,则实例之间不会共享缓存。为了克服这个问题,我们可以引入一个可以被所有实例访问的分布式缓存层。

在这篇文章中,我们将学习如何在Spring中实现二级缓存机制。我们将展示如何使用 Spring 的缓存支持来实现这两个层,以及如果本地缓存层发生缓存未命中,如何调用分布式缓存层。

首先,让我们包含spring-boot-starter-web 依赖项:

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

我们将实现一个从存储库获取数据的 Spring 服务。

首先,我们对Customer类进行建模:

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> {
}

接下来,我们来实现两级缓存。

实现一级缓存
我们将利用 Spring 的缓存支持和 Caffeine 库来实现第一个缓存层。

让我们包含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>

要启用咖啡因缓存,我们需要添加一些与缓存相关的配置。

首先,我们在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());
    }
}

接下来,让我们使用SimpleCacheManager类添加CaffeineCacheManager bean并设置缓存配置:

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

要启用上述缓存,我们需要在getCustomer方法中添加@Cacheable注解 :

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

正如前面所讨论的,这在单实例部署环境中效果很好,但当应用程序运行多个副本时,效果就不那么有效了。

实现二级缓存
我们将使用Redis服务器实现第二级缓存。当然,我们可以使用任何其他分布式缓存(例如Memcached)来实现它。我们应用程序的所有副本都可以访问这一层缓存。

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

启用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();
}

我们将使用@Caching和@Cacheable注释在getCustomer方法中包含第二个缓存:

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

我们应该注意到Spring 将从第一个可用的缓存中获取缓存对象。如果两个缓存管理器都未命中,它将运行实际的方法。

实现自定义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 代理方法内部调用 get 缓存方法时,自定义拦截器都会拦截该调用。

实施集成测试
为了验证我们的设置,我们将实施一些集成测试并验证两个缓存。

首先,让我们创建一个集成测试来使用嵌入式 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);<code class="language-java">

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 不管理多个缓存之间的任何数据,即使它们是为同一个方法声明的。

这告诉我们,每当再次访问一级缓存时,我们都需要更新一级缓存。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK