Spring Boot 缓存优化攻略:5个不可错过的技巧

129 阅读9分钟

​缓存是提高应用性能的有效方法,在本文中,我们将探讨优化 Spring Boot 应用程序缓存的 7 种技术。

1.确定待缓存的对象

首先,我们需要明确哪些对象最适合缓存。一般而言,那些代价高昂且耗时的操作的结果需要优先考虑,例如数据库查询、网络服务调用或复杂计算的结果。然而,定义一些理想缓存候选对象的通用特征将更重要。这些特征有助于我们在应用程序中识别适合缓存的对象:

  • 频繁访问的数据:经常被访问和重复访问的数据是良好的缓存候选对象。
  • 代价高昂的获取或计算:需要大量时间或计算资源来检索或处理的数据。
  • 静态或变化较少的数据:变化不频繁的数据,确保缓存的数据在较长时间内保持有效。
  • 高读写比率:当数据被访问的频率远高于修改或更新的频率时,可以有效地进行缓存。这保证了缓存快速读取的优势超过其更新成本。
  • 可预测的访问模式:遵循可预测访问模式的数据,允许更高效的缓存管理。

这些特征可以帮助我们有效地识别和缓存能够显著提升应用程序性能的数据。 既然我们知道如何找到理想的缓存候选对象,就可以开始在 Spring Boot 应用程序中启用缓存。可以使用注解或编程方式进行缓存配置。我在这篇文章中详细讨论了如何在 Spring Boot 中使用缓存,以及_ Digma_ 如何帮助我们发现缓存未命中或识别缓存候选对象。

2.缓存过期

缓存过期策略设置得当可以确保缓存数据的有效性和及时更新,提高内存利用率,从而优化 Spring Boot 应用程序的性能和一致性。以下是一些推荐的管理 Spring Boot 应用程序中缓存过期的方法:

淘汰策略

常见的淘汰策略包括:

  • 最近最少使用(LRU):优先淘汰最近最少访问的对象。
  • 最不经常使用(LFU):优先淘汰访问频率最低的对象。
  • 先进先出(FIFO):优先淘汰最早放入缓存的对象。

虽然 Spring Cache 抽象本身不直接支持这些淘汰策略,但你可以根据所选的缓存提供者使用其特定配置。通过仔细选择和配置合适的淘汰策略,可以确保缓存机制高效运行,并与应用程序的性能和资源利用目标相一致。

基于时间的过期策略

定义缓存条目的生存时间(TTL)在不同缓存提供者中有所不同。例如,在 Spring Boot 应用程序中使用 Redis 进行缓存时,可以通过以下配置指定生存时间:

复制

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

如果缓存提供者不支持 TTL,可以使用@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注解都具有condition和unless属性,允许我们为缓存项定义条件:

  • condition:指定一个 SpEL(Spring表达式语言)表达式,该表达式必须评估为true,数据才会被缓存(或更新)。
  • unless:指定一个 SpEL 表达式,该表达式必须评估为false,数据才会被缓存(或更新)。

为了更清楚,请看以下代码示例:

复制

@Cacheable(value = "employeeByName", condition = "#result.size() > 10", unless = "#result.size() < 1000")
public List<Employee> employeesByName(String name) {
    // 检索数据的方法逻辑
    return someEmployeeList;
}

在这段代码中,只有当结果列表的大小大于 10 且小于 1000 时,员工列表才会被缓存。 最后一点,与前一部分类似,我们也可以使用CacheManager和Cache接口以编程方式实现条件缓存。这提供了更多的灵活性和对缓存行为的控制。

4.分布式缓存 vs. 本地缓存

谈到缓存,我们通常会想到分布式缓存,如 Redis、Memcached 或 Hazelcast。在微服务架构盛行的时代,本地缓存也在提升应用性能方面发挥了重要作用。 理解本地缓存和分布式缓存之间的差异,有助于选择合适的策略来优化 Spring Boot 应用中的缓存。每种类型都有其优缺点,根据应用需求进行权衡至关重要。

什么是本地缓存?

本地缓存是一种缓存机制,其中数据存储在与应用运行的同一台机器或实例的内存中。一些知名的本地缓存库包括 Ehcache、Caffeine 和 Guava Cache。 本地缓存允许快速访问缓存数据,因为它避免了与远程数据检索(分布式缓存)相关的网络延迟和开销。本地缓存通常比分布式缓存更易于设置和管理,并且不需要额外的基础设施。

何时使用本地缓存 vs. 分布式缓存?

本地缓存适用于小型应用程序或数据集较小且可以舒适地放入单台机器内存中的微服务。它也适用于低延迟至关重要且实例间数据一致性不是主要问题的场景。本地缓存的优势在于其速度快、设置和管理简单。 另一方面,分布式缓存系统适用于具有大量数据缓存需求的大规模应用,在这些应用中,可伸缩性、容错性和多个实例间的数据一致性至关重要。分布式缓存能够分担数据存储负担,并在节点故障时提供数据冗余。

在 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>

然后使用 @EnableCaching 注解启用缓存:

复制

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

除了通用的 Spring 缓存配置外,我们还可以使用特定的配置来调整 Caffeine 缓存,如下所示:

复制

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

5. 自定义键生成策略

Spring 缓存注解中的默认键生成算法通常如下:

如果没有参数,则返回 0。 如果只有一个参数,则返回该实例。 如果有多个参数,则返回由所有参数的哈希值计算出的键。 只要 hashCode() 能准确反映对象的自然键,这种方法对具有自然键的对象效果良好。

但在某些情况下,默认的键生成策略效果并不好:

  • 我们需要有意义的键。
  • 方法有多个相同类型的参数。
  • 方法具有可选参数或空参数。
  • 我们需要在键中包含上下文数据,如区域、租户 ID 或用户角色,以使其唯一。

Spring Cache 提供了两种定义自定义键生成策略的方法:

  • 通过 key 属性指定一个 SpEL(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);
}

使用自定义键生成策略可以显著提升应用程序缓存的性能。设计良好的键生成策略能够确保缓存条目的唯一性,最大限度地减少缓存丢失,并提高缓存命中率。

总结

以上介绍了5个Spring Boot 缓存优化攻略,作为开发者,我们需要保持好奇心和学习热情,不断探索新的技术,只有这样,我们才能在这个快速发展的时代中立于不败之地。介绍一款程序员都应该知道的软件JNPF快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。

JNPF可以实现应用从创建、配置、开发、测试到发布、运维、升级等完整生命周期的管理。减少了传统应用程序的代码编写量,通过图形化、可视化的界面,以拖放组件的方式,即可快速生成应用程序的产品,大幅降低了开发企业管理类软件的难度。

感谢阅读本文,如果有什么建议,请在评论中让我知道。我很乐意改进。