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
更多实现细节,请通过移步公众号~