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

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">

4.0.0

com.young

caffeine02

1.0-SNAPSHOT

spring-boot-starter-parent

org.springframework.boot

2.7.0

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

mysql

mysql-connector-java

com.baomidou

mybatis-plus-boot-starter

3.4.3

com.github.ben-manes.caffeine

caffeine

org.projectlombok

lombok

11

11

数据库内容如下图: 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 {

}

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 CachecaffeineCache;

@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 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 loadingCaffeineCache(){

return Caffeine.newBuilder()

.expireAfterWrite(60, TimeUnit.SECONDS)

.maximumSize(500)

.build(new CacheLoader() {

//缓存未命中时,使用下面的方法生成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 MaploadAll(Iterablekeys){

Mapmap=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 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);

Listlist= 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 cache = Caffeine.newBuilder()

.maximumSize(500)

.expireAfterWrite(10, TimeUnit.SECONDS)

.buildAsync();

// Lookup and asynchronously compute an entry if absent

CompletableFuture 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 cache = Caffeine.newBuilder()

.expireAfterWrite(10, TimeUnit.SECONDS)

.maximumSize(500)

.buildAsync(k -> createExpensiveGraph(k));

CompletableFuture 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() {

@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 CachelistenerCaffeineCache;

@Test

public void testListenerCaffeineCache() throws InterruptedException {

listenerCaffeineCache.put("cxy","程序员");

listenerCaffeineCache.put("best","java");

listenerCaffeineCache.invalidate("cxy");

listenerCaffeineCache.asMap().remove("best");

}

测试结果如下:

参考文章:

Caffeine本地缓存详解 高性能缓存 Caffeine 原理及实战

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: