SpringBoot整合Caffeine
1. 简介
Caffeine 是基于Java 8 开发的、提供了近乎最佳命中率的高性能本地缓存组件,Spring5 开始不再支持 Guava Cache,改为使用 Caffeine。Caffeine与其他本地缓存的性能比较如下:
Caffeine具有以下功能:
1. 自动加载条目到缓存中,可选异步方式
2. 可以基于大小剔除
3. 可以设置过期时间,时间可以从上次访问或上次写入开始计算
4. 异步刷新
5. keys自动包装在弱引用中
6. values自动包装在弱引用或软引用中
7. 条目剔除通知
8. 缓存访问统计
2. SpringBoot整合Caffeine
下面介绍SpringBoot使用Caffeine的简单案例
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.young</groupId>
<artifactId>caffeine02</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--引入caffeine依赖-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
数据库内容如下图:
User实体类
package com.young.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@TableName(value = "t_user")
@ToString
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String sex;
private Integer age;
}
UserMapper.java
package com.young.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.young.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
UserService.java
package com.young.service;
import com.young.entity.User;
public interface UserService {
Boolean saveUser(User user);
Boolean updateUser(User user);
Boolean deleteUserById(Integer id);
User getUserById(Integer id);
}
UserServiceImpl.java
package com.young.service.impl;
import com.github.benmanes.caffeine.cache.Cache;
import com.young.entity.User;
import com.young.mapper.UserMapper;
import com.young.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Resource
private Cache<String,Object>caffeineCache;
@Override
public Boolean saveUser(User user) {
return userMapper.insert(user)>0;
}
@Override
public Boolean updateUser(User user) {
if (user.getId()==null){
return false;
}
if(userMapper.updateById(user)>0){
//删除缓存
caffeineCache.asMap().remove(user.getId()+"");
return true;
}
return false;
}
@Override
public Boolean deleteUserById(Integer id) {
if (userMapper.deleteById(id)>0){
//删除缓存
caffeineCache.asMap().remove(id+"");
return true;
}
return false;
}
@Override
public User getUserById(Integer id) {
User user=(User)caffeineCache.asMap().get(id+"");
if (user!=null){
log.info("从缓存中获取==============");
return user;
}
log.info("从数据库中获取===============");
user=userMapper.selectById(id);
if (user==null){
log.info("数据为空===========");
return null;
}
caffeineCache.put(id+"",user);
return user;
}
}
常用的配置参数
expireAfterWrite:写入间隔多久淘汰;
expireAfterAccess:最后访问后间隔多久淘汰;
refreshAfterWrite:写入后间隔多久刷新,该刷新是基于访问被动触发的,支持异步刷新和同步刷新,如果和 expireAfterWrite 组合使用,能够保证即使该缓存访问不到、也能在固定时间间隔后被淘汰,否则如果单独使用容易造成OOM;
expireAfter:自定义淘汰策略,该策略下 Caffeine 通过时间轮算法来实现不同key 的不同过期时间;
maximumSize:缓存 key 的最大个数;
weakKeys:key设置为弱引用,在 GC 时可以直接淘汰;
weakValues:value设置为弱引用,在 GC 时可以直接淘汰;
softValues:value设置为软引用,在内存溢出前可以直接淘汰;
executor:选择自定义的线程池,默认的线程池实现是 ForkJoinPool.commonPool();
maximumWeight:设置缓存最大权重;
weigher:设置具体key权重;
recordStats:缓存的统计数据,比如命中率等;
removalListener:缓存淘汰监听器;
writer:缓存写入、更新、淘汰的监听器。
CaffeineCache的配置类,我们配置过期时间为10秒,初始容量100,最大容量200
package com.young.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CaffeineConfig {
@Bean
public Cache caffeineCache(){
return Caffeine.newBuilder()
//设置10秒后过期,方便后续观察现象
.expireAfterWrite(10, TimeUnit.SECONDS)
//初始容量为100
.initialCapacity(100)
//最大容量为200
.maximumSize(200)
.build();
}
}
然后创建测试类,进行测试:
package com.young;
import com.young.entity.User;
import com.young.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class Caffine02ApplicationTest {
@Autowired
private UserService userService;
@Test
public void testCache(){
//获取缓存
User user = userService.getUserById(1);
log.info("第一次从数据库获取缓存:{}",user);
user=userService.getUserById(1);
log.info("第二次从缓存中获取:{}",user);
//过期时间为10秒,我们10秒后再获取
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
user=userService.getUserById(1);
log.info("10秒后再次获取user:{}",user);
}
}
第一次获取user时,因为缓存中没有内容,所以会从数据库中查询,第二次会从缓存中获取到内容,然后睡眠10秒,此时缓存过期了,因此再次获取user的时候,会从数据库中获取,运行结果如下图所示:
3. Caffeine的四种类型的加载策略
3.1 Manual手动加载
我们修改CaffeineConfig.java
package com.young.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CaffeineConfig {
@Bean
@Qualifier(value = "caffeineCache")
public Cache caffeineCache(){
return Caffeine.newBuilder()
//设置10秒后过期,方便后续观察现象
.expireAfterWrite(10, TimeUnit.SECONDS)
//初始容量为100
.initialCapacity(100)
//最大容量为200
.maximumSize(200)
.build();
}
//定义manualCaffeineCache,用来演示手动加载
@Bean
@Qualifier(value = "manualCaffeineCache")
public Cache manualCaffeineCache(){
return Caffeine.newBuilder()
.expireAfterWrite(10,TimeUnit.SECONDS)
.initialCapacity(50)
.maximumSize(100)
.build();
}
}
修改Caffeine02ApplicationTest.java,添加测试用例
package com.young;
import com.github.benmanes.caffeine.cache.Cache;
import com.young.entity.User;
import com.young.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
@Slf4j
public class Caffine02ApplicationTest {
@Autowired
private UserService userService;
@Resource
@Qualifier(value = "manualCaffeineCache")
private Cache<String,String> manualCaffineCache;
@Test
public void testCache(){
//获取缓存
User user = userService.getUserById(1);
log.info("第一次从数据库获取缓存:{}",user);
user=userService.getUserById(1);
log.info("第二次从缓存中获取:{}",user);
//过期时间为10秒,我们10秒后再获取
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
user=userService.getUserById(1);
log.info("10秒后再次获取user:{}",user);
}
@Test
public void testManualCaffeineCache(){
//将数据放入缓存
manualCaffineCache.put("The best language","java");
//获取key对应的value,如果不存在,返回null
String the_best_language = manualCaffineCache.getIfPresent("The best language");
System.out.println(the_best_language);
//删除entry
manualCaffineCache.invalidate("The best language");
the_best_language= manualCaffineCache.getIfPresent("The best language");
System.out.println(the_best_language);
//以map的形式进行增删改查==================
manualCaffineCache.asMap().put("best","java");
manualCaffineCache.asMap().put("best1","SpringBoot");
String best = manualCaffineCache.asMap().get("best");
String best1 = manualCaffineCache.asMap().get("best1");
System.out.println("best:"+best);
System.out.println("best1:"+best1);
//删除entry
manualCaffineCache.asMap().remove("best");
manualCaffineCache.asMap().remove("best1");
best = manualCaffineCache.asMap().get("best");
best1 = manualCaffineCache.asMap().get("best1");
System.out.println("best:"+best);
System.out.println("best1:"+best1);
}
}
常用的方法:
getIfPresent(Object key): 获取value值,如果entry不存在,返回null
put(Object key,Object value): 添加entry到缓存中
invalidate(Object key): 删除entry
asMap(): 将cache以map的形式进行操作
测试testManualCaffeineCache,结果如下:
3.2 loading
LoadingCache通过关联一个CacheLoader来构建Cache, 当缓存未命中会调用CacheLoader的load方法生成V,还可以通过LoadingCache的getAll方法批量查询, 当CacheLoader未实现loadAll方法时, 会批量调用load方法聚合会返回。当CacheLoader实现loadAll方法时, 则直接调用loadAll返回。
我们在CaffeineConfig中添加下面的bean
@Bean
@Qualifier(value = "loadingCaffeineCache")
public LoadingCache<String, Object> loadingCaffeineCache(){
return Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(500)
.build(new CacheLoader<String, Object>() {
//缓存未命中时,使用下面的方法生成value
@Override
public @Nullable Object load(@NonNull String key) throws Exception {
User user=new User();
user.setId(-1);
user.setUsername(key);
user.setPassword(key);
return user;
}
@Override
public Map<String,Object>loadAll(Iterable<? extends String>keys){
Map<String,Object>map=new HashMap<>();
for (String key:keys){
User user=new User();
user.setId(-1);
user.setUsername(key);
user.setPassword(key);
map.put(key,user);
}
return map;
}
});
}
然后添加测试方法
@Resource
@Qualifier(value = "loadingCaffeineCache")
private LoadingCache<String,Object> loadingCaffeineCache;
@Test
public void testLoadingCaffeineCache(){
User user=new User();
user.setId(1);
user.setUsername("cxy");
user.setPassword("123456");
user.setAge(20);
user.setSex("男");
loadingCaffeineCache.put("1",user);
User res=(User)loadingCaffeineCache.getIfPresent("1");
System.out.println("res:"+res);
res=(User)loadingCaffeineCache.getIfPresent("2");
System.out.println("res:"+res);
List<String>list= Arrays.asList("1","2","3");
Map<@NonNull String, @NonNull Object> resMap = loadingCaffeineCache.getAllPresent(list);
System.out.println("resMap:"+resMap);
System.out.println("上面调用的都是IfPresent(),即存在才返回,因此不会触发我们刚才的两个load函数==========");
res=(User)loadingCaffeineCache.get("2");
System.out.println("res:"+res);
resMap= loadingCaffeineCache.getAll(list);
System.out.println("resMap:"+resMap);
}
测试结果如下:
3.3 Asynchronous Manual异步手动
AsyncCache是另一种Cache,它基于Executor计算Entry,并返回一个CompletableFuture
和Cache的区别是, AsyncCache计算Entry的线程是ForkJoinPool线程池. 手动Cache缓存是调用线程进行计算
private static void demo() throws ExecutionException, InterruptedException {
AsyncCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.SECONDS)
.buildAsync();
// Lookup and asynchronously compute an entry if absent
CompletableFuture<String> future = cache.get("hello", k -> createExpensiveGraph(k));
System.out.println(future.get());
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
3.4 异步自动
AsyncLoadingCache 是关联了 AsyncCacheLoader 的 AsyncCache
public static void demo() throws ExecutionException, InterruptedException {
AsyncLoadingCache<String,String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.maximumSize(500)
.buildAsync(k -> createExpensiveGraph(k));
CompletableFuture<String> future = cache.get("hello");
System.out.println(future.get());
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
4. 配置监听器
修改caffeineConfig,添加监听器
@Bean
@Qualifier(value = "listenerCaffeineCache")
public Cache listenerCaffeineCache(){
return Caffeine.newBuilder()
.expireAfterWrite(10,TimeUnit.SECONDS)
.initialCapacity(100)
.maximumSize(200)
.evictionListener(new RemovalListener<String, Object>() {
@Override
public void onRemoval(@Nullable String key, @Nullable Object value, @NonNull RemovalCause removalCause) {
System.out.println("evictionListener:key="+key+",value="+value+",removalCause="+removalCause);
}
}).removalListener((key,value,cause)->{
System.out.println("removalListener:key="+key+",value="+value+",cause="+cause);
})
.build();
}
测试代码
@Resource
@Qualifier(value = "listenerCaffeineCache")
private Cache<String,Object>listenerCaffeineCache;
@Test
public void testListenerCaffeineCache() throws InterruptedException {
listenerCaffeineCache.put("cxy","程序员");
listenerCaffeineCache.put("best","java");
listenerCaffeineCache.invalidate("cxy");
listenerCaffeineCache.asMap().remove("best");
}
测试结果如下: