Caffeine 是基于Java 8 开发的、提供了近乎最佳命中率的高性能本地缓存组件,Spring5 开始不再支持 Guava Cache,改为使用 Caffeine。Caffeine与其他本地缓存的性能比较如下:
Caffeine具有以下功能:
1. 自动加载条目到缓存中,可选异步方式 2. 可以基于大小剔除 3. 可以设置过期时间,时间可以从上次访问或上次写入开始计算 4. 异步刷新 5. keys自动包装在弱引用中 6. values自动包装在弱引用或软引用中 7. 条目剔除通知 8. 缓存访问统计
下面介绍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
数据库内容如下图:
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的时候,会从数据库中获取,运行结果如下图所示:
我们修改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 CachemanualCaffineCache; @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,结果如下:
LoadingCache通过关联一个CacheLoader来构建Cache, 当缓存未命中会调用CacheLoader的load方法生成V,还可以通过LoadingCache的getAll方法批量查询, 当CacheLoader未实现loadAll方法时, 会批量调用load方法聚合会返回。当CacheLoader实现loadAll方法时, 则直接调用loadAll返回。
我们在CaffeineConfig中添加下面的bean
@Bean @Qualifier(value = "loadingCaffeineCache") public LoadingCacheloadingCaffeineCache(){ 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 Map loadAll(Iterable extends String>keys){ Map 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 LoadingCacheloadingCaffeineCache; @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 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); }
测试结果如下:
AsyncCache是另一种Cache,它基于Executor计算Entry,并返回一个CompletableFuture
和Cache的区别是, AsyncCache计算Entry的线程是ForkJoinPool线程池. 手动Cache缓存是调用线程进行计算
private static void demo() throws ExecutionException, InterruptedException { AsyncCachecache = 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(); }
AsyncLoadingCache 是关联了 AsyncCacheLoader 的 AsyncCache
public static void demo() throws ExecutionException, InterruptedException { AsyncLoadingCachecache = 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(); }
修改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 原理及实战
上一篇:爬虫基本原理