Spring中@Cacheable、@CacheEvict以及其他缓存相关注解的实用介绍

介绍

缓存是一种优化技术,它存储经常使用的数据或计算以使后续访问更快。在软件领域,缓存减少了耗时操作的数量,例如数据库调用或复杂计算。Spring框架是使用最广泛的Java框架之一,它提供了一组丰富的与缓存相关的注释,使缓存机制更加高效和简单。在这篇博文中,我们将深入研究一些与缓存相关的主要注释:@Cacheable、@CacheEvict等等。

为什么在Spring中使用缓存?

缓存的核心是一种以允许更快检索的方式存储经常访问的数据的方法。在软件应用程序中,由于现代数据驱动环境带来的独特挑战,它变得不可或缺。下面更深入地探讨了为什么缓存至关重要,尤其是在 Spring 应用程序中:

  • 减少延迟: 最重要的好处是数据检索时间的大幅减少。数据库,尤其是在重负载或查询复杂数据结构时,可能会给系统带来显着的延迟。缓存该数据意味着后续请求几乎可以立即检索该数据,从而带来更流畅的用户体验。
  • 减少数据库负载: 对数据库进行的每个查询都会消耗资源。这不仅仅与数据检索时间有关;还与数据检索时间有关。它还涉及 CPU 负载、内存使用和 I/O 操作。通过从缓存中提供数据,应用程序可以显着减少实际数据库命中次数,从而保留系统资源并可能降低成本,特别是在按使用量付费的云环境中。
  • 增强的可靠性: 数据库可能会出现故障,或者有时可能需要维护。当缓存的数据满足用户请求时,即使在数据库停机期间,应用程序也可以继续正常运行,尽管功能有限。
  • 成本效率: 频繁的数据库调用,尤其是在微服务等分布式架构中,可能会导致网络成本增加。缓存有助于减少这些调用,从而最大限度地减少与网络相关的成本。
  • 灵活的可扩展性: 随着应用程序的增长,对数据库的需求也随之增加。缓存允许应用程序更优雅地扩展。通过将大量数据检索操作卸载到缓存,您可以确保数据库不会因请求而不堪重负,从而为您赢得时间来制定策略和实施长期扩展解决方案。
  • 改善用户体验: 从用户的角度来看,性能是用户体验的一个重要方面。响应式应用程序比让用户等待的应用程序更能有效地留住用户。通过有效地使用缓存,Spring 应用程序可以为最终用户提供更快、响应更灵敏的体验。
  • 战略数据可用性: 缓存不仅仅与速度有关,还与速度有关。这也与策略有关。通过缓存机制,您可以确定缓存数据的优先级。这意味着高优先级数据(例如经常查看的项目或高需求配置)始终可以随时可用,从而确保在最重要的地方实现最佳应用程序性能。
  • 与 Spring 生态系统集成: Spring 提供与各种缓存解决方案的无缝集成,从 Redis 和 Ehcache 等内存数据库到 Hazelcast 等分布式缓存。这使得开发人员可以轻松地实现和配置适合其应用程序需求的缓存。

设置缓存管理器

缓存管理器是 Spring 应用程序中缓存机制的支柱。它负责管理缓存数据,确保有效存储、检索和逐出条目。如果没有它,注释就会知道@Cacheable或@CacheEvict不知道在哪里存储或检索数据。让我们探讨一下它的意义、配置以及 Spring 提供的各种选择:

缓存管理器的意义

  • 集中控制: 它为所有缓存操作提供集中控制点,确保一致性。
  • 生命周期管理: 缓存管理器监督缓存区域的生命周期,从创建到销毁。
  • 资源分配: 决定应向缓存分配多少内存或磁盘空间,并在达到这些限制时处理情况。

ConcurrentMapCacheManager 的基本配置

Spring 提供了一个简单的内存缓存管理器,称为ConcurrentMapCacheManager. 它基于Java ConcurrentHashMap,这意味着它是线程安全的并且适合大多数单实例应用程序。

less

复制代码

@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager () { return new ConcurrentMapCacheManager ( "books" , "authors" ); } } }

该@EnableCaching注释向 Spring 的缓存机制发出信号,以在整个应用程序中查找其他与缓存相关的注释。

多种缓存管理器选项

虽然ConcurrentMapCacheManagerSpring 非常适合简单的用例,但它还提供与其他几种缓存解决方案的集成,以满足不同的需求:

  • EhCache: 一个强大的、可扩展的、可配置的缓存解决方案。
  • Redis: 一种流行的内存数据结构存储,用作缓存或数据存储。
  • Hazelcast: 分布式内存数据网格,可用于微服务架构中的缓存。
  • Caffeine: 一个基于 Java 的高性能缓存库。

根据使用情况,您可能会选择其中一种。例如,分布式应用程序可能会从 Hazelcast 或 Redis 中受益更多,而单实例应用程序可能会发现 EhCache 或 Caffeine 更合适。

自定义缓存管理器

对于独特的用例,Spring 允许开发人员通过实现接口来创建自定义缓存管理器CacheManager。这提供了根据应用程序的需求精确定制缓存行为的灵活性。

调整和配置

除了设置之外,大多数缓存管理器还带有许多配置选项。这些可能包括与以下相关的设置:

  • 驱逐策略: 根据 LRU(最近最少使用)等策略决定何时删除项目。
  • 大小限制: 将缓存限制为特定大小。
  • TTL(生存时间): 设置一个时间,超过该时间后缓存的项目将被自动删除。
  • 持久性: 决定是否应将缓存数据写入磁盘。

监控与维护

许多缓存管理器都带有内置工具或集成,可以监控缓存的性能。这对于确保缓存发挥其增强性能的作用而不成为瓶颈来说是非常宝贵的。

@Cacheable

该@Cacheable注释告诉 Spring 将方法的返回值存储在指定的缓存中。后续使用相同参数调用此方法将从缓存中获取结果,而不是执行该方法。用法:

kotlin

复制代码

@Service public class BookService { @Cacheable("books") public Book findBookById (Long id) { // 模拟一个耗时方法 return databaseCallToFindBookById(id); } } }

每次使用findBookById相同的方法调用时id,Spring都会返回缓存的结果而不是执行该databaseCallToFindBookById方法。参数:

  • value或cacheNames: 存储结果的缓存的名称。
  • key: 用于动态计算密钥的 SpEL 表达式。
  • condition: SpEL 表达式,确定是否应缓存该方法。

@CacheEvict

虽然缓存很有用,但在某些情况下可能需要逐出(删除)缓存的数据。注释@CacheEvict就是用于此目的。它确保在特定操作下,缓存保持最新。用法:


kotlin

复制代码

@Service public class BookService { @CacheEvict(value = "books", key = "#id") public void deleteBookById (Long id) { // 从数据库中删除书籍的方法 actualDatabaseDeleteMethod(id); } }

当deleteBookById调用时,“books”缓存中的相应条目将被逐出。参数:

  • allEntries: 如果设置为true,它将清除指定缓存中的所有条目。
  • beforeInvocation: 如果设置为true,则会在方法执行之前清除缓存。

组合注释

Spring 的缓存注释不仅仅是单独的工具;它们是可以分层和组合的构建块,以根据特定应用程序需求制定复杂的缓存策略。这种组合注释的能力带来了无限的可能性。让我们深入研究一下细节:

为什么要合并注释?

  • 更丰富的行为: 单个注释是为特定任务设计的。将它们组合起来可以进行更复杂的操作,反映复杂的业务逻辑。
  • 代码清晰度: 在单个方法上使用多个注释可以清晰地概述所采用的缓存策略,使开发人员重新访问代码时更容易理解。
  • 动态缓存管理: 通过配对注释,您可以创建响应不同条件或触发器的动态缓存行为。

例子:

less

复制代码

@Cacheable("books") @CacheEvict(value = "books", allEntries=true) public Book ComplexBookOperation (Long id) { // 一些复杂的逻辑 }

在上面的方法中,@Cacheable确保如果complicatedBookOperation特定的结果id已在缓存中,则返回该结果而不执行该方法。同时,@CacheEvict确保执行该方法后,“books”缓存中的所有条目都被逐出。在该方法影响可能导致整个缓存过时的数据的情况下,这可能很有用。

组合注解的场景

  1. 刷新和驱逐: 考虑数据频繁更新但也定期访问的场景。在这里,结合@CachePut(更新缓存)@CacheEvict可以确保缓存始终是最新的,并且旧的不相关条目被逐出。
  2. 条件缓存: 通过使用@Cacheablewith 条件并将其与 结合使用@CacheEvict,您可以缓存某些结果,同时确保发生特定更新时缓存保持最新状态。
  3. 批量操作: 在数据批量操作很常见的应用程序中,组合多个缓存策略可确保缓存反映这些批量更改,而不会成为瓶颈或提供陈旧数据。

需要注意的事项

  1. 执行顺序: 组合注释时,必须了解它们的执行顺序以准确预测结果缓存行为。
  2. 性能影响: 虽然组合注释可以优化缓存,但如果不明智地进行,它也会带来性能开销。引入新的缓存组合时,请始终测试和监控应用程序的性能。
  3. 可读性: 虽然组合注释可以提供强大的功能,但过度使用可能会降低代码的可读性。力求优化和清晰度之间的平衡。

可扩展性@Caching

对于更高级的场景,您可能会发现自己组合了多个@Cacheable、@CacheEvict、 或@CachePut操作,Spring 提供了@Caching注释。当单个方法需要多个缓存操作时,这可以实现更清晰、更有组织的配置。

其他与缓存相关的注释

Spring 的缓存库并不限于众所周知的注释,例如@Cacheable和@CacheEvict。有一套鲜为人知的注释和属性可以改进缓存策略并提供对缓存行为的精细控制。下面进行全面的探索:

@CacheConfig

目的:

  • 此类级注释用于在类中的所有方法之间共享一些与缓存相关的通用设置。

好处:

  1. 一致性: 确保类中的所有缓存方法都遵循一致的配置。
  2. 减少冗余: 无需为每个方法指定公共属性,从而保持代码干燥(不要重复)。

用法:


kotlin

复制代码

@CacheConfig(cacheNames = "items") public class ItemService { @Cacheable public Item findItem (Long id) { // 这里的逻辑 } }

在这里,@Cacheable其中的方法ItemService将使用名为“items”的缓存,而无需明确指定它。

@CachePut

目的:

  • 确保方法的返回值始终放置在缓存中,从而在每次执行该方法时有效地更新缓存。

好处:

  1. 动态更新: 非常适合更新资源的方法,确保缓存始终是最新的。
  2. 多功能性: 可以与缓存行为相结合@CacheEvict或@Cacheable提供更广泛的缓存行为。

用法:

kotlin

复制代码

@CachePut(cacheNames = "items", key = "#item.id") public Item updateItem (Item item) { // 更新并返回 item }

@CacheConfig的cacheResolver

目的:

  • 该属性允许您定义CacheResolver在运行时动态解析缓存的自定义 bean,而不是直接定义缓存名称。

好处:

  1. 动态缓存解析: 对于需要动态确定缓存目标的场景很有用。
  2. 自定义逻辑集成: 提供在选择适当的缓存时合并自定义逻辑的机会。

用法:

kotlin

复制代码

@CacheConfig(cacheResolver = "myCacheResolver") public class DynamicCacheService { // 这里的方法 }

myCacheResolver的bean应该实现该CacheResolver接口并定义解析缓存的逻辑。

密钥生成

目的:

  • 虽然key许多与缓存相关的注释中的属性很方便,但有时您需要更动态的方式来生成键。Spring 允许通过keyGenerator属性生成自定义密钥。

好处:

  1. 复杂的键结构: 非常适合需要使用多个方法参数或其他动态数据构造缓存键的情况。
  2. 方法之间的一致性: 自定义密钥生成器可确保多个缓存方法之间的密钥生成策略一致。

用法:

kotlin

复制代码

@Cacheable(cacheNames = "orders", keyGenerator = "myKeyGenerator") public Order getOrder (Long userId, Date date) { // 这里的逻辑 }

myKeyGenerator应该实现该KeyGenerator接口并提供自定义密钥生成逻辑。

自定义缓存条件

目的:

  • @Cacheable类似或@CachePut支持属性的注释condition,仅在满足某些条件时才启用缓存操作。

好处:

  • 细粒度控制:提供对何时缓存的细粒度控制,以满足复杂的业务需求。

用法:


java

复制代码

@Cacheable(cacheNames = "premiumUsers", condition = "#user.isPremium()") public UserProfile getPremiumUserProfile (User user) { // 这里的逻辑 }

在此示例中,仅当所提供的用户是高级用户时才会进行缓存。

最佳实践

将缓存合并到 Spring 应用程序中可以显着提高性能。然而,为了最大限度地发挥其优势并避免陷阱,坚持最佳实践至关重要。以下是对注意事项的深入探讨:

了解业务需求

  1. 相关性: 确保缓存与当前的问题相关。并非所有方法或数据都受益于缓存。分析数据检索是否耗时或者数据是否在相当长的时间内保持静态。
  2. 新鲜度: 对于经常变化的数据,需要考虑性能和数据新鲜度之间的权衡。用户看到稍微过时的数据是否可以接受,或者实时数据是否至关重要?

最佳缓存配置

  1. 大小很重要: 为缓存分配适当的内存或存储大小。缓存过小可能会导致频繁驱逐,而缓存过大可能会浪费资源。
  2. 逐出策略: 选择与应用程序的访问模式一致的逐出策略(例如,LRU、LFU)。这可确保缓存随着时间的推移保持有效。

使用正确的缓存类型

  1. 内存中与分布式: 内存中缓存速度很快ConcurrentMapCacheManager,Caffeine而分布式缓存Redis则Hazelcast提供跨多个实例或微服务的可扩展性。
  2. 持久性: 如果优先考虑针对故障的恢复能力,请选择提供数据持久性的缓存解决方案,确保缓存的数据在应用程序重新启动后仍然存在。

注意缓存粒度

  1. 粒度选择: 决定是缓存整个对象、对象子集还是仅缓存原始数据。每个选择都会影响存储利用率和使缓存条目无效的复杂性。
  2. 过于细化: 在非常细粒度的级别(例如各个字段)进行缓存可能会导致缓存管理的开销,并可能抵消其好处。
  3. 太粗略: 大对象可能会占用大量空间,并可能导致缓存逐出,尤其是在仅频繁访问对象的一小部分的情况下。

动态缓存管理

  1. 使用条件缓存: 通过condition缓存注解中的属性,您可以引入选择性缓存的逻辑。
  2. 生存时间 (TTL): 设置 TTL 可确保数据不会无限期地保留在缓存中。根据基础数据更改的频率调整 TTL。

关键设计考虑因素

  1. 一致性: 确保密钥生成一致。不一致的键可能会导致缓存未命中或同一数据有多个缓存条目。
  2. 复杂性: 虽然键应该是唯一的,但请避免使它们过于复杂。密钥生成的开销会降低缓存的好处。

优雅地处理缓存故障

  1. 回退策略: 实施处理缓存故障的机制,例如直接从源提供数据或使用过时的缓存数据。
  2. 监控和警报: 定期监控缓存命中率和未命中率、延迟和逐出率。设置异常警报,以便及时解决问题。

定期测试和评估

  1. 性能测试: 定期测试缓存与非缓存操作的性能。这有助于识别瓶颈并确保缓存增加价值。
  2. 逐出测试: 模拟缓存达到最大大小的场景。这有助于理解逐出行为和调整缓存大小。

文档和评论

  1. 清晰的注释: 每当您在方法或服务上实现缓存时,请留下清晰的注释,解释为什么使用缓存以及预期的行为是什么。
  2. 文档: 维护缓存策略的全面文档,包括缓存名称、TTL 值和使用的任何自定义逻辑。

保持更新

  1. 框架更新: 随着 Spring 的发展,其缓存功能也在不断发展。定期检查可合并到您的应用程序中的更新或改进。
  2. 缓存解决方案更新: 外部缓存解决方案类似Redis或EhCache也收到更新和改进。确保您充分利用他们的潜力。

结论

Spring 的缓存相关注解提供了强大而灵活的机制来优化应用程序。通过理解并有效使用@Cacheable、@CacheEvict和其他注释,开发人员可以显着提高 Spring 应用程序的响应能力和可扩展性。与往常一样,虽然缓存带来了许多好处,但明智地使用它并监控其对应用程序的影响至关重要。

猜你喜欢

转载自blog.csdn.net/m0_71777195/article/details/132975306
今日推荐