一文掌握SpringBoot注解之@Cacheable 知识文集(3)

在这里插入图片描述

作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
欢迎 点赞✍评论⭐收藏

SpringBoot 领域知识

链接 专栏
SpringBoot 专业知识学习一 SpringBoot专栏
SpringBoot 专业知识学习二 SpringBoot专栏
SpringBoot 专业知识学习三 SpringBoot专栏
SpringBoot 专业知识学习四 SpringBoot专栏
SpringBoot 专业知识学习五 SpringBoot专栏
SpringBoot 专业知识学习六 SpringBoot专栏
SpringBoot 专业知识学习七 SpringBoot专栏
SpringBoot 专业知识学习八 SpringBoot专栏
SpringBoot 专业知识学习九 SpringBoot专栏
SpringBoot 专业知识学习十 SpringBoot专栏
SpringBoot 专业知识学习十一 SpringBoot专栏
SpringBoot 专业知识学习十二 SpringBoot专栏
SpringBoot 专业知识学习十三 SpringBoot专栏
SpringBoot 专业知识学习十四 SpringBoot专栏
SpringBoot 专业知识学习十五 SpringBoot专栏

文章目录

    • Java 注解 @Cacheable 学习(3)
      • 01. @Cacheable 注解是如何处理方法的返回值的?是否有限制或约束?
      • 02. 如何在使用 @Cacheable 注解时设置缓存的有效时间?
      • 03. @Cacheable 注解的缓存对象是存储在哪里的?可以使用多种缓存存储方式吗?
      • 04. @Cacheable 注解是否支持异步方法?
      • 05. 如果使用 @Cacheable 注解多次调用同一个方法,但缓存的 key 不同,会出现什么情况?
      • 06. @Cacheable 注解的缓存过期时间为0会发生什么?
      • 07. @Cacheable 注解的缓存失效时间配置优先级如何?
      • 08. 如果缓存中的数据被删除,但仍然返回了旧数据,是什么原因?
      • 09. 执行 @Cacheable 注解方法时,是否会执行实际方法的逻辑?
      • 10. 如何在使用 @Cacheable 注解时控制缓存的大小?


Java 注解 @Cacheable 学习(3)

一文掌握SpringBoot注解之@Cacheable 知识文集(3)_第1张图片


01. @Cacheable 注解是如何处理方法的返回值的?是否有限制或约束?

@Cacheable 注解通过在方法执行前检查缓存中是否存在相同输入参数的结果来处理方法的返回值。如果缓存中已有相同参数的结果,那么方法将不会被执行,直接返回缓存中的结果;如果缓存中不存在相同参数的结果,那么方法会被执行,并将方法的返回值放入缓存中,以供后续相同参数的请求使用。

对于方法的返回值,@Cacheable 注解没有特定的限制或约束。它可以用于缓存任何类型的方法返回值,包括但不限于基本数据类型、引用类型、集合、对象等。

需要注意的是,为了使方法的返回值能够正常缓存和使用,被缓存的对象需要满足以下条件:

1.序列化支持:如果缓存使用的是分布式缓存或者需要在不同的 JVM 之间传输,那么被缓存的对象需要实现序列化接口。

2.可哈希:缓存的 key 通常会使用方法的参数作为标识,因此被缓存的对象需要正确实现 equals() 和 hashCode() 方法,以保证缓存 key 的唯一性和正确性。

另外,使用 @Cacheable 注解时,需要注意缓存逻辑的一致性和正确性,例如缓存的过期策略、缓存数据的一致性等。不合理使用缓存可能导致缓存不一致、过期不及时等问题。因此,在使用 @Cacheable 注解时,需要根据具体业务需求和系统性能考虑,合理地配置缓存相关的参数和策略。

当使用 @Cacheable 注解时,还有一些附加约束和注意事项需要考虑:

1.方法参数和缓存的 key 关联:@Cacheable 注解默认将方法的所有参数都包含在缓存的 key 中。如果方法的某些参数不应该影响缓存的 key,可以使用 key 属性或自定义 CacheKeyGenerator 来指定自定义的缓存 key。

2.缓存的 key 生成规则:默认情况下,Spring 使用 SimpleKeyGenerator 生成缓存的 key。该生成器根据方法的参数进行拼接生成缓存 key。如果需要自定义缓存 key 的生成规则,可以实现 CacheKeyGenerator 接口,并通过 keyGenerator 属性指定自定义的缓存 key 生成器。

3.缓存的 value 类型:缓存的 value 类型通常是返回值的类型。如果方法返回的是集合或其他复杂类型,需要确保缓存支持该类型。对于分布式缓存,还需要确保 value 类型可以进行序列化。

4.缓存的条件:@Cacheable 注解还可以结合条件判断来决定是否进行缓存。通过 condition 属性可以指定一个 SpEL 表达式,只有在满足该条件时才会进行缓存。

5.缓存的名字或区域:@Cacheable 注解可以使用 value 属性指定缓存的名字或区域。如果没有指定 value 属性,默认会使用默认的缓存名字或区域。可以在配置文件或者使用 @EnableCaching 注解进行缓存配置。

需要注意的是,@Cacheable 注解只能用于缓存查询操作,对于更新、删除等写操作,应使用其他的缓存注解,如 @CachePut 或 @CacheEvict。

综上所述,@Cacheable 注解对于方法返回值没有特定的限制,但需要关注缓存的 key 关联、缓存的 key 生成规则、缓存的 value 类型以及缓存的条件等方面的约束和注意事项。在实际应用中,应根据具体需求和业务场景合理使用该注解,并结合其他缓存相关注解进行缓存操作的管理。


02. 如何在使用 @Cacheable 注解时设置缓存的有效时间?

在使用 @Cacheable 注解时,可以通过两种方式设置缓存的有效时间:

1.使用缓存管理器的配置:通过在缓存配置中设置缓存的过期时间,来控制缓存的有效时间。此方法适用于整个应用程序中所有使用该缓存管理器的缓存。

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
    
    @Bean
    public CacheManager cacheManager() {
        // 缓存过期时间设置为60秒
        return new SimpleCacheManager() {
            @Override
            protected Collection<Cache> loadCaches() {
                List<Cache> caches = new ArrayList<>();
                caches.add(new ConcurrentMapCache("cacheName") {
                    @Override
                    public ValueWrapper get(Object key) {
                        // 在获取缓存时可以指定过期时间
                        return super.get(key);
                    }
                });
                return caches;
            }
        };
    }
}

2.使用 @Cacheable 注解的配置:通过在 @Cacheable 注解中使用 cacheNames 属性指定缓存的名称,并使用 key 属性指定缓存的 key,再结合使用 CacheManager 的方法来手动设置缓存的过期时间。

@Cacheable(value = "cacheName", key = "#param", condition = "#param.length() < 10")
public Object getSomeData(String param) {
    // 方法实现
}

在方法内部,可以通过获取缓存对象 ValueWrapper,并使用对应的缓存提供商的方法来手动设置缓存的过期时间。

@Cacheable(value = "cacheName", key = "#param", condition = "#param.length() < 10")
public Object getSomeData(String param) {
    Cache cache = cacheManager.getCache("cacheName");
    ValueWrapper valueWrapper = cache.get(param);
    if (valueWrapper != null) {
        Object value = valueWrapper.get();
        // 设置该缓存的过期时间为60秒
        cache.put(param, value, Duration.ofSeconds(60));
        return value;
    }
    // 方法实现
}

通过以上两种方式,可以根据具体需求和业务场景来设置缓存的有效时间。需要根据具体的缓存管理器和缓存提供商的实现来确定可用的方法和配置选项。


03. @Cacheable 注解的缓存对象是存储在哪里的?可以使用多种缓存存储方式吗?

@Cacheable 注解的缓存对象可以存储在多种地方,具体取决于你所使用的缓存管理器和缓存服务商。一般情况下,常见的缓存存储方式如下:

1.内存:将缓存对象保存在应用程序的内存中,通常性能较好,但是存储的数据有大小限制。

2.磁盘:将缓存对象保存在磁盘上,通常存储的数据可以比较大,但是由于需要读取磁盘,性能通常比内存缓存差。

3.分布式存储:将缓存对象保存在分布式存储系统中,可以实现数据跨节点的共享,提高了可伸缩性和容灾性能。

可以使用多种缓存存储方式,以适应不同的应用场景和需求。在 Spring 中,通过使用缓存抽象接口,可以使应用程序方便地切换不同的缓存实现,而不需要修改代码。Spring 支持多种缓存实现,如 JCache、Ehcache、Redis 等,你可以选择最适合自己应用程序的缓存实现。

下面是一个使用 Redis 存储缓存的示例:

1.引入 Redis 的相关依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2.配置 Redis 缓存

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager cacheManager = RedisCacheManager.create(redisConnectionFactory());
        return cacheManager;
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
    }
}

3.使用 @Cacheable 注解

@Cacheable(value = "cacheName", key = "#param", condition = "#param.length() < 10")
public Object getSomeData(String param) {
    // 方法实现
}

以上示例使用 Redis 作为缓存保存库,可以根据具体业务情况和实际需求来选择不同的缓存存储方式。


04. @Cacheable 注解是否支持异步方法?

是的,@Cacheable 注解在异步方法上同样可以使用。在 Spring 中,异步方法可以使用 @Async 注解来标注,在使用时需要满足一些条件:

1.@Async 注解和异步方法都只能在 Spring 管理的 bean 中使用,也就是只能在容器中进行方法调用,无法在普通的对象中使用。

2.由于使用了异步方法,调用结果并不会立即返回,而是通过 Future 或 CompletableFuture 进行异步回调。因此需要考虑如何使得调用结果可以缓存,并解决多线程之间的线程安全问题。

如果使用了 @Cacheable 注解,并且异步方法返回值类型为 Future 或 CompletableFuture,则缓存系统将默认使用异步方式来处理缓存,以确保异步回调结果的正确性和可用性。

下面是一个使用 @Cacheable 注解和 @Async 注解的示例:

@Cacheable(value = "cacheName", key = "#param")
@Async
public CompletableFuture<Object> getSomeDataAsync(String param) {
    // 异步方法实现,并返回 CompletableFuture 对象
}

需要注意的是,如果异步方法返回值类型为 void 或者是其他不支持缓存的类型,则无法使用 @Cacheable 注解来进行缓存相关的处理。如果需求比较复杂,可以考虑使用更为高级的缓存框架来进行异步缓存处理。

05. 如果使用 @Cacheable 注解多次调用同一个方法,但缓存的 key 不同,会出现什么情况?

如果使用 @Cacheable 注解多次调用同一个方法,但缓存的 key 不同,那么每一个缓存 key 对应的缓存结果是独立的,不会互相影响,因此不会出现冲突或者错误结果的情况。

例如,假设有如下代码:

@Cacheable(value = "userCache", key = "#userId")
public User getUserById(String userId) {
    // 方法实现
}

如果第一次调用 getUserById(“10001”) 返回了结果 A,并将结果 A 存储在了缓存 key 为 “10001” 的缓存对象中。那么当第二次调用 getUserById(“10002”) 时,将会重新调用方法实现,并将结果存储在缓存 key 为 “10002” 的缓存对象中。

虽然这两个缓存对象都是保存在同一个 cacheName 的缓存中,但是由于 key 不同,所以两个缓存对象是互相独立的,并不会互相干扰。

这个特性也可以用于多次调用同一个方法,但是需要根据不同的参数条件来缓存结果的情况,可以通过使用 SpEL 表达式来动态生成 key,以实现动态的缓存策略。例如:

@Cacheable(value = "userCache", key = "#root.methodName + ':' + #userId")
public User getUserById(String userId) {
    // 方法实现
}

通过 SpEL 表达式,将缓存的 key 设置为当前方法名 + 参数 userId,这样在多次调用方法时,不同的 userId 值将会使用不同的缓存 key 来缓存数据,确保缓存结果的正确性。


06. @Cacheable 注解的缓存过期时间为0会发生什么?

当使用 @Cacheable 注解时,可以通过设置缓存过期时间来控制缓存对象的有效期。如果将缓存过期时间设置为0,会发生以下情况:

  1. 当第一次调用方法时,缓存对象会被创建并存储在缓存中。

  2. 然而,由于缓存过期时间设置为0,缓存对象会立即过期。

  3. 当再次调用相同的方法时,会重新执行方法逻辑,并创建新的缓存对象。

换句话说,将缓存过期时间设置为0,意味着缓存对象在存储之后立即过期,每次调用方法都会重新计算并创建新的缓存对象,而不会复用之前的缓存对象。

这在某些情况下可以用来实现即时刷新缓存的效果,当每次调用方法时,都可以获取最新的数据,而不受旧缓存数据的影响。

请注意,缓存过期时间的单位取决于所使用的缓存框架和配置。一般情况下,可以使用秒、分钟、小时等来指定缓存过期时间。具体的时间单位和格式需要参考所使用的缓存框架的文档。

例如,在使用 Spring Boot 和 Redis 进行缓存时,可以在配置文件(例如 application.properties)中进行如下设置:

spring.cache.redis.time-to-live=0

这将会将缓存过期时间设置为0,即立即过期,每次调用方法都会重新计算缓存对象。


07. @Cacheable 注解的缓存失效时间配置优先级如何?

在 Spring 中,使用 @Cacheable 注解可以启用方法的缓存功能,并可以配置缓存失效时间。优先级如下:

1.方法级别的配置优先级最高:可以在方法级别的 @Cacheable 注解中使用 keyexpire 参数来指定缓存的键和缓存的失效时间。

@Cacheable(value = "userCache", key = "#userId", expire = 60)
public User getUserById(String userId) {
    // 方法实现
}

在上述示例中,expire = 60 表示该方法的缓存对象将在缓存存储之后的60秒内过期。

2.缓存管理器级别的配置其次:可以在缓存管理器的配置中指定默认的缓存失效时间。

@Bean
public CacheManager cacheManager() {
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    // 配置缓存
    cacheManager.setCaches(Arrays.asList(
            new ConcurrentMapCache("userCache"),
            new ConcurrentMapCache("productCache")));
    // 设置默认失效时间
    cacheManager.setDefaultExpiration(60); // 60秒
    return cacheManager;
}

在上述示例中,通过 setDefaultExpiration() 方法设置了默认失效时间为60秒。

3.全局默认配置:如果没有指定方法级别和缓存管理器级别的缓存失效时间配置,将会使用全局默认的缓存失效时间配置。

在 Spring Boot 中,可以通过在 application.propertiesapplication.yml 文件中进行全局默认配置。

spring.cache.redis.time-to-live=60s

在上述示例中,将全局默认的缓存失效时间配置为60秒。

需要注意的是,不同的缓存实现框架可能会有不同的配置方式和参数名称,具体的配置方式和参数名称,请查阅各自框架的文档和官方指南。


08. 如果缓存中的数据被删除,但仍然返回了旧数据,是什么原因?

如果缓存中的数据被删除,但仍然返回了旧数据,可能是由于以下原因:

1.读写并发问题:在缓存删除数据后,如果有其他线程或进程并发地去读取该数据,由于读取操作是在缓存删除操作之前发生的,所以仍然返回了旧数据。这是因为缓存删除操作可能需要一些时间来生效,读取操作可能在删除操作之前完成。

为了解决这个问题,可以尝试使用同步机制,如锁来确保缓存删除操作和读取操作的顺序性。或者可以在删除缓存后,添加一个等待时间,确保缓存已经失效。

2.缓存同步延迟:某些缓存实现可能存在缓存同步延迟的情况。即使删除了缓存中的数据,缓存系统可能需要一些时间来同步和更新所有缓存节点。在这段时间内,读取操作可能仍然返回旧数据。

解决这个问题的方法是,等待缓存同步完成后再进行读取操作。可以使用一些手段,如等待一段时间,或者使用缓存系统提供的同步机制或回调函数来处理。

3.数据库和缓存不一致:如果缓存中的数据是从数据库中读取并进行缓存的,当数据库中的数据发生变化时,缓存可能没有及时更新。在这种情况下,即使删除了缓存中的数据,读取操作仍然返回旧数据。

解决这个问题的方法是,使用一些机制来确保数据库和缓存之间的一致性,例如通过数据库事件或使用缓存失效策略来在数据库更新后立即使缓存失效。


09. 执行 @Cacheable 注解方法时,是否会执行实际方法的逻辑?

执行 @Cacheable 注解方法时,是否会执行实际方法的逻辑取决于缓存中是否已经存在对应的缓存数据。具体情况如下:

1.缓存中存在对应的数据:当调用 @Cacheable 注解修饰的方法时,框架会首先检查缓存中是否已经存在对应的缓存数据。如果存在,即命中缓存,框架会直接返回缓存中的数据,而不会执行实际方法的逻辑。这是 @Cacheable 注解的主要目的,即避免重复执行相同的方法逻辑。

2.缓存中不存在对应的数据:当缓存中不存在对应的缓存数据时,框架会执行实际方法的逻辑,并将方法的返回结果存储到缓存中,以备后续的访问使用。执行实际方法的逻辑主要包括:方法的参数处理、业务逻辑处理、数据访问等。同时,方法的返回结果会被缓存起来,以便后续的相同请求可以直接从缓存中获取数据。

需要注意的是,如果缓存存在并发访问的情况,可能会出现多个线程同时访问同一个缓存数据不存在的情况。在这种情况下,只有一个线程会执行实际方法的逻辑,其它线程会等待该线程执行完毕后,直接从缓存中获取数据。其他线程返回的结果将是第一个线程执行方法逻辑后存储到缓存中的数据。

如果想要强制执行实际方法的逻辑,而不管缓存中是否存在对应的数据,可以使用 @CacheEvict 注解来删除缓存中的数据,然后再执行方法逻辑。


10. 如何在使用 @Cacheable 注解时控制缓存的大小?

在使用 @Cacheable 注解时,控制缓存的大小通常是由具体的缓存实现来完成的,而不是由 @Cacheable 注解本身提供的功能。下面是一些常见的方法来控制缓存的大小:

1.使用缓存淘汰策略:某些缓存实现(如 Ehcache、Caffeine 等)提供了缓存淘汰策略,可以根据一定的规则自动删除缓存中的数据,以保持缓存的大小。这些策略包括基于时间、基于大小、基于访问频率等。

例如,可以使用 @Cacheable 注解与 Ehcache 结合,配置合适的缓存策略来控制缓存的大小。可以在 Ehcache 的配置文件中设置缓存最大的元素数量,以及缓存元素的过期时间等。

2.手动管理缓存大小:在某些情况下,可能需要手动删除缓存中的一些数据来控制缓存的大小。可以通过 @CacheEvict 注解来删除缓存中的特定数据,或者使用缓存管理器提供的删除数据的方法。

例如,可以通过定时任务或其他触发机制,调用适当的方法来删除缓存中的一些数据,以保持缓存的大小。

3.使用缓存代理机制:一些缓存实现(如 Guava Cache)提供了缓存代理机制,在获取缓存数据时,会自动检测缓存的大小并进行缓存数据的清理。这样可以避免手动管理缓存大小的复杂性。

例如,可以使用 @Cacheable 注解与 Guava Cache 结合,通过配置的缓存代理来自动管理和控制缓存的大小。

4.使用缓存过期机制:一些缓存实现(如 Redis)提供了缓存过期机制,可以设置缓存数据的过期时间,当缓存数据的过期时间到达后,缓存自动失效并被删除。可以通过@CachePut 注解来手动更新缓存数据的过期时间。

例如,可以使用 @Cacheable 注解与 Redis 结合,通过设置缓存数据的 TTL (time-to-live) 来控制缓存的大小。可以在 Redis 的配置文件中设置缓存数据的过期时间,并在实际方法中使用 @CachePut 注解来更新缓存数据的过期时间。

需要注意的是,缓存大小的控制不应该仅仅依赖于缓存的大小,还应该考虑缓存数据的访问频率、缓存数据的生命周期、系统的负载等多方面因素。同时,为了避免缓存数据的淘汰带来的性能损耗,通常应该预先设置比实际需要更大的缓存大小。

一文掌握SpringBoot注解之@Cacheable 知识文集(3)_第2张图片

你可能感兴趣的:(Java专栏,Java基础学习,SpringBoot专栏,spring,boot,java,redis,spring,cloud,后端,spring,intellij-idea)