审校 | 重楼
缓存是提高应用性能的有效方法。我们之前发布过一篇文章,介绍了缓存的概念和好处,主要针对 Spring Boot 进行了讨论。 在本文中,我们将探讨优化 Spring Boot 应用程序缓存的 7 种技术。
目录
1- 确定待缓存的对象
2- 缓存过期
淘汰策略
基于时间的过期策略
自定义淘汰策略
3- 条件缓存
4- 分布式缓存 vs 本地缓存
什么是本地缓存?
何时使用本地缓存 Vs. 分布式缓存?
在Spring Boot中实现本地缓存
5- 自定义键生成策略
6- 异步缓存
7- 监控缓存指标以发现瓶颈
如何在Spring Boot中监控缓存指标
总结
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 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 实现本地缓存,我们需要添加以下依赖项:
org.springframework.boot
spring-boot-starter-cache
com.github.ben-manes.caffeine
caffeine
然后使用 @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);
}
使用自定义键生成策略可以显著提升应用程序缓存的性能。设计良好的键生成策略能够确保缓存条目的唯一性,最大限度地减少缓存丢失,并提高缓存命中率。
6. 异步缓存
如你所见,Spring 缓存抽象 API 是阻塞且同步的。如果你使用 WebFlux 栈,通过 Spring 缓存注解(如 @Cacheable 或 @CachePut)将缓存应用于 Reactor 包装对象(Mono 或 Flux)。在这种情况下,你有三种方法:
- 在 Reactor 类型上调用 cache() 方法,并在该方法上添加 Spring 缓存注解。
- 使用底层缓存提供程序的异步 API(如果支持),并以编程方式处理缓存。
- 实现一个围绕缓存 API 的异步包装器,使其支持异步操作(如果缓存提供程序不支持)。
然而,自Spring Framework 6.2 发布以来,如果缓存提供程序支持 WebFlux 项目的异步缓存(如 Caffeine Cache):
Spring 的声明式缓存基础设施会检测到返回 Mono 或 Flux 的反应式方法,并异步缓存其产生的值,而不是缓存返回的 Reactive Streams Publisher 实例。这需要目标缓存提供程序的支持,例如将 CaffeineCacheManager 设置为 setAsyncCacheMode(true)。
配置示例如下:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(buildCaffeineCache());
cacheManager.setAsyncCacheMode(true); // <--
return cacheManager;
}
}
7. 监控缓存指标以发现瓶颈
监控缓存指标对于识别瓶颈和优化应用程序中的缓存策略至关重要。 需要监控的关键指标包括:
- 缓存命中率:缓存命中次数与总请求次数的比率,表明缓存的有效性。低命中率表明缓存未被有效利用。
- 缓存未命中率:缓存未命中次数与总请求次数的比率,表明缓存经常无法提供请求的数据,可能是由于缓存大小不足或键管理不当。
- 缓存淘汰率:缓存中项目被淘汰的频率。高淘汰率表明缓存大小过小或淘汰策略不适合当前的访问模式。
- 内存使用量:缓存使用的内存量。
- 延迟:从缓存中检索数据所需的时间。
- 错误率:通常指的是系统在处理请求时遇到的错误数量或比例。错误率可以包括缓存无法响应请求、超时错误等。
如何在 Spring Boot 中监控缓存指标
Spring Boot Actuator 启动时会自动为所有可用的 Cache 实例配置 Micrometer。对于启动后,动态创建或以编程方式创建的缓存,需要进行注册。请查看相关文档以确保您的缓存提供程序得到支持。 首先,添加 Actuator 和 Micrometer 依赖项:
org.springframework.boot
spring-boot-starter-actuator
io.micrometer
micrometer-registry-prometheus
然后,启用 Actuator 端点:
management.endpoints.web.exposure.include=*
现在,可以使用 /actuator/caches 端点查看配置的缓存列表。对于缓存指标,可以使用以下端点:
- /actuator/metrics/cache.gets
- /actuator/metrics/cache.puts
- /actuator/metrics/cache.evictions
- /actuator/metrics/cache.removals
总结
在本文中,我们详细学习了 7 种优化 Spring Boot 应用缓存的技术。优化缓存至关重要,因为它通过减少后端系统的负载和加快数据检索速度,直接提升了应用的性能和可扩展性。高效的缓存策略能够最小化延迟,确保更快的响应时间,从而显著改善整体用户体验。
译者介绍
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号,博客阅读量 400W+,粉丝 3W+。2022 年腾讯云优秀创作者,2022 年阿里云技术社区最受欢迎技术电子书 TOP 10 《性能优化方法论》作者,慕课网:剖析《阿里巴巴 Java 开发手册》、深度解读《Effective Java》 技术专栏作者。
原文Top 7 Techniques to Optimize Caching in Spring Boot,作者:Saeed Zarinfam