SpringBoot整合Caffeine
作者:mmseoamin日期:2023-12-19

SpringBoot整合Caffeine

1. 简介

Caffeine 是基于Java 8 开发的、提供了近乎最佳命中率的高性能本地缓存组件,Spring5 开始不再支持 Guava Cache,改为使用 Caffeine。Caffeine与其他本地缓存的性能比较如下:

SpringBoot整合Caffeine,在这里插入图片描述,第1张

Caffeine具有以下功能:

1. 自动加载条目到缓存中,可选异步方式
2. 可以基于大小剔除
3. 可以设置过期时间,时间可以从上次访问或上次写入开始计算
4. 异步刷新
5. keys自动包装在弱引用中
6. values自动包装在弱引用或软引用中
7. 条目剔除通知
8. 缓存访问统计

2. SpringBoot整合Caffeine

下面介绍SpringBoot使用Caffeine的简单案例

pom.xml



    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
    

数据库内容如下图:

SpringBoot整合Caffeine,在这里插入图片描述,第2张

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的时候,会从数据库中获取,运行结果如下图所示:

SpringBoot整合Caffeine,在这里插入图片描述,第3张

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,结果如下:

SpringBoot整合Caffeine,在这里插入图片描述,第4张

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);
    }

测试结果如下:

SpringBoot整合Caffeine,在这里插入图片描述,第5张

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");
    }

测试结果如下:

SpringBoot整合Caffeine,在这里插入图片描述,第6张

参考文章:

Caffeine本地缓存详解

高性能缓存 Caffeine 原理及实战

上一篇:爬虫基本原理

下一篇:仓库管理系统