Spring Boot中7种优化缓存方法

在本文中,列举了 7 种在 Spring Boot 应用程序中优化缓存的技术。优化缓存至关重要,因为它通过减少后端系统的负载和加快数据检索速度直接增强了应用程序的性能和可扩展性。高效的缓存策略可最大限度地减少延迟,确保更快的响应时间,并改善整体用户体验。


1- 确定理想候选
首先,我们需要了解缓存的理想候选者。
我们首先想到的是缓存昂贵且耗时的操作,例如查询数据库、调用 Web 服务或执行复杂的计算。
这些都是正确的候选者,但定义一些理想的缓存候选者的一般特征是有帮助的。这些准备将帮助我们在应用程序中识别这些特征:

  • 频繁访问的数据:经常且重复访问的数据适合缓存。
  • 昂贵的获取或计算:需要大量时间或计算资源来检索或处理的数据。
  • 静态或很少变化的数据:不经常变化的数据,确保缓存的数据在较长时间内保持有效。
  • 高读写比:访问频率高于修改或更新频率的数据可以得到有效缓存。这保证了从缓存快速读取的好处超过更新带来的成本。
  • 可预测模式:遵循可预测访问模式的数据,可以实现更高效的缓存管理。

这些特性可以帮助我们有效地识别和缓存能够为我们的应用程序带来最显著的性能提升的数据。


2-缓存过期
设置适当的过期策略可确保我们的缓存数据保持有效、 最新且内存高效。它将优化您的 Spring Boot 应用程序的性能和一致性。

建议使用这些方法来管理 Spring Boot 应用程序中的缓存过期:

驱逐策略:
有以下著名的驱逐策略:

  • 最近最少使用(LRU):首先驱逐最近最少访问的项目。
  • 最不频繁使用(LFU):首先驱逐最不频繁访问的项目。
  • 先进先出(FIFO):首先淘汰访问频率最低的项目。

Spring Cache 抽象不支持这些驱逐策略,但您可以根据所选的提供程序使用缓存提供程序的特定配置。通过仔细选择和配置驱逐策略,您可以确保您的缓存机制保持高效、有效,并与应用程序的性能和资源利用率目标保持一致。

基于时间的到期:
对于每个缓存提供程序,定义在一定时间段后清除缓存条目的生存时间 (TTL) 间隔是不同的。例如,在Redis我们的 Spring Boot 应用程序中使用缓存的情况下,我们可以使用此配置指定生存时间:

spring.cache.redis.time-to-live=10m

如果你的缓存提供程序不支持生存时间,你可以使用注释@CacheEvict和调度程序来实现它,如下所示:

@CacheEvict (value = "cache1" , allEntries = true
    @Scheduled (fixedRateString =
"${your.config.key.for.ttl.in.milli}"
    public void emptyCache1 () { 
       
// 刷新缓存,除了描述性日志外,我们不需要在这里编写任何代码!
     }

自定义驱逐策略:通过为单个缓存条目或所有条目定义基于事件或情况的自定义过期策略,我们可以防止缓存污染并保持其一致性。 Spring Boot 有不同的注解来支持自定义过期策略:

  • @CacheEvict: 从缓存中删除一个或所有条目。
  • @CachePut: 使用新值更新条目。
  • CacheManager:缓存管理器: 我们可以使用 Spring 的 CacheManager 和 Cache 接口实现自定义驱逐策略。 为此,我们可以使用 evict()、put() 或 clear() 等方法。 我们还可以使用 getNativeCache() 方法访问底层缓存提供程序,以获得更多功能。

定制驱逐政策最重要的是找到合适的驱逐地点和条件。

3-条件缓存
条件缓存与驱逐策略一起在优化缓存策略中发挥着重要作用。在某些情况下,我们不需要将特定实体的所有数据存储在缓存中,

条件缓存确保只有满足特定条件的数据才会存储在缓存中。

这样可以防止缓存空间中不必要的数据,从而优化资源利用率。

@Cacheable 和 @CachePut 注解都有条件和除非属性,允许我们定义缓存项的条件:

  • 条件: 指定 SpEL(Spring Expression Language,Spring 表达式语言)表达式,该表达式必须求值为真才能缓存(或更新)数据。
  • 除非: 指定一个 SpEL 表达式,该表达式必须求值为 false 才能缓存(或更新)数据。

为了澄清起见,看一下这段代码:

@Cacheable(value = "employeeByName", condition = "#result.size() > 10", unless = "#result.size() < 1000")
public List<Employee> employeesByName(String name) {
   
// Method logic to retrieve data
    return someEmployeeList;
}

在这段代码中,只有当结果列表的大小大于 10 且小于 1000 时,雇员列表才会被缓存。

最后一点是,与上一节类似,我们也可以使用 CacheManager 和 Cache 接口以编程方式实现条件缓存。 这为缓存行为提供了更多的灵活性和控制。

4-分布式缓存与本地缓存
当我们谈论缓存时,我们通常会想到分布式缓存,例如 Redis,Memcached 或 Hazelcast。在微服务架构流行的时代,本地缓存在提高应用程序性能方面也发挥着很大的作用。

了解本地缓存和分布式缓存之间的区别可以帮助您选择正确的策略来优化我们的 Spring Boot 应用程序中的缓存。

每种类型都有其优点和缺点,必须根据您的应用程序需求来考虑。

什么是本地缓存?
本地缓存是一种缓存机制,其中数据存储在应用程序运行的同一台机器或实例的内存中。一些著名的本地缓存库是 Ehcache、Caffeine 和 Guava Cache。

本地缓存可以非常快速地访问缓存数据,因为它可以避免与远程数据检索相关的网络延迟和开销(分布式缓存)。本地缓存通常比分布式缓存更易于设置和管理,并且不需要额外的基础设施。

何时应该使用本地缓存,何时应该使用分布式缓存?
本地缓存适用于小型应用程序或微服务,其中数据集较小,可以轻松容纳在单台计算机的内存中。它也适用于低延迟至关重要且跨实例数据一致性不是主要问题的场景。

另一方面,分布式缓存系统适用于具有大量数据缓存需求的大型应用程序,对于这种应用程序来说,可扩展性、容错性和跨多个实例的数据一致性至关重要。

在 Spring Boot 中实现本地缓存
Spring Boot 通过各种内存缓存提供程序(如Ehcache、Caffeine或ConcurrentHashMap )支持本地缓存。我们唯一需要做的就是添加所需的依赖项并在 Spring Boot 应用程序中启用缓存。例如,要使用 Caffeine 进行本地缓存,我们需要添加以下依赖项:

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


@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

spring:
  cache:
    caffeine:
      spec: maximumSize=500,expireAfterAccess=10m


5- 自定义键生成策略
Spring 缓存注解中的默认键生成算法通常是这样的:

  • 如果没有给出参数,则返回 0。
  • 如果仅给出一个参数,则返回该实例。
  • 如果给出了多个参数,则返回根据所有参数的哈希值计算出的键。

只要 hashCode() 能反映出自然键,这种方法就能很好地适用于具有自然键的对象。

但在某些场景下,默认的键生成策略并不能很好地发挥作用:

  • 我们需要有意义的键
  • 具有同一类型的多个参数的方法
  • 具有可选参数或空参数的方法
  • 我们需要在键中包含上下文数据,例如语言环境、宗旨 ID 或用户角色,以使其具有唯一性

Spring Cache 提供了两种方法来定义自定义键生成策略:
  • 为键属性指定 SpEL(Spring Expression Language,Spring 表达式语言)表达式,生成新键时必须对该表达式进行评估:

@CachePut(value = "phonebook", key = "#phoneNumber.name")
    PhoneNumber create(PhoneNumber phoneNumber) {
        return phonebookRepository.insert(phoneNumber);
    }

  • 定义一个实现了 KeyGenerator 接口的 Bean,然后将其指定为 keyGenerator 属性:

@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return
"UNIQUE_KEY";
    }
}

///////

@CachePut(value =
"phonebook", keyGenerator = "customKeyGenerator")
PhoneNumber create(PhoneNumber phoneNumber) {
    return phonebookRepository.insert(phoneNumber);
}

在我们的应用中,使用自定义键生成策略可以大大提高高速缓存的效率和有效性。 精心设计的键生成策略可确保正确、唯一地识别高速缓存条目,从而最大限度地减少高速缓存丢失,最大限度地提高高速缓存命中率。

6- 异步缓存
您可能已经注意到,Spring 缓存抽象 API 是阻塞和同步的,如果您使用 WebFlux 堆栈,使用 Spring 缓存注解(如 @Cacheable 或 @CachePut)将缓存反应堆包装器对象(Mono 或 Flux)。 在这种情况下,你有三种方法:

  • 调用反应器类型上的 cache() 方法,并使用 Spring Cache 注释对该方法进行注解。
  • 使用底层缓存提供商提供的异步 API(如果支持),并以编程方式处理缓存。
  • 围绕缓存 API 实施异步封装,并使其异步化(如果缓存提供商不支持)。

不过,在 Spring Framework 6.2 发布后,如果缓存提供程序支持 WebFlux 项目的异步缓存(如 Caffeine Cache):Spring 的声明式缓存基础架构会检测反应式方法签名(如返回 Reactor Mono 或 Flux),并专门处理此类方法,以便对其生成的值进行异步缓存,而不是尝试缓存返回的反应式流发布器实例本身。 这需要目标缓存提供商的支持,例如将 CaffeineCacheManager 设置为 setAsyncCacheMode(true)。

@Configuration
@EnableCaching
public class CacheConfig {
  @Bean
  public CacheManager cacheManager() {
    final CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(buildCaffeineCache());
    cacheManager.setAsyncCacheMode(true); // <--
    return cacheManager;
  }
}


7- 监控缓存以查找瓶颈
监控缓存指标对于识别应用程序中的瓶颈和优化缓存策略至关重要。
要监控的最重要的指标是:

  • 缓存命中率:缓存命中率与总缓存请求数之比表明缓存有效,而低命中率则表明缓存未得到有效利用。
  • 缓存未命中率:缓存未命中数与总缓存请求数之比,表明缓存经常无法提供所请求的数据,可能是由于缓存大小不足或密钥管理不善造成的。
  • 缓存驱逐率:缓存中项目被驱逐的频率。如果驱逐率很高,则表明缓存大小太小或驱逐策略不适合访问模式。
  • 内存使用情况:缓存使用的内存量。
  • 延迟:从缓存中检索数据所需的时间。
  • 错误率:与缓存服务器负载相关的指标,例如每秒请求数。