SpringBoot&Caffeine 灵活支持多个缓存配置策略

前言

缓存是几乎所有应用程序性能的关键。很多时候需要分布式缓存(比如常用的 Redis、Codis),但在许多情况下,本地缓存也可以很好地工作,并且不需要分布式缓存的开销和复杂性。

对于 DotNet 开发来说,本地 cache 很方便使用(比如 RuntimeCache 等); 对于 Java 说,也有很多优秀的本地 cache 库(比如 Ehcache、GuavaCache 等),而 Java 这个帝国中,spring 是一个伟大且垄断的存在。针对不同的缓存技术,Spring 定义了如下的 cacheManger 实现。

CacheManger 描述
SimpleCacheManager 使用简单的 Collection 来存储缓存,主要用于测试
ConcurrentMapCacheManager 使用 ConcurrentMap 作为缓存技术(默认),需要显式的删除缓存,无过期机制
NoOpCacheManager 仅测试用途,不会实际存储缓存
EhCacheCacheManager 使用 EhCache 作为缓存技术,以前在 hibernate 的时候经常用
GuavaCacheManager 使用 google guava 的 GuavaCache 作为缓存技术(1.5 版本已不建议使用)
CaffeineCacheManager 是使用 Java8 对 Guava 缓存的重写,spring5(springboot2)开始用 Caffeine 取代 guava
HazelcastCacheManager 使用 Hazelcast 作为缓存技术
JCacheCacheManager 使用 JCache 标准的实现作为缓存技术,如 Apache Commons JCS
RedisCacheManager 使用 Redis 作为缓存技术

因此,在许多应用程序中(包括普通的 Spring 和 Spring Boot),你都可以引入相应的依赖包后,简单的把 @Cacheable 打在任何方法上使用它,以使其结果被缓存,以便下次调用该方法时,将返回缓存的结果。

Tips:spring cache 使用基于动态生成子类的代理机制来对方法的调用进行切面,如果缓存的方法是内部调用而不是外部引用,会导致代理失败,切面失效。

虽然 Spring 有一些默认的缓存管理器实现,有时候,一些外部库总是比简单的实现更好、更灵活。例如,其中一个高性能的 Java 缓存库 Caffeine 。

今天,我们主要目标就是:认识 Caffeine 以及如何实现多缓存灵活配置。

那么,简单认识一下 Caffeine

Caffeine 是使用 Java8 对 Guava 缓存的重写版本,缓存类似于 ConcurrentMap,但并不完全相同,可提供接近最佳的命中率。它基于 LRU 算法实现,支持多种缓存过期策略。在 Spring Boot 2.0 中将取代 GuavaCache。 特性如下:

  • 自动将实体加载到缓存中,可以选择异步加载;
  • 基于频率和新近度超过最大值时基于大小的淘汰;
  • 自上次访问或上次写入以来的基于时间的过期;
  • 基于第一个请求旧数据时的异步刷新(只放行一个请求去刷新数据);
  • 其他

Caffeine 的一些参数,我们后续也会用到

参数 描述
initialCapacity=[integer] 初始的缓存空间大小(比较常用)
maximumSize=[long] 缓存的最大条数 (比较常用)
maximumWeight=[long] 缓存的最大权重
expireAfterAccess=[duration] 最后一次写入或访问后经过固定时间过期 (比较常用)
expireAfterWrite=[duration] 最后一次写入后经过固定时间过期(比较常用)
refreshAfterWrite=[duration] 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存 refreshAfterWrite requires a LoadingCache
weakKeys 打开 key 的弱引用
weakValues 打开 value 的弱引用
softValues 打开 value 的软引用
recordStats 开发统计功能

注意:

  • refreshAfterWrite 必须实现 LoadingCache,跟 expire 的区别是,指定时间过后 expire 是 remove 该 key,下次访问是同步去获取返回新值,而 refresh 则是指定时间后,不会 remove 该 key,下次访问会触发刷新,新值没有回来时返回旧值。
  • expireAfterWrite 和 expireAfterAccess 同时存在时,以 expireAfterWrite 为准。
  • maximumSize 和 maximumWeight 不可以同时使用。
  • weakValues 和 softValues 不可以同时使用。

简单了解了 caffeine 是什么,有哪些属性可用,那么我们回过头来,你会发现,其实 SpringBoot 内部已经提供了一个默认实现 CaffeineCacheManager(具体可以参见源码 org.springframework.cache.caffeine.CaffeineCacheManager )。这里不再过多的展开,可以自行阅读一下源码了解下~

因此,理想情况下,这就是你所需要的一切了:只需简单的创建一个 CacheManager 的 bean,就可以为带 @Cacheable 注释的方法进行缓存。

到此,我们大概了解了 caffeine 是个什么,以及应该如何用,那么接下来,我们就用示例说话。看不到代码瞎 BB 也是很让人讨厌的,不是么。

实战

阶段一目标:定义两个 manager,实现不同的缓存配置。

举例如下:

@Configuration
public class CaffeineConfig extends CachingConfigurerSupport {

    @Override
    @Bean(name = "cacheManager")
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();

        // 方案一(常用):定制化缓存Cache
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .initialCapacity(100)
                .maximumSize(10_000));

        return cacheManager;
    }

    /**
     * 在@cacheable使用时,指定cacheManager=specCacheManager
     *
     * @return CacheManager
     */
    @Bean(name = "specCacheManager")
    public CacheManager cacheManagerWithSpec() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 不允许空值
        cacheManager.setAllowNullValues(false);
        // 传入一个CaffeineSpec定制缓存,它的好处是可以把配置方便写在配置文件里
        cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=20,maximumSize=100,expireAfterWrite=10m"));

        // 指定使用该策略的CacheNames
        cacheManager.setCacheNames(new ArrayList<String>(Arrays.asList("fetchById", "fetchByName")));

        return cacheManager;
    }
}

  

使用起来也很简单,毕竟 springBoot 这么牛 x 的框架提供了很好的集成和灵活性。

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Override
    @Cacheable(cacheNames = "userSelectOrDefault", cacheManager = "cacheManager", key = "#userId")
    public MyUser selectOrDefault(Integer userId) {

        System.out.println("我要执行【selectOrDefault】方法的查询逻辑啦~ userId=" + userId);
        System.out.println("当前时间:" + LocalDateTime.now().toString());

        return new MyUser()
                .setUserId(userId)
                .setGender(userId % 2)
                .setUserName("userName_" + userId);
    }

    @Override
    @Cacheable(cacheNames = "fetchByName", cacheManager = "specCacheManager", key = "#userName")
    public MyUser fetchByName(String userName) {
        System.out.println("我要执行【fetchByName】方法的查询逻辑啦~userName=" + userName);
        System.out.println("当前时间:" + LocalDateTime.now().toString());

        int hashCode = userName.hashCode();

        return new MyUser()
                .setUserId(hashCode)
                .setGender(hashCode % 2)
                .setUserName(userName);
    }
}

  

通过指定 @Cacheable 的 cacheNames、 cacheManager 就可以“灵活”的使用不同的缓存策略了。可能你觉得已经有点小满足了,毕竟能灵活配置了嘛~~

然鹅~~

冷静下,真的“灵活”么?

假如一个项目中有很多要缓存(而且也肯定很常见),并且缓存的策略规则也不尽相同时(比如重要的到期时间、初始容量、最大大小等),你是不是就觉得写很多类似的 cacheManger 有点不爽?

笔者也翻阅了网上一些文章,但大多是告诉你如何使用自定义规范定义自定义缓存。但是,没有一个实现了我希望的理想状态。

我期望的是:既可以使用默认的一些策略规范自动创建缓存,又可以灵活的自定义设置你想要的缓存策略。

是不是听起来有点贪心?其实,我个人觉得这是追求完美的人很正常的一个想法。

毕竟方法总比困那多~

那么,接下来,我们就要把这个想法落地。

阶段二:目标:实现一个既可以手动配置,又可以默认的 CacheManger

更多实现细节,请通过移步公众号~ 

猜你喜欢

转载自www.cnblogs.com/hager/p/13197673.html