目录
- 1、Caffeine 简介
- 1.1、Caffeine 简介
- 1.2、对比 Guava cache 的性能主要优化项
- 1.3、常见的缓存淘汰算法
- 1.4、SpringBoot 集成 Caffeine 两种方式
- 2、SpringBoot 集成 Caffeine 方式一
- 2.1、缓存加载策略
- 2.1.1、手动加载
- 2.1.2、自动加载【Loading Cache】
- 2.1.3、异步加载【AsyncLoadingCache】
- 2.2、回收策略
- 2.2.1、基于大小的过期方式
- 2.2.2、基于时间的过期方式
- 2.2.3、基于引用的过期方式
- 2.3、写入外部存储
- 2.4、统计
- 2.5、刷新机制
- 2.6、快速入门
- 2.6.1、引入依赖
- 2.6.2、创建 `Caffeine` 缓存配置类
- 2.6.3、定义实体对象
- 2.6.4、定义服务实现类
- 2.6.5、定义控制器,调用测试
- 2.7、总结
- 3、SpringBoot 集成 Caffeine 方式二
- 3.1、常用的注解
- 3.1.1、`@Cacheable` 注解
- 3.1.2、`@CachePut` 注解
- 3.1.3、`@CacheEvict` 注解
- 3.1.4、`@Caching` 注解
- 3.1.5、`@CacheConfig` 注解
- 3.2、快速入门
- 3.2.1、引入依赖
- 3.2.2、开启缓存功能
- 3.2.3、配置缓存类
- 3.2.4、使用缓存
- 3.2.5、添加 Controller
- 3.3、配置数据源
- 3.3.1、不配置(默认)
- 3.3.2、配置 Caffeine 缓存
- 3.3.2.1、CaffeineCacheManager
- 3.3.2.1.1、使用 Java 配置类
- 3.3.2.1.2、使用配置文件
- 3.3.2.2、SimpleCacheManager
- 3.3.3、配置 Redis 缓存
- 3.3.4、配置多缓存源
- 3.4、keyGenerator
- 3.5、CacheResolver
- 5、SPEL
- 6、Caffeine 的优劣势和适用场景
1、Caffeine 简介
1.1、Caffeine 简介
Caffeine 官网
Caffeine 是基于Java 1.8 的高性能本地缓存库,同样是 Google 开发的,由 Guava 改进而来,底层设计思路、功能和使用方式与 Guava 非常类似,但是各方面的性能都要远远超过前者,可以看做是 Guava cache 的升级版。而且在 Spring5 开始的默认缓存实现就将 Caffeine 代替原来的 Google Guava,官方说明指出,其缓存命中率已经接近最优值
可以通过下图观测到,在下面缓存组件中 Caffeine 性能是其中最好的:
1.2、对比 Guava cache 的性能主要优化项
Caffeine 底层又做了哪些优化,才能让其性能高于 Guava cache 呢?主要包含以下三点:
- 异步策略:Guava cache 在读操作中可能会触发淘汰数据的清理操作,虽然自身也做了一些优化来减少读的时候的清理操作,但是一旦触发,就会降低查询效率,对缓存性能产生影响。而Caffeine 支持异步操作,采用异步处理的策略,查询请求在触发淘汰数据的清理操作后,会将清理数据的任务添加到独立的线程池中进行异步操作,不会阻塞查询请求,提高了查询性能
- ConcurrentHashMap 优化:Caffeine 底层都是通过
ConcurrentHashMap
来进行数据的存储,因此随着 Java8 中对ConcurrentHashMap
的调整,数组 + 链表的结构升级为数组 + 链表 + 红黑树的结构以及分段锁升级为 syschronized + CAS,降低了锁的粒度,减少了锁的竞争,这两个优化显著提高了 Caffeine 在读多写少场景下的查询性能 - 新型淘汰算法 W-TinyLFU:传统的淘汰算法,如 LRU、LFU、FIFO,在实际的缓存场景中都存在一些弊端。因此,Caffeine 引入了 W-TinyLFU 算法,由窗口缓存、过滤器、主缓存组成。缓存数据刚进入时会停留在窗口缓存中,这个部分只占总缓存的 1%,当被挤出窗口缓存时,会在过滤器汇总和主缓存中淘汰的数据进行比较,如果频率更高,则进入主缓存,否则就被淘汰,主缓存被分为淘汰段和保护段,两段都是 LRU 算法,第一次被访问的元素会进入淘汰段,第二次被访问会进入保护段,保护段中被淘汰的元素会进入淘汰段,这种算法实现了高命中率和低内存占用
1.3、常见的缓存淘汰算法
常见的缓存淘汰算法:
- FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰,会导致命中率很低
- 局限性:如果缓存使用的频率较高,那么缓存数据会一直处在进进出出的状态,间接影响到缓存命中率
- LRU:最近最少使用算法,每次访问数据都会将其放在我们的队首,如果需要淘汰数据,就只需要淘汰队尾即可。仍然有个问题:如果有个数据在 1 分钟访问了 1000次,再后 1 分钟没有访问这个数据,但是有其它的数据访问,就导致了我们这个热点数据被淘汰
- 局限性:LRU 可以很好的应对突发流量的情况,因为它不需要累计数据频率。但 LRU 通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。
- LFU:最近最少频率使用,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰
- 局限性:在 LFU 中只要数据访问模式的概率分布随时间保持不变时,其命中率就能变得非常高。比如:有部新剧出来了,我们使用 LFU 给它缓存下来,这部新剧在这几天大概访问了几亿次,这个访问频率也在我们的 LFU 中记录了几亿次。但是新剧总会过气的,比如一个月之后这个新剧的前几集其实已经过气了,但是它的访问量的确是太高了,其它的电视剧根本无法淘汰这个新剧,所以在这种模式下是有局限性。
上面三种策略各有利弊,实现的成本也是一个比一个高,同时命中率也是一个比一个好。Guava Cache 虽然有这么多的功能,但是本质上还是对 LRU 的封装,如果有更优良的算法,并且也能提供这么多功能,相比之下就相形见绌了。
在现有算法的局限性下,会导致缓存数据的命中率或多或少的受损,而命中略又是缓存的重要指标。HighScalability 网站刊登了一篇文章,由前 Google 工程师发明的 W-TinyLFU ——一种现代的缓存 。Caffine Cache
就是基于此算法而研发。Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率
当数据的访问模式不随时间变化的时候,LFU 的策略能够带来最佳的缓存命中率。然而 LFU 有两个缺点:
- 它需要给每个记录项维护频率信息,每次访问都需要更新,这是个巨大的开销;
- 如果数据访问模式随时间有变,LFU 的频率信息无法随之变化。因此,早先频繁访问的记录可能会占据缓存,而后期访问较多的记录则无法被命中。
因此,大多数的缓存设计都是基于LRU或者其变种来进行的。相比之下,LRU 并不需要维护昂贵的缓存记录元信息,同时也能够反应随时间变化的数据访问模式。然而,在许多负载之下,LRU 依然需要更多的空间才能做到跟 LFU一致的缓存命中率。因此,一个“现代”的缓存,应当能够综合两者的长处。
TinyLFU 维护了近期访问记录的频率信息,作为一个过滤器,当新记录来时,只有满足 TinyLFU 要求的记录才可以被插入缓存。如前所述,作为现代的缓存,它需要解决两个挑战:
- 如何避免维护频率信息的高开销
- 如何反应随时间变化的访问模式
更详细的解释可以参考论文:https://arxiv.org/pdf/1512.00727.pdf
1.4、SpringBoot 集成 Caffeine 两种方式
Caffeine Cache 的 github 地址
在 SpringBoot 中,有两种使用 Caffeine 作为缓存的方式:
- 直接引入
Caffeine
依赖,然后使用Caffeine
方法实现缓存 - 引入
Caffeine
和Spring Cache
依赖,使用SpringCache
注解方法实现缓存
2、SpringBoot 集成 Caffeine 方式一
引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.5</version>
</dependency>
Caffeine Cache 提供了三种缓存填充策略:手动、同步加载和异步加载。
2.1、缓存加载策略
2.1.1、手动加载
最普通的一种缓存,无需指定加载方式,需要手动调用 put()
进行加载,通过 get
获取缓存值。如果想要的缓存值不存在,并且原子地将值写入缓存,则可以调用 get(key, k -> value)
方法,该方法将避免写入竞争。
在多线程情况下,当使用 get(key, k -> value)
时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用 getIfPresent()
方法,则会立即返回 null,不会被阻塞
Cache<String, Object> cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大缓存数量
.maximumSize(500)
// 缓存过期时间:写入缓存后,经过某个时间缓存失效
.expireAfterWrite(3, TimeUnit.SECONDS)
// 缓存失效监听器
.removalListener((key, value, cause) -> System.out.println("key:" + key + " value:" + value + " cause:" + cause))
// 开启统计功能
.recordStats()
.build();
cache.put("name", "zzc");
// 如果一个 key 不存在,那么会进入指定的函数生成 value
cache.get("age", key -> "18");
// 判断是否存在,如果不存在,则返回 null
Object ageValue = cache.getIfPresent("age");
// 移除一个key
cache.invalidate("name");
return cache;
2.1.2、自动加载【Loading Cache】
LoadingCache
是一种自动加载的缓存。其和普通缓存不同的地方在于:当缓存不存在/缓存已过期时,若调用 get()
方法,则会自动调用 CacheLoader.load()
方法加载最新值;调用 getAll()
方法将遍历所有的 key 调用 get()
,除非实现了 CacheLoader.loadAll()
方法。
使用 LoadingCache
时,需要指定 CacheLoader
,并实现其中的 load()
方法供缓存缺失时自动加载。
在多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新缓存完成
LoadingCache<String, Object> cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大缓存数量
.maximumSize(500)
// 缓存过期时间:写入缓存后,经过某个时间缓存失效【仅支持 LoadingCache】
.refreshAfterWrite(3, TimeUnit.SECONDS)
// 缓存失效监听器
.removalListener((key, value, cause) -> System.out.println("key:" + key + " value:" + value + " cause:" + cause))
// 开启统计功能
.recordStats()
.build(key -> "zzc");
return cache;
2.1.3、异步加载【AsyncLoadingCache】
AsyncLoadingCache
是 Cache 的一个变体,其响应结果均为 CompletableFuture
,通过这种方式,AsyncLoadingCache
对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()
作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.executor(Executor)
方法。
synchronous()
提供了阻塞直到异步缓存生成完毕的能力,它将以 Cache 进行返回。
在多线程情况下,当两个线程同时调用 get(key, k -> value)
,则会返回同一个 CompletableFuture
对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞
如果要以同步方式调用时,应提供 CacheLoader
;要以异步表示时,应该提供一个AsyncCacheLoader
,并返回一个 CompletableFuture
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大缓存数量
.maximumSize(500)
// 缓存过期时间:写入缓存后,经过某个时间缓存失效
.expireAfterWrite(3, TimeUnit.SECONDS)
// 缓存失效监听器
.removalListener((key, value, cause) -> System.out.println("key:" + key + " value:" + value + " cause:" + cause))
// 开启统计功能
.recordStats()
.buildAsync(key -> "zzc");
//异步缓存返回的是 CompletableFuture
CompletableFuture<Object> future = cache.get("1");
future.thenAccept(System.out::println);
2.2、回收策略
Caffeine提供了 3 种回收策略:基于大小回收,基于时间回收,基于引用回收。
2.2.1、基于大小的过期方式
基于大小的回收策略有两种方式:
- 基于缓存大小:
maximumSize()
- 基于权重:
maximumWeight()
、weigher()
maximumWeight
与 maximumSize
不可以同时使用
2.2.2、基于时间的过期方式
- 基于固定的到期策略:
expireAfterAccess()
expireAfterWrite()
- 基于不同的到期策略:
expireAfter(Expiry expiry)
expireAfterCreate()
expireAfterUpdate()
expireAfterRead()
2.2.3、基于引用的过期方式
Java 中四种引用类型:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 Strong Reference | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 Soft Reference | 在内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 Weak Reference | 在垃圾回收时 | 对象缓存 | gc运行后终止 |
虚引用 Phantom Reference | 从来不会 | 可以用虚引用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知 | JVM停止运行时终止 |
// 当key和value都没有引用时驱逐缓存
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> function(key));
// 当垃圾收集器需要释放内存时驱逐
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.softValues()
.build(key -> function(key))
【注意】:AsyncLoadingCache
不支持弱引用和软引用
weakKeys()
:使用弱引用存储 key。如果没有其它地方对该 key 有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()weakValues()
:使用弱引用存储value。如果没有其它地方对该 value 有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()softValues()
:使用软引用存储 value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值
【注意】:weakValues()
和 softValues()
不可以一起使用
2.3、写入外部存储
CacheWriter
方法可以将缓存中所有的数据写入到第三方
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
.writer(new CacheWriter<String, Object>() {
@Override public void write(String key, Object value) {
// 写入到外部存储
}
@Override public void delete(String key, Object value, RemovalCause cause) {
// 删除外部存储
}
})
.build(key -> function(key));
如果你有多级缓存的情况下,这个方法还是很实用
【注意】:CacheWriter
不能与弱键或 AsyncLoadingCache
一起使用
2.4、统计
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通过使用 Caffeine.recordStats()
, 可以转化成一个统计的集合. 通过 Cache.stats()
返回一个 CacheStats
。
CacheStats
提供以下统计方法:
// 返回缓存命中率
hitRate()
// 缓存回收数量
evictionCount()
// 加载新值的平均时间
averageLoadPenalty()
2.5、刷新机制
refreshAfterWrite()
表示 x 秒后自动刷新缓存的策略可以配合淘汰策略使用
private static int NUM = 0;
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.SECONDS)
//模拟获取数据,每次获取就自增1
.build(integer -> ++NUM);
//获取ID=1的值,由于缓存里还没有,所以会自动放入缓存
System.out.println(cache.get(1));// 1
// 延迟2秒后,理论上自动刷新缓存后取到的值是2
// 但其实不是,值还是1,因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新
// 而是x秒后&&第二次调用getIfPresent的时候才会被动刷新
Thread.sleep(2000);
System.out.println(cache.getIfPresent(1));// 1
//此时才会刷新缓存,而第一次拿到的还是旧值
System.out.println(cache.getIfPresent(1));// 2
【注意】:刷新机制只支持 LoadingCache
和 AsyncLoadingCache
2.6、快速入门
2.6.1、引入依赖
2.6.2、创建 Caffeine
缓存配置类
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
Cache<String, Object> cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大缓存数量
.maximumSize(500)
// 缓存过期时间:写入缓存后,经过某个时间缓存失效
.expireAfterWrite(3, TimeUnit.SECONDS)
// 缓存失效监听器
.removalListener((key, value, cause) -> System.out.println("key:" + key + " value:" + value + " cause:" + cause))
// 开启统计功能
.recordStats()
.build();
cache.put("name", "zzc");
// 如果一个 key 不存在,那么会进入指定的函数生成 value
cache.get("age", key -> "18");
// 判断是否存在,如果不存在,则返回 null
Object ageValue = cache.getIfPresent("age");
// 移除一个key
cache.invalidate("name");
return cache;
}
}
2.6.3、定义实体对象
@Data
public class User {
private Integer id;
private String name;
}
2.6.4、定义服务实现类
@Service
public class UserServiceImpl {
@Autowired
private Cache<String, Object> caffeineCache;
// 模拟数据库数据
private Map<Integer, User> userMap = new HashMap<>();
public void add(User user) {
userMap.put(user.getId(), user);
// 添加缓存
caffeineCache.put(String.valueOf(user.getId()), user);
}
public User get(Integer id) {
// 从缓存中获取
User user = (User)caffeineCache.asMap().get(String.valueOf(id));
if (Objects.nonNull(user)) {
return user;
}
// 缓存没有,从数据库获取
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 添加缓存
caffeineCache.put(String.valueOf(id), user);
}
return user;
}
public void update(User user) {
// 更新数据库
userMap.put(user.getId(), user);
// 更新缓存
caffeineCache.put(String.valueOf(user.getId()), user);
}
public void delete(Integer id) {
// 删除数据库
userMap.remove(id);
// 删除缓存
caffeineCache.asMap().remove(String.valueOf(id));
}
}
2.6.5、定义控制器,调用测试
2.7、总结
上述一些策略在创建时都可以进行自由组合,一般情况下有两种方法:
- 设置
maxSize
、refreshAfterWrite
,不设置expireAfterWrite/expireAfterAccess
:设置expireAfterWrite
,当缓存过期时会同步加锁获取缓存,所以设置expireAfterWrite
时性能较好,但是某些时候会取旧数据,适合允许取到旧数据的场景 - 设置
maxSize
、expireAfterWrite/expireAfterAccess
,不设置refreshAfterWrite
:数据一致性好,不会获取到旧数据,但是性能没那么好,适合获取数据时不耗时的场景
3、SpringBoot 集成 Caffeine 方式二
3.1、常用的注解
Spring缓存注解@Cacheable、@CachePut、@CacheEvict
【日积月累】SpringBoot 通过注解@CacheConfig @Cacheable @CacheEvict @CachePut @Caching使用缓存
cache 方面的注解主要有以下 5 个:
@Cacheable
【创建、查询缓存】:触发缓存入口(一般放在创建和获取的方法上,@Cacheable
注解会先查询是否已经有缓存。如果有,则直接从缓存中返回;如果没有,则会执行方法并返回结果缓存【返回方法返回 NULL,则不进行缓存】)@CachePut
【更新缓存】:更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)@CacheEvict
【删除缓存】:触发缓存的 eviction(用于删除的方法上)@Caching
【组合缓存配置】:将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)@CacheConfig
【类级别共享配置】:在类级别设置一些缓存相关的共同配置(与其它缓存配合使用),避免在每个缓存方法上重复配置相同的缓存属性
@Cacheable
和@CachePut
的区别?
@Cacheable
:它的注解的方法是否被执行取决于Cacheable
中的条件,方法很多时候都可能不被执行@CachePut
:这个注解不会影响方法的执行,也就是说无论它配置的条件是什么,方法都会被执行,更多的时候是被用到修改上
如果不想使用注解的方式去操作缓存,也可以直接使用
SimpleCacheManager
获取缓存的 key 进而进行操作
3.1.1、@Cacheable
注解
@Cacheable
注解:使方法返回结果被缓存【返回结果为 NULL,则不进行缓存】,再次通过相同参数调用时,会直接从缓存获取,而不再执行该方法逻辑。
@Cacheable
注解参数如下:
value()/cacheNames()
:指定缓存/缓存管理器名称,用来划分不同的缓存区,避免相同 key 值互相影响key()
:指定缓存的键值 key。默认情况下,Spring 会根据方法的参数生成缓存键;也可以使用 Spring EL 表达式来自定义缓存键的生成方式,如:"#id"
,"#user.id"
等keyGenerator()
:指定使用的缓存键生成器的名称,可以自定义 key 生成类,通过反射方式自己构建 key,跟 key() 参数不能同时赋值- 在 Spring 框架中,缓存键生成器(KeyGenerator)负责为缓存中的每个数据项生成唯一的键,用于在检索时查找数据项。默认情况下,Spring 使用
SimpleKeyGenerator
作为缓存键生成器,它使用方法的参数作为键 key
- 在 Spring 框架中,缓存键生成器(KeyGenerator)负责为缓存中的每个数据项生成唯一的键,用于在检索时查找数据项。默认情况下,Spring 使用
cacheManager()
:指定使用的缓存管理器的名称。不赋值时,为默认管理器,常用于多缓存源场景cacheResolver()
:指定使用的缓存解析器的名称,跟keyGenerator()
类似condition()
:设置匹配条件【针对请求参数】,用于判断是否执行缓存操作,只有当表达式的结果为 true 时,才会执行缓存操作。格式为 spring EL 表达式unless()
:设置排除条件【针对返回值】,用于判断是否不执行缓存操作,只有当表达式的结果为 false 时,才会执行缓存操作。格式为 spring EL 表达式sync()
:指定是否使用同步模式进行缓存操作。若使用同步模式,在多个线程同时对一个 key 进行 load 时,其它线程将被阻塞;默认值为false,表示使用异步模式。在异步模式下,如果多个线程同时访问同一个缓存项,可能会导致缓存穿透的问题。可以将 sync 设置为 true 来避免这个问题
如:
@Cacheable(cacheNames="users", key="#user.id")
public User get(User user) {
return new User(user.getId());
}
@Cacheable(cacheNames="users", key="#user.id", condition="#user.id > 300", unless="#result.id > 500")
public User get(User user) {
return new User(user.getId());
}
3.1.2、@CachePut
注解
@CachePut
注解:在功能上跟 @Cacheable
基本相同,不同之处就是,每次都会执行方法逻辑,更新缓存
参数:除不包含 sync
参数外,其它跟 @Cacheable
一致
如:
@CachePut(cacheNames="users", key="#id")
public User put(int id) {
return new User(id);
}
3.1.3、@CacheEvict
注解
@CacheEvict
注解:对符合参数条件的缓存,做删除处理
参数:除跟 @Cacheable
类似的参数外,还包含另外
- allEntries:删除指定
cacheNames
区域内,所有的缓存 - beforeInvocation:如果为 true ,在执行方法之前做删除缓存处理;为 false 时,在执行方法之后做删除处理,默认为 false
如:
@CacheEvict(cacheNames="users", key="#id")
public void delete(int id) {
System.out.println("do delete: " + id);
}
@CacheEvict(cacheNames="users", allEntries=true)
public void clear() {
System.out.println("do clear");
}
3.1.4、@Caching
注解
@Caching
注解:可同时组合、配置多个 @Cacheable
、@CachePut
、@CacheEvict
注解
@Caching(
cacheable = {
@Cacheable(value = "userCache", key = "#id")
},
put = {
@CachePut(value = "userCache", key = "#result.id")
},
evict = {
@CacheEvict(value = "userListCache", allEntries = true)
}
)
public User getUserById(Long id) {
// 从数据库中获取用户信息的逻辑
return user;
}
当调用 getUserById
方法时,会先从名为 userCache
的缓存中查找对应的用户信息,如果缓存中不存在,则执行方法的逻辑,并将返回的用户信息存储到 userCache
缓存中,并且将 userListCache
缓存中的所有数据移除
3.1.5、@CacheConfig
注解
@CacheConfig
注解:类级别的注解,类下所有被缓存注解的方法都会继承所配置的参数,避免方法上相同参数重复配置
如:
@Service
@CacheConfig(cacheNames="users", cacheManager="myCacheManager")
public class UserService2 {
@Cacheable(key="#id")
public User get(int id) {
return new User(id);
}
@CachePut(key="#id")
public User put(int id) {
return new User(id);
}
}
3.2、快速入门
3.2.1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
3.2.2、开启缓存功能
开启缓存功能,需要先添加使能注解 @EnableCaching
,通常习惯在启动类配置,否则缓存注解@Cacheable
等不起作用
@EnableCaching
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
3.2.3、配置缓存类
@Configuration
public class CacheConfig {
@Bean("caffeineCacheManager")
public Cache<String, Object> caffeineCache() {
Cache<String, Object> cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大缓存数量
.maximumSize(500)
// 缓存过期时间:写入缓存后,经过某个时间缓存失效
.expireAfterWrite(3, TimeUnit.SECONDS)
// 缓存失效监听器
.removalListener((key, value, cause) -> System.out.println("key:" + key + " value:" + value + " cause:" + cause))
// 开启统计功能
.recordStats()
.build();
return cache;
}
}
3.2.4、使用缓存
修改 UserServiceImpl
:
@Slf4j
@Service
@CacheConfig(cacheNames = "caffeineCacheManager")
public class UserServiceImpl3 {
// 模拟数据库数据
private Map<Integer, User> userMap = new HashMap<>();
@CachePut(key = "#user.id")
public User add(User user) {
log.info("add");
userMap.put(user.getId(), user);
return user;
}
@Cacheable(key = "#id")
public User get(Integer id) {
log.info("get");
return userMap.get(id);
}
@CachePut(key = "#user.id")
public User update(User user) {
log.info("update");
userMap.put(user.getId(), user);
return user;
}
@CacheEvict(key = "#id")
public void delete(Integer id) {
log.info("delete");
userMap.remove(id);
}
}
3.2.5、添加 Controller
@RestController
public class TestController {
@Autowired
private UserServiceImpl3 userServiceImpl3;
@PostMapping
public String add(@RequestBody User user) {
userServiceImpl3.add(user);
return "add";
}
@GetMapping("/{id}")
public String get(@PathVariable Integer id) {
User user = userServiceImpl3.get(id);
return "get";
}
@PutMapping
public String update(@RequestBody User user) {
userServiceImpl3.update(user);
return "update";
}
@DeleteMapping("/{id}")
public String delete(@PathVariable Integer id) {
userServiceImpl3.delete(id);
return "delete";
}
}
3.3、配置数据源
既然是对数据进行缓存,就会涉及数据缓存到哪里问题,是进程本地内存?还是进程外远程存储?就需要配置缓存源,Spring 提供了丰富的缓存源种类。不同的种类引入的依赖不同:
<!-- Cache公共依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 配置redis缓存时 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 配置本地caffeine缓存时 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
3.3.1、不配置(默认)
如果项目没有第三方缓存源依赖时,SpringBoot 会默认配置 ConcurrentMapCacheManager
缓存管理器,其内部由 ConcurrentHashMap
存储缓存数据,如果有第三方缓存依赖,例如:caffeine、redis 时,就会相应的配置 CaffeineCacheManager
或 RedisCacheManager
做默认缓存管理器,
3.3.2、配置 Caffeine 缓存
缓存配置有两种:
CaffeineCacheManager
:使用一个全局的 Caffeine 配置,来创建所有的缓存。不能为每个方法,单独配置缓存过期时间,但可以在程序启动时,全局的配置缓存,方便设置所有方法的缓存过期时间SimpleCacheManager
:当应用程序启动时,通过配置多个CaffeineCache
来创建多个缓存。可以为每个方法单独配置缓存过期时间
3.3.2.1、CaffeineCacheManager
3.3.2.1.1、使用 Java 配置类
Java 配置方式
@Configuration
public class CacheConfig {
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterWrite(3, TimeUnit.SECONDS));
return cacheManager;
}
}
当然,也可以使用配置文件进行配置缓存项信息,但是,灵活性不够高。
3.3.2.1.2、使用配置文件
配置文件方式可参考通用配置类 CacheProperties
定义
使用配置文件进行配置缓存项信息:
spring:
cache:
type: caffeine
cache-names:
- userCache
caffeine:
spec: maximumSize=1024,refreshAfterWrite=60s
如果使用 refreshAfterWrite
配置,必须指定一个 CacheLoader
;不用该配置,则无需这个 bean。如上所述:该 CacheLoader
将关联被该缓存管理器管理的所有缓存,所以必须定义为CacheLoader<Object, Object>
,自动配置将忽略所有泛型类型:
@Configuration
public class CacheConfig {
/**
* 相当于在构建LoadingCache对象的时候 build()方法中指定过期之后的加载策略方法
* 必须要指定这个Bean,refreshAfterWrite=60s属性才生效
* @return
*/
@Bean
public CacheLoader<String, Object> cacheLoader() {
CacheLoader<String, Object> cacheLoader = new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return null;
}
// 重写这个方法将oldValue值返回回去,进而刷新缓存
@Override
public Object reload(String key, Object oldValue) throws Exception {
return oldValue;
}
};
return cacheLoader;
}
}
3.3.2.2、SimpleCacheManager
配置多个 CaffeineCache 来创建多个缓存
@Bean(name = "simpleCacheManager")
public SimpleCacheManager simpleCacheManager() {
SimpleCacheManager result = new SimpleCacheManager();
CaffeineCache users = new CaffeineCache("users",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
CaffeineCache roles = new CaffeineCache("roles",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
result.setCaches(Arrays.asList(users, roles));
return result ;
}
3.3.3、配置 Redis 缓存
@Bean(name = "redisCacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.prefixCacheNameWith("mtr");
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
3.3.4、配置多缓存源
@Configuration
public class CacheConfig {
@Bean(name = "simpleCacheManager")
public SimpleCacheManager simpleCacheManager() {
SimpleCacheManager result = new SimpleCacheManager();
CaffeineCache users = new CaffeineCache("users",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
CaffeineCache roles = new CaffeineCache("roles",
Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(10000L).build());
result.setCaches(Arrays.asList(users, roles));
return result ;
}
@Bean(name = "redisCacheManager")
@Primary
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.prefixCacheNameWith("mtr");
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
}
【注意】:如果使用了多个 cahce,比如:redis、caffeine 等,必须指定某一个 CacheManage
为 @primary
,在@Cacheable
注解中没指定 cacheManager
则使用标记为 primary
的那个。
3.4、keyGenerator
@Component("keyGenerator")
public class CacheKeyGenerator implements KeyGenerator {
public static final int NO_PARAM_KEY = 0;
public static final int NULL_PARAM_KEY = 53;
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(".").append(method.getName()).append(":");
if (params.length == 0) {
key.append(NO_PARAM_KEY);
} else {
int count = 0;
for (Object param : params) {
if (0 != count) {
key.append(',');
}
if (param == null) {
key.append(NULL_PARAM_KEY);
} else if (ClassUtils.isPrimitiveArray(param.getClass())) {
int length = Array.getLength(param);
for (int i = 0; i < length; i++) {
key.append(Array.get(param, i));
key.append(',');
}
} else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
key.append(param);
} else {
//Java一定要重写hashCode和eqauls
key.append(param.hashCode());
}
count++;
}
}
String finalKey = key.toString();
System.out.println("using cache key=" + finalKey);
return finalKey;
}
}
3.5、CacheResolver
SimpleCacheResolver
,NamedCacheResolver
是 Spring 内部的 CacheResolver
接口实现类,可根据实际情况参考实现
5、SPEL
Spring Cache 提供了一些供我们使用的 SpEL 上下文数据,下表:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root 对象 | 当前被调用的方法名 | #root.methodname |
method | root 对象 | 当前被调用的方法 | #root.method.name |
target | root 对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root 对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root 对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root 对象 | 当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
- 当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:
@Cacheable(key = "targetClass + methodName +#p0")
- 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。如:
@Cacheable(value="userCache", key="#id")
、@Cacheable(value="userCache", key="#p0")
SpEL 提供了多种运算符
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&, |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],1,$[…] |
6、Caffeine 的优劣势和适用场景
- 优势:对比 Guava cache 有更高的缓存性能
- 劣势:仍然存在缓存漂移的问题;JDK 版本过低无法使用
- 适用场景:读多写少,对数据一致性要求不高的场景;纯内存缓存,JDK8 及更高版本中,追求比 Guava cache 更高的性能
… ↩︎