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

在这里插入图片描述

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

?? 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 学习(2)
      • ?? 01. @Cacheable 注解的缓存过期策略有哪些?如何设置?
      • ?? 02. @Cacheable 注解的缓存淘汰策略有哪些?如何设置?
      • ?? 03. 如果在使用 @Cacheable 注解时,缓存中无数据,如何保证并发请求只有一个真正去查询数据库?
      • ?? 04. 如果缓存中的数据被修改,如何更新缓存中的数据?
      • ?? 05. 如何手动清空缓存中的数据?
      • ?? 06. 如何在使用 @Cacheable 注解时,同时设置多个缓存策略?
      • ?? 07. 如何实现一个基于 @Cacheable 注解的缓存管理器?
      • ?? 08. @Cacheable 注解适用于哪些场景,哪些场景不适用?
      • ?? 09. 如何处理缓存穿透和缓存膨胀的问题?
      • ?? 10. 在使用 @Cacheable 注解时,如何优化缓存的命中率?

?? Java 注解 @Cacheable 学习(2)

在这里插入图片描述


?? 01. @Cacheable 注解的缓存过期策略有哪些?如何设置?

@Cacheable 注解的缓存过期策略可以使用缓存提供商(如 Ehcache、Redis)的 TTL(Time To Live)或 TTI(Time To Idle)机制来实现,也可以使用 Spring 自带的过期策略。

1.TTL 策略:TTL 机制是基于缓存对象在缓存中的存储时间来设置过期时间的。当缓存对象超过了这个时间,缓存提供商就会清理该缓存对象。使用 TTL 策略时,需要确保缓存提供商支持该机制。

@Cacheable(value = "userCache", key = "#userId",  ttl = 3600)
public User getUserById(String userId) {
    // ...
    // 查询用户信息的具体逻辑
    // ...
}

在上述示例中,ttl 属性设置为 3600 表示缓存对象的过期时间是 1 小时。需要注意的是,不同的缓存提供商可能对 ttl 参数的实现方式有所不同,具体需要查看对应的文档。

2.TTI 策略:TTI 机制是基于缓存对象的访问时间来设置过期时间的。当缓存对象在一段时间内没有被访问,缓存提供商就会将其清理。使用 TTI 策略时,需要确保缓存提供商支持该机制。

@Cacheable(value = "userCache", key = "#userId",  tti = 3600)
public User getUserById(String userId) {
    // ...
    // 查询用户信息的具体逻辑
    // ...
}

在上述示例中,tti 属性设置为 3600 表示缓存对象访问间隔超过 1 小时就会被清理。

3.Spring 自带过期策略:Spring 提供了一些内置的过期策略,可以通过 @Cacheable 注解的 expire 属性来设置。其中,expire 属性是一个字符串,格式为 ${timeToLive},${timeToIdle},表示缓存对象的过期时间和访问间隔时间。

@Cacheable(value = "userCache", key = "#userId",  expire = "3600,1800")
public User getUserById(String userId) {
    // ...
    // 查询用户信息的具体逻辑
    // ...
}

在上述示例中,expire 属性设置为 3600,1800 表示缓存对象的过期时间为 1 小时,访问间隔时间为半个小时。

4.静态过期策略:在使用静态过期策略时,缓存对象被缓存后,就不再变化。因此,可以通过在 @Cacheable 注解上指定一个固定的过期时间来让它自动在指定时间后剔除。

@Cacheable(value = "userCache", key = "#userId",  unless="#result == null",  cacheManager="cacheManager1")
public User getUserById(String userId) {
    // ...
    // 查询用户信息的具体逻辑
    // ...
}

在上述示例中,我们使用了 unless 属性,它表示如果返回结果为 null,就不会将它放入缓存中。这样的话,如果之后对象的值不发生变化的话,就不会再次查询数据库,直到过期时间到达。需要注意的是,需要根据具体业务情况灵活设置静态过期时间。

5.变化时间策略:变化时间策略是一种动态过期策略,缓存对象的过期时间和变化时间相关。如果缓存对象在规定时间内没有发生变化,就会被强制剔除。

@Cacheable(value = "userCache", key = "#userId",  condition= "#age < 30")
public User getUserById(String userId, int age) {
    // ...
    // 查询用户信息的具体逻辑
    // ...
}

在上述示例中,我们使用了 condition 属性。如果满足给定的条件,对应的缓存对象就会被缓存指定的时间,然后才会被清理。需要根据具体业务情况设置变化时间。

以上是常用的缓存过期策略,根据实际情况选择并合理设置缓存过期策略,可以实现缓存的高效使用。同时,需要注意避免缓存数据的膨胀和失效,以避免不必要的资源浪费和程序异常。


?? 02. @Cacheable 注解的缓存淘汰策略有哪些?如何设置?

@Cacheable 注解的缓存淘汰策略通常包括以下几种:

  1. LRU(Least Recently Used):最近最少使用策略,即淘汰最长时间没有被使用的缓存。

  2. LFU(Least Frequently Used):最不经常使用策略,即淘汰最不经常使用的缓存。

  3. FIFO(First In First Out):先进先出策略,即淘汰最早进入缓存的缓存。

  4. Random:随机策略,即随机淘汰一个缓存。

  5. Size-based Eviction:基于缓存大小的淘汰策略,即根据缓存项的数量来决定淘汰哪些缓存项。可以通过设置最大缓存项数来限制缓存的大小,并根据需要淘汰最旧或最少使用的缓存项。

  6. Time-to-Live (TTL):时间到期淘汰策略,即为缓存项设置一个固定的生存时间,一旦缓存项超过设定的时间就会被淘汰。

  7. Time-to-Idle (TTI):空闲时间淘汰策略,即为缓存项设置一个空闲时间阈值,一旦缓存项在设定的时间内没有被访问过,就会被淘汰。

  8. Custom Eviction Strategy:自定义淘汰策略,即根据业务需求自定义实现淘汰规则。可以根据一定的逻辑判断来决定哪些缓存项需要被淘汰。

缓存淘汰策略的选择会影响缓存的效果和性能。需要根据实际场景进行选择。例如,如果缓存数据的访问频次是非常有规律的,那么 LFU 可能是比较好的选择;如果数据访问频次没有规律,可以使用 LRU 或 FIFO 等策略来进行数据淘汰。

在 Spring 中,常用的缓存提供商(如 Ehcache 和 Redis)自带了多种缓存淘汰策略,并且还可以进行自定义淘汰策略。在使用 Spring 的 Cache 技术时,可以在注解上使用 @CacheConfig 注解进行配置,例如:

@CacheConfig(cacheNames = "users", eviction = CacheEvictionPolicy.LRU)
@Service
public class UserServiceImpl implements UserService {
    // ...
}

在上述代码中,我们使用了 eviction 属性来指定缓存淘汰策略为 LRU 策略。需要注意的是,不同的缓存提供商可能对缓存淘汰策略的实现方式有所不同,具体需要查看对应的文档。同时,我们也可以通过自定义 CacheManager 的方式来实现自定义缓存淘汰策略。


?? 03. 如果在使用 @Cacheable 注解时,缓存中无数据,如何保证并发请求只有一个真正去查询数据库?

在使用 @Cacheable 注解时,如果缓存中不存在对应的数据,需要保证并发请求只有一个真正去查询数据库,可以通过使用 Spring 的 @CachePut 注解结合双重检查锁定(Double-Checked Locking)的方式实现。

@CachePut 注解可以用于更新缓存或向缓存中添加数据,即使缓存中已存在相同的 key,也会执行相应的方法并更新缓存。通过结合双重检查锁定,可以避免多个线程同时查询数据库的情况。

下面是一个示例代码:

@Cacheable(value = "userCache", key = "#userId")
public User getUserById(String userId) {
    User user = cacheManager.getUserFromCache(userId);
    if (user == null) {
        synchronized (this) {
            user = cacheManager.getUserFromCache(userId);
            if (user == null) {
                user = userDao.getUserById(userId);
                cacheManager.putUserInCache(user);
            }
        }
    }
    return user;
}

在上述示例中,首先检查缓存中是否存在对应的用户数据。如果不存在,使用双重检查锁定的方式进行并发控制。其中,cacheManager 是负责缓存管理的对象,userDao 是负责数据库操作的对象。cacheManager.getUserFromCache(userId)cacheManager.putUserInCache(user) 分别用于从缓存中获取用户数据和将用户数据放入缓存中。

使用双重检查锁定的方式可以保证只有一个线程去查询数据库并将结果放入缓存,其他并发请求可以等待该线程完成后直接从缓存中获取数据,提高了性能和并发访问的效率。

需要注意的是,双重检查锁定在一些特定的情况下可能会存在问题,例如在多线程环境下可能会有可见性问题。因此,也可以考虑使用其他并发控制方式,如使用分布式锁或使用缓存中的原子操作来保证并发请求只有一个真正查询数据库。在实际应用中,需要根据具体需求和环境选择合适的方式来保证并发访问的正确性和性能。


?? 04. 如果缓存中的数据被修改,如何更新缓存中的数据?

如果缓存中的数据被修改,需要更新缓存中的数据,可以通过使用 Spring 的 @CachePut 注解来实现,该注解用于更新缓存中的数据。

下面是一个示例代码:

@CachePut(value = "userCache", key = "#user.userId")
public User updateUser(User user) {
    // update user in database
    userDao.updateUser(user);
    User updatedUser = userDao.getUserById(user.getUserId());
    return updatedUser;
}

在上述示例中,@CachePut 注解用于更新 userCache 缓存中对应用户的数据。其中,value 属性用于指定缓存的名称,key 属性用于指定缓存的键名,这里使用 user.userId 作为键名。

在更新用户数据时,首先会将数据更新到数据库中,然后再查询更新后的用户数据。最后返回更新后的用户数据,并将其放入缓存中。这样,下一次访问该用户数据时就可以从缓存中获取到更新后的数据,而不是从数据库中查询。

需要注意的是,当使用 @CachePut 注解更新缓存时,需要同时更新数据库中的数据,以保证缓存和数据库中的数据一致性。同时,也要注意缓存中的数据过期时间,避免缓存中的数据和数据库中的数据不一致的情况。在实际应用中,还需要根据具体业务需求和性能要求来选择合适的缓存更新策略。

当缓存中的数据被修改时,还可以使用以下方式更新缓存中的数据:

1.手动更新缓存:在数据被修改后,手动更新缓存中对应的数据。可以通过调用缓存管理器的方法,如 cacheManager.put(key, value),将更新后的数据放入缓存。这种方式适用于需要精确控制缓存更新时机的情况,但需要自行管理缓存的更新逻辑。

2.自动更新缓存:可以通过使用缓存框架提供的自动缓存更新机制,如使用 Spring Cache 中的 @CacheEvict 注解。@CacheEvict 注解用于从缓存中移除数据,可以在数据被修改或删除后触发缓存的更新操作。当数据被修改时,可以同时触发缓存的移除操作,下次访问时会重新查询数据并放入缓存。这种方式无需手动管理缓存的更新逻辑,减少了代码的复杂性。

下面是一个使用 @CacheEvict 注解自动更新缓存的示例代码:

@CacheEvict(value = "userCache", key = "#user.userId")
public void updateUser(User user) {
    // update user in database
    userDao.updateUser(user);
}

在上述示例中,@CacheEvict 注解用于从 userCache 缓存中移除对应用户的数据,然后会执行用户数据的更新操作。

需要注意的是,自动更新缓存的方式需要确保在数据修改后能够及时更新缓存,以避免缓存中的数据和数据库中的数据不一致。同时,还需注意不要过度频繁地更新缓存,以避免对系统性能造成负面影响。根据具体业务场景,可以根据数据修改的频率和对数据一致性的要求来选择合适的缓存更新方式。


?? 05. 如何手动清空缓存中的数据?

要手动清空缓存中的数据,可以使用缓存管理器提供的方法来实现。具体的实现方式取决于使用的缓存框架和缓存管理器。

以下是一种常用的手动清空缓存的方法:

1.获取缓存管理器:首先需要获取到缓存管理器的实例,可以通过依赖注入或其他方式获取。例如,在使用 Spring Cache 框架时,可以注入 CacheManager 类型的实例。

2.清空缓存:使用缓存管理器的方法来清空缓存中的数据。根据具体的缓存框架和实现,可能有不同的清空缓存的方法。以下是一种常见的通用方法:

  • 如果使用的是 Spring Cache 框架,可以使用 CacheManagerclearAllCaches() 方法来清空所有缓存。例如:cacheManager.clearAllCaches()
  • 如果使用的是其他缓存框架,可以查阅相应的文档,了解如何清空缓存的方法。

以下是一个示例代码,展示如何手动清空缓存:

@Autowired
private CacheManager cacheManager;

public void clearCache() {
    // 清空所有缓存
    cacheManager.clearAllCaches();
}

在上述示例中,通过注入的方式获取到 CacheManager 实例,并定义了一个 clearCache() 方法来清空缓存。在调用该方法时,会调用 cacheManagerclearAllCaches() 方法,从而清空所有的缓存。

需要注意的是,使用手动清空缓存的方式可能会导致缓存中的所有数据被清空,因此要慎重使用,确保在适当的时机清空缓存。另外,清空缓存可能会对系统性能产生一定的影响,需要根据实际情况进行评估和选择。


?? 06. 如何在使用 @Cacheable 注解时,同时设置多个缓存策略?

在使用 @Cacheable 注解时,通常只能设置一个缓存策略,即指定一个缓存名称。但是可以通过自定义缓存管理器来达到同时设置多个缓存策略的效果。

以下是一种实现方式:

1.自定义缓存管理器:创建一个实现了 CacheManager 接口的自定义缓存管理器类,并在该类中实现对多个缓存策略的管理和操作。可以根据具体需要使用不同的缓存策略,如内存缓存、Redis 缓存等。

2.配置缓存管理器:在 Spring 配置文件中配置自定义的缓存管理器。可以通过 @Bean 注解或 XML 配置等方式进行配置。

3.在使用 @Cacheable 注解时指定缓存名称:在需要使用多个缓存策略的方法上,使用 @Cacheable 注解时,指定对应的缓存名称,即使用自定义缓存管理器中的缓存策略。

下面是一个示例代码,展示如何同时设置多个缓存策略:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        // 创建自定义的缓存管理器,例如使用一个内存缓存和一个 Redis 缓存
        SimpleCacheManager cacheManager = new SimpleCacheManager();

        // 创建内存缓存
        Cache cache1 = new ConcurrentMapCache("cache1");
        // 创建 Redis 缓存
        Cache cache2 = new RedisCache("cache2", /* Redis 配置 */);

        // 设置缓存策略
        cacheManager.setCaches(Arrays.asList(cache1, cache2));

        return cacheManager;
    }
}

在上述示例中,使用 SimpleCacheManager 创建了一个自定义缓存管理器,并设置了两个缓存策略:一个是名为 “cache1” 的内存缓存,另一个是名为 “cache2” 的 Redis 缓存。

然后,可以在使用 @Cacheable 注解时,指定对应的缓存名称,即使用自定义缓存管理器中的缓存策略。例如:

@Service
public class UserService {

    @Cacheable(cacheNames = "cache1")
    public User getUserById(Long userId) {
        // ...
    }
    
    @Cacheable(cacheNames = "cache2")
    public User getUserByName(String userName) {
        // ...
    }
    
    // ...
}

在上述示例中,getUserById 方法使用了 “cache1” 缓存策略,而 getUserByName 方法使用了 “cache2” 缓存策略。

通过自定义缓存管理器的方式,可以灵活配置和管理多个缓存策略,并在使用 @Cacheable 注解时按需指定要使用的缓存策略。


?? 07. 如何实现一个基于 @Cacheable 注解的缓存管理器?

要实现一个基于 @Cacheable 注解的缓存管理器,可以使用 Spring Cache 框架提供的相关注解和接口。以下是一个简单的示例代码:

1.创建一个自定义缓存管理器类,实现 CacheManager 接口,并注解为 Spring Bean:

@Configuration
public class CustomCacheManager implements CacheManager {

    @Override
    public Cache getCache(String name) {
        return new CustomCache(name);
    }

    @Override
    public Collection<String> getCacheNames() {
        // 返回所有缓存的名称
        return Arrays.asList("cache1", "cache2");
    }
}

在上述示例中,自定义了一个 CustomCacheManager 类,并实现了 CacheManager 接口。在 getCache 方法中,我们返回了一个自定义的缓存对象 CustomCache,可以根据具体需求进行实现。另外,在 getCacheNames 方法中,我们返回了所有缓存的名称,即 “cache1” 和 “cache2”。

2.创建一个自定义缓存类,实现 Cache 接口,并注解为 Spring Bean:

public class CustomCache implements Cache {

    private final String name;

    // 使用一个 Map 作为缓存存储
    private final Map<Object, Object> cache = new ConcurrentHashMap<>();

    public CustomCache(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Object getNativeCache() {
        return cache;
    }

    @Override
    public ValueWrapper get(Object key) {
        Object value = cache.get(key);
        return (value != null ? new SimpleValueWrapper(value) : null);
    }

    @Override
    public void put(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public void evict(Object key) {
        cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }
}

在上述示例中,自定义了一个 CustomCache 类,并实现了 Cache 接口。在这个示例中,使用了一个 ConcurrentHashMap 作为缓存的存储容器。可以根据具体需求选择合适的缓存存储方式。在此示例中,我们实现了 getNamegetNativeCachegetputevictclear 方法,用于获取缓存名称、获取缓存底层存储、获取缓存值、存储缓存值、删除缓存值和清空所有缓存值。

3.在 Spring 配置文件中配置自定义缓存管理器

@Configuration
@EnableCaching
public class CacheConfig {

    // 将自定义的缓存管理器注入到 Spring 容器中
    @Bean
    public CustomCacheManager cacheManager() {
        return new CustomCacheManager();
    }
}

在上述示例中,使用了 @Configuration@EnableCaching 注解来配置自定义缓存管理器,并通过 @Bean 注解将其注入到 Spring 容器中。

最后,可以在需要使用缓存的方法上使用 @Cacheable 注解,并指定要使用的缓存名称。例如:

@Service
public class UserService {

    @Cacheable(cacheNames = "cache1")
    public User getUserById(Long userId) {
        // ...
    }

    @Cacheable(cacheNames = "cache2")
    public User getUserByName(String userName) {
        // ...
    }
    
    // ...
}

在上述示例中,getUserById 方法使用了名为 “cache1” 的缓存策略,而 getUserByName 方法使用了名为 “cache2” 的缓存策略。

通过实现自定义的缓存管理器和缓存对象,并使用 @Cacheable 注解来指定缓存名称,可以实现基于 @Cacheable 注解的缓存管理器。这样,就可以方便地对不同的方法使用不同的缓存策略。


?? 08. @Cacheable 注解适用于哪些场景,哪些场景不适用?

@Cacheable 注解适用于需要缓存方法的返回值的场景。它可以在方法执行前先检查缓存中是否存在相同输入参数的结果,如果存在则直接返回缓存中的结果,而不执行方法的实际逻辑。这样可以提高系统的性能和响应速度。

适用场景:

  1. 频繁查询数据库或其他耗时操作,但是结果不经常变化的方法。可以使用缓存来避免重复查询。
  2. 需要对方法的结果进行缓存,并且希望能够根据方法的输入参数来作为缓存的 key。

不适用场景:

  1. 方法的返回结果经常变化,不适合缓存。例如,计算方法返回的结果会不断变化,每次调用都需要重新计算。
  2. 对于写操作(如新增、更新、删除),由于缓存的一致性问题,不适合使用缓存。因为写操作可能会修改缓存中的数据,导致缓存数据和数据库中的数据不一致。

需要注意的是,使用 @Cacheable 注解需要谨慎考虑缓存的一致性和过期策略,以及缓存的空间管理。不合理使用缓存可能导致缓存过期不及时,或者缓存空间占满等问题。


?? 09. 如何处理缓存穿透和缓存膨胀的问题?

缓存穿透问题指的是缓存中查不到数据,导致每次请求都需要去数据库查找,从而增加数据库的压力;缓存膨胀问题指的是缓存中数据过多,占用了太多的空间,从而导致系统的性能问题。下面是处理这两个问题的一些方法:

1.缓存穿透

  • 前置校验:在查询数据前,先进行参数的校验,若参数不符合要求,则直接返回错误结果。
  • 布隆过滤器:使用布隆过滤器对请求参数进行过滤,将在缓存中不存在的参数拦截掉,从而减轻了数据库的访问压力。
  • 热点数据预加载:将缓存中的热点数据提前加载到缓存中,从而避免因为缓存失效而导致的缓存穿透问题。

2.缓存膨胀

  • 设置过期时间:设置缓存数据的过期时间,避免缓存数据一直存在而导致缓存膨胀问题。
  • 缓存清理策略:在缓存中设置合适的清理策略,如 LRU (Least Recently Used 最近最少使用), LFU (Least Frequently Used 最不经常使用) 等,来清理不常用的数据。
  • 分布式缓存:使用分布式缓存来分散缓存的存储,从而减轻单个缓存节点的压力,即使缓存膨胀,也不会对整个系统造成太大的影响。

以上是几种较常见的处理缓存穿透和缓存膨胀问题的方法,但是不同的场景可能需要使用不同的方法。因此,在实际场景中需要根据具体情况采用合适的方法来进行处理。


?? 10. 在使用 @Cacheable 注解时,如何优化缓存的命中率?

要优化缓存的命中率,可以考虑以下几点:

1.精确设置缓存的 key:确保生成缓存 key 的算法准确无误,避免相同参数得到不同的缓存 key。可以使用参数的组合作为缓存 key,确保每个请求的唯一性。

2.缓存热点数据:根据业务需求,将最常被访问的数据预先加载到缓存中,提前设置好合适的过期时间,以充分利用缓存资源,提高缓存命中率。

3.考虑缓存的淘汰策略:根据业务需求和数据特点,选择合适的缓存淘汰策略,例如 Least Recently Used (LRU)、Least Frequently Used (LFU) 等,将不常访问的数据及时从缓存中清理掉,防止缓存的膨胀。

4.设置适当的缓存过期时间:根据数据的变化频率和业务特点,合理设置缓存的过期时间,确保缓存命中时数据仍然是有效的。过长的过期时间可能导致缓存中的数据已过时,过短的过期时间可能增加缓存失效导致的数据库负载。

5.缓存穿透预防:通过参数校验、布隆过滤器等方式,拦截可能产生缓存穿透的请求,避免无效请求落到数据库上。

6.合理处理异常情况:当从缓存获取数据失败时,可以考虑补偿措施,如从数据库中获取,并将数据添加到缓存中,以提高缓存的命中率。

综上所述,通过合理设置缓存的 key,缓存热点数据,选择合适的淘汰策略和过期时间,以及预防缓存穿透,能够有效提高缓存的命中率,减轻后端系统的负载。需根据具体业务场景和数据特点来选择合适的优化方法。

在这里插入图片描述