SpringBoot 集成 Caffeine 实现本地缓存
目录
- 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 {@Beanpublic 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 不存在,那么会进入指定的函数生成 valuecache.get("age", key -> "18");// 判断是否存在,如果不存在,则返回 nullObject ageValue = cache.getIfPresent("age");// 移除一个keycache.invalidate("name");return cache;}
}
2.6.3、定义实体对象
@Data
public class User {private Integer id;private String name;
}
2.6.4、定义服务实现类
@Service
public class UserServiceImpl {@Autowiredprivate 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 CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(100).maximumSize(500).expireAfterWrite(3, TimeUnit.SECONDS));return cacheManager;}
}
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 {@Autowiredprivate UserServiceImpl3 userServiceImpl3;@PostMappingpublic 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";}@PutMappingpublic 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: caffeinecache-names:- userCachecaffeine:spec: maximumSize=1024,refreshAfterWrite=60s
如果使用 refreshAfterWrite 配置,必须指定一个 CacheLoader;不用该配置,则无需这个 bean。如上所述:该 CacheLoader 将关联被该缓存管理器管理的所有缓存,所以必须定义为CacheLoader<Object, Object>,自动配置将忽略所有泛型类型:
@Configuration
public class CacheConfig {/*** 相当于在构建LoadingCache对象的时候 build()方法中指定过期之后的加载策略方法* 必须要指定这个Bean,refreshAfterWrite=60s属性才生效* @return*/@Beanpublic CacheLoader<String, Object> cacheLoader() {CacheLoader<String, Object> cacheLoader = new CacheLoader<String, Object>() {@Overridepublic Object load(String key) throws Exception {return null;}// 重写这个方法将oldValue值返回回去,进而刷新缓存@Overridepublic 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")@Primarypublic 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;@Overridepublic 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和eqaulskey.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 更高的性能
- … ??
相关文章:
SpringBoot 集成 Caffeine 实现本地缓存
目录 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、异步加载…...
druid连接池参数配置
最近发现生产环境经常有数据库连接超时的问题,排查发现是druid连接池参数设置不合理导致 总结问题如下: 为了防止僵尸连接,k8s ipvs做了连接超时限制,如果TCP连接闲置超过900s(15分钟),客户端再尝试通过这个连接去发起…...
【OceanBase】通过 OceanBase 的向量检索技术构建图搜图应用
文章目录 一、向量检索概述1.1 关键概念① 非结构化数据② 向量③ 向量嵌入(Embedding)④ 向量相似性检索 1.2 应用场景 二、向量检索核心功能三、图搜图架构四、操作步骤4.1 使用 Docker 部署 OceanBase 数据库4.2 测试OceanBase数据库连通性4.3 开启数据库向量检索功能4.4 克…...
Linux 安装运行gatk的教程
1.下载安装 wget https://github.com/broadinstitute/gatk/releases/download/4.1.8.1/gatk-4.1.8.1.zip2.解压 unzip *.zip3.查看 gatk --help 如下显示表示安装成功: 注意:仅限在该包所在位置的路径下能使用...
什么是unit l2 norm
1. L2 Norm 定义 L2 norm(或称欧几里得范数)是用来衡量一个向量的“长度”或“大小”的一种方式。在 n 维空间中,给定一个向量V(V1,V2,…,Vn),其 L2 norm 定义为: 也可以理解为该向量与原点之间的欧几里得距离。 2…...
手写顺序流程图组件
效果图 完整代码 <template><div><div class"container" :style"{ width: ${spacingX * (colNum - 1) itemWidth * colNum}px }"><divv-for"(item, i) in recordList":key"i"class"list-box":style&…...
适配器模式概述
大体介绍 适配器模式(Adapter Pattern)是一种结构型设计模式,其核心目的是通过提供一个适配器类来使得原本接口不兼容的类可以一起工作。它通过将一个类的接口转换成客户端所期望的接口,使得原本因接口不兼容而无法一起工作的类可…...
Logo设计免费生成器:轻松设计个性化标志
在当今这个信息爆炸的时代,一个好的Logo标志已经成为品牌和企业的名片。它不仅是品牌的象征,也是企业文化和价值观的体现。然而,很多初创企业或小型团队往往因为预算有限,无法请专业的设计师来打造专属的Logo。这时候,…...
智能停车场车牌识别计费系统
作者简介:Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验,被多个学校常年聘为校外企业导师,指导学生毕业设计并参与学生毕业答辩指导,…...
谷歌开通第三方平台OAuth登录及Java对接步骤
调研起因: 当然还是因为手头的海外项目,用户注册通常要用邮箱,正常流程需要给用户邮箱发送验证码,再让用户输入密码进行注册。 为了简化流程,让用户使用谷歌邮箱一键完成注册或登录, 我们直接获取谷歌邮箱、…...
人体:精妙绝伦的生命之躯
人体:精妙绝伦的生命之躯 在浩瀚宇宙中,人体犹如一颗璀璨的明珠,是自然界最伟大的杰作之一。它是一个高度复杂且精妙绝伦的有机系统,承载着生命的奥秘与奇迹,展现出令人惊叹的适应性、协调性和自我修复能力。从微观的…...
python的urllib模块和http模块
1.python的urllib库用于操作网页,并对网页内容进行处理 urllib包有如下模块: urllib.request:打开和读取URL urllib.error: 包含urllib.request抛出的异常 urllib.parse: 解析URL urllib.robotparser࿱…...
Java [后端] 开发日常记录(1)
目录 1、常用的注解 2、对字符串的处理 3、对JSON串的处理 -- The End -- 详细如下: 1、常用的注解 若返回的字段中有NUll,则不返回 JsonInclude(value JsonInclude.Include.NON_NULL) //在实体类中添加这个注解 JsonInclude(JsonInclude.Include.NON…...
jetbrain 安装 copilot
问题一:Sign in failed. Reason: Request signInInitiate failed with message: Request to /github.com/login/device/code> timed out after 30000ms, request id: 11, error code: -32603 解决方案: 参考资料:https://github.com/orgs/…...
万里数据库GreatSQL监控解析
GreatSQL是MySQL的一个分支,专注于提升MGR(MySQL Group Replication)的可靠性及性能。乐维监控平台可以有效地监控GreatSQL,帮助用户及时发现并解决潜在的性能问题。 通过在GreatSQL服务器上安装监控代理,收集数据库性…...
OpenCV-Python实战(9)——滤波降噪
一、均值滤波器 cv2.blur() img cv2.blur(src*,ksize*,anchor*,borderType*)img:目标图像。 src:原始图像。 ksize:滤波核大小,(width,height)。 anchor:滤波核锚点,…...
Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击
Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集DTA介绍算法流程 DTA代码实现DTA算法实现攻击效果 代码汇总dta.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器: Pytorch | 从零构建AlexNet对CIFAR10进行分类 Pytorch | 从零构建…...
Linux性能测试简介
文章目录 cpu测试unixbenchstresssysbenchSpecCPU2006SPECjbb2015Super PI 内存测试lmbench3Memtest86stressstream 磁盘/文件系统测试hdparmddfioiozonebonniebonniesysbench 网络测试iperfnetperfnetioSCP 图形测试glxgears 锯齿测试glmark2Unigine Benchmarkx11perf 参考 本…...
Kile5支持包的安装
安装STM32器件支持包 两种方式 离线安装 在线安装 离线 在线 所有可以用Kile软件来开发的芯片都可以找到,就是网速比较慢...
【Ubuntu 系统 之 开启远程桌面SSH登录】
【Ubuntu 系统 之 开启远程桌面&SSH登录】 一、开启 SSH 登录二、开启远程桌面1、更新包管理器并安装 xrdp1.1、遇到错误1.2、解决方法 2、安装桌面环境(如果服务器上没有 GUI)3、配置 xrdp 使用默认的 GNOME 桌面环境4、配置防火墙允许远程桌面连接…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
快速排序算法改进:随机快排-荷兰国旗划分详解
随机快速排序-荷兰国旗划分算法详解 一、基础知识回顾1.1 快速排序简介1.2 荷兰国旗问题 二、随机快排 - 荷兰国旗划分原理2.1 随机化枢轴选择2.2 荷兰国旗划分过程2.3 结合随机快排与荷兰国旗划分 三、代码实现3.1 Python实现3.2 Java实现3.3 C实现 四、性能分析4.1 时间复杂度…...
WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...
