caffeine本地缓存
文章目录
一:本地缓存Caffeine介绍
一般情况下,缓存针对的主要是读操作。当你的功能遇到下面的场景时,就可以选择使用缓存组件进行性能优化:
- 存在数据热点,缓存的数据能够被频繁使用;
- 读操作明显比写操作要多;
- 下游功能存在着比较悬殊的性能差异,下游服务能力有限;
- 加入缓存以后,不会影响程序的正确性,或者引入不可预料的复杂性
日常开发中,基本上每个项目中都会使用到Redis、MongoDB等缓存中间件
它能够很好的作为分布式缓存组件提供多个服务间的缓存,但是还是需要网络开销,增加时耗
除了分布式缓存,其实还有一种缓存 - 本地缓存:直接从本地内存中读取,没有网络开销,在某些场景比远程缓存更合适。
Guava cache、ehcache、Caffeine是目前比较流行的本地缓存组件,但Caffeine号称是本地缓存绝对的王者
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。缓存和ConcurrentMap有点相似,但还是有所区别,最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。
Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。
在 Spring5 (springboot 2.x) 后,Spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件
二:Caffeine功能和性能
Caffeine提供了多种灵活的构造方法,从而可以创建多种特性的本地缓存。
- 自动把数据加载到本地缓存中,并且可以配置异步
- 基于数量剔除策略
- 基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】
- 异步刷新
- Key会被包装成Weak引用
- Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏
- 数据剔除提醒
- 写入广播机制
- 缓存访问可以统计
不管在并发读、并发写还是并发读写的场景下,Caffeine 的性能都大幅领先于其他本地开源缓存组件
三:Caffeine 配置说明
Caffeine主要提供了以下一些配置:
- initialCapacity=[integer]: 设置初始缓存的空间大小;
- maximumSize=[long]: 设置缓存的最大条数;
- maximumWeight=[long]: 设置缓存的最大权重;
- expireAfterAccess=[持续时间]: 最后一次写入或者访问后经过多久时间过期;
- refreshAfterWrite=[持续时间]: 创建缓存或者最后一次更新缓存后经过多久时间间隔,刷新缓存;
- weakKeys: 打开key的弱引用;
- weakValues: 打开value的弱引用;
- softValues: 打开value的软引用;
- recordStats: 打开统计功能;
注意下面的问题
- weakValues 和 softValues 不可以同时使用
- maximumSize 和 maximumWeight 不可以同时使用
- expireAfterWrite 和 expireAfterAccess 同时存在时,以expireAfterWrite为准
四:SpringBoot多级缓存
首先我们要明白为什么要使用多级缓存?
- 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多;
- 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的;
因此在项目中,我们可以将热点数据放本地缓存,作为一级缓存,将非热点数据放redis缓存,作为二级缓存,减少Redis的查询压力。
使用流程大致如下:
- 首先从一级缓存(caffeine-本地应用内)中查找数据;
- 如果没有的话,则从二级缓存(redis-内存)中查找数据;
- 如果还是没有的话,再从数据库(数据库-磁盘)中查找数据;
SpringBoot 有两种使用 Caffeine 作为缓存的方式:
- 方式一:直接引入 Caffeine 依赖,然后使用 Caffeine 方法实现缓存;
- 方式二:引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现缓存;
方式二举例
第一步:依赖 & 配置
<!-- caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.13</version>
</dependency>
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=50,expireAfterWrite=30s
第二步:本地缓存配置类
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* <p>
* 功能描述:咖啡因进程内缓存配置
* </p>
*
* @author cui haida
* @date 2024/07/03/10:39
*/
@Configuration
@ConditionalOnExpression("'${spring.cache.type}'.equals('caffeine')") // 当前仅当spring.cache.type=caffeine时生效
public class CaffeineCacheConfig {
/**
* 这个方式一般封装成为工具类
* 存储
* Cache<String, Object> caffeine = (Cache<String, Object>) SpringUtils.getBean("caffeineCache");
* caffeine.put(key, value);
* 获取
* Cache<String, Object> caffeine = (Cache<String, Object>) SpringUtils.getBean("caffeineCache");
* Object value = caffeine.asMap().get(key);
*/
@Bean
public Cache<String, Object> caffeineCache() {
// 创建一个名为"myCache"的缓存,使用caffeineConfig()方法获取的Caffeine配置。
return Caffeine.newBuilder()
.maximumSize(50)
.expireAfterWrite(Duration.ofSeconds(30))
.build();
}
/**
* 配置缓存管理器。
* 使用CaffeineCacheManager来管理缓存,它基于Caffeine构建,提供了缓存的创建和管理能力。
* 配置Caffeine缓存。
*
* 方式一:@Cacheable(value = "xxx")
* 优点:通过配置不同的缓存名称,可以实现每个缓存大小独立管理
* 问题:面这种方式,无法手动set缓存,只有在获取的时候,才会塞缓存进去
* 方式二:CacheManager类
* Cache caffeineCacheBuilder = cacheManager.getCache("xxx");
* caffeineCacheBuilder.put(key, value);
*
* @return 返回配置好的CaffeineCacheManager实例。
*/
@Bean(name = "myCacheManager")
public CacheManager myCacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().maximumSize(50)
// 设置缓存条目的过期时间,这里设置为30秒,超过30秒未访问的缓存条目将被自动移除。
.expireAfterWrite(Duration.ofSeconds(30));
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
第三步:主启动类中加入@EnableCache
@EnableCaching
@SpringBootApplication
public class ParagraphSplittingTesterApplication {
public static void main(String[] args) {
SpringApplication.run(ParagraphSplittingTesterApplication.class, args);
}
}
第四步:使用@CacheConfig
& @Cacheable
package cn.com.chnsys.logic.impl;
import cn.com.chnsys.entity.ParagraphNameEntity;
import cn.com.chnsys.logic.ParagraphLogic;
import cn.com.chnsys.logic.ParagraphNameLogic;
import cn.com.chnsys.pojo.dto.ParagraphEntityDTO;
import cn.com.chnsys.service.ParagraphNameService;
import cn.com.chnsys.service.ParagraphService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 段落名称实现类
*/
@Component
@CacheConfig(cacheNames = "paragraphName")
public class ParagraphNameLogicImpl implements ParagraphNameLogic {
@Resource
private ParagraphNameService paragraphNameService;
/**
* 获取段落名称列表
* @return 段落名称列表
*/
@Override
@Cacheable(key = "'paragraphNameList' + #paragraphType", cacheManager = "myCacheManager")
public List<ParagraphNameEntity> getParagraphNameList(String paragraphType) {
LambdaQueryWrapper<ParagraphNameEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ParagrphNameEntity::getFileType, paragraphType);
return paragraphNameService.list(queryWrapper);
}
}
下面以第一种方式为例说下多级缓存的实现过程
第一步:依赖 & 配置
<!--caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
第二步:本地缓存配置类
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 本地缓存Caffeine配置类
*/
@Configuration
public class LocalCacheConfiguration {
@Bean("localCacheManager")
public Cache<String, Object> localCacheManager() {
return Caffeine.newBuilder()
//写入或者更新5s后,缓存过期并失效, 实际项目中肯定不会那么短时间就过期,根据具体情况设置即可
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(50)
// 缓存的最大条数,通过 Window TinyLfu算法控制整个缓存大小
.maximumSize(500)
//打开数据收集功能
.recordStats()
.build();
}
}
第三步:redis缓存配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
//关联
template.setConnectionFactory(factory);
//设置key的序列化方式
// template.setKeySerializer();
//设置value的序列化方式
// template.setValueSerializer();
return template;
}
}
第四步:定义实体对象 <- 缓存的Value
// 省略全参构造器,get & set,toString
public class User implements Serializable {
private String id;
private String name;
}
第五步:定义Service层
public interface UserService {
// add
void add(User user);
// 通过id查找
User getById(String id);
// 更新
User update(User user);
// 删除
void deleteById(String id);
}
import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.Cache;
import com.wsh.springboot_caffeine.entity.User;
import com.wsh.springboot_caffeine.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库存储数据
*/
private static HashMap<String, User> userMap = new HashMap<>();
private final RedisTemplate<String, Object> redisTemplate;
private final Cache<String, Object> caffeineCache;
@Autowired
public UserServiceImpl(RedisTemplate<String, Object> redisTemplate, @Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
}
static {
userMap.put("1", new User("1", "zhangsan"));
userMap.put("2", new User("2", "lisi"));
userMap.put("3", new User("3", "wangwu"));
userMap.put("4", new User("4", "zhaoliu"));
}
@Override
public void add(User user) {
// 1.保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 2.保存redis缓存
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
// 3.保存数据库(模拟)
userMap.put(user.getId(), user);
}
@Override
public User getById(String id) {
// 1.先从Caffeine缓存中读取
Object o = caffeineCache.getIfPresent(id);
if (Objects.nonNull(o)) {
System.out.println("从Caffeine中查询到数据...");
return (User) o;
}
// 2.如果缓存中不存在,则从Redis缓存中查找
String jsonString = (String) redisTemplate.opsForValue().get(id);
User user = JSON.parseObject(jsonString, User.class);
if (Objects.nonNull(user)) {
System.out.println("从Redis中查询到数据...");
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
return user;
}
// 3.如果Redis缓存中不存在,则从数据库中查询
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 保存Caffeine缓存
caffeineCache.put(user.getId(), user);
// 保存Redis缓存,20s后过期
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
}
System.out.println("从数据库中查询到数据...");
return user;
}
@Override
public User update(User user) {
User oldUser = userMap.get(user.getId());
oldUser.setName(user.getName());
// 1.更新数据库
userMap.put(oldUser.getId(), oldUser);
// 2.更新Caffeine缓存
caffeineCache.put(oldUser.getId(), oldUser);
// 3.更新Redis数据库
redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
return oldUser;
}
@Override
public void deleteById(String id) {
// 1.删除数据库
userMap.remove(id);
// 2.删除Caffeine缓存
caffeineCache.invalidate(id);
// 3.删除Redis缓存
redisTemplate.delete(id);
}
}
使用方式二进行多级缓存实现
第一步:依赖 & 配置
<!--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>
spring:
cache:
type: caffeine
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
第二步:caffeine自动配置类,注意:如果使用了多个cahce,比如redis、caffeine等,必须指定某一个CacheManage为@Primary,在@Cacheable注解中没指定 cacheManager 的话,则使用标记为primary的那个。
/**
* @Description: Caffeine自动配置类
*/
//自动配置功能
@Configuration
//开启缓存功能
@EnableCaching
public class CaffeineCacheConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = CaffeineCacheInitializer.initCaffeineCache();
if (CollectionUtils.isEmpty(caches)) {
return cacheManager;
}
cacheManager.setCaches(caches);
return cacheManager;
}
}
/**
* @Description: CaffeineCache初始化器
*/
public class CaffeineCacheInitializer {
public static List<CaffeineCache> initCaffeineCache() {
List<CaffeineCache> caffeineCacheList = new ArrayList<>();
CaffeineCache userCache = new CaffeineCache(CacheKey.USER_CACHE_KEY, Caffeine.newBuilder().recordStats()
.expireAfterWrite(5, TimeUnit.SECONDS)
.maximumSize(100)
.build());
caffeineCacheList.add(userCache);
//将所有需要定义的CaffeineCache添加到容器中
//....
return caffeineCacheList;
}
}
/**
* @Description: 缓存Key常量,统一维护
*/
public class CacheKey {
public static final String USER_CACHE_KEY = "userCache";
}
第三步:redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
//关联
template.setConnectionFactory(factory);
//设置key的序列化方式
// template.setKeySerializer();
//设置value的序列化方式
// template.setValueSerializer();
return template;
}
}
第四步:接口实现类
import com.alibaba.fastjson.JSON;
import com.wsh.springboot_caffeine2.constant.CacheKey;
import com.wsh.springboot_caffeine2.entity.User;
import com.wsh.springboot_caffeine2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库存储数据
*/
private static HashMap<String, User> userMap = new HashMap<>();
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public UserServiceImpl(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
static {
userMap.put("1", new User("1", "zhangsan"));
userMap.put("2", new User("2", "lisi"));
userMap.put("3", new User("3", "wangwu"));
userMap.put("4", new User("4", "zhaoliu"));
}
@Override
// 1.保存Caffeine缓存 注意必须返回User对象出去,如果是void的话,Caffeine并不能帮我们存入缓存中
@CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
public User add(User user) {
// 2.保存redis缓存
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
// 3.保存数据库(模拟)
userMap.put(user.getId(), user);
return user;
}
@Override
// 1.先从Caffeine缓存中读取
@Cacheable(value = CacheKey.USER_CACHE_KEY, key = "#id", sync = true)
public User getById(String id) {
// 2.如果缓存中不存在,则从Redis缓存中查找
String jsonString = (String) redisTemplate.opsForValue().get(id);
User user = JSON.parseObject(jsonString, User.class);
if (Objects.nonNull(user)) {
System.out.println("从Redis中查询到数据...");
return user;
}
// 3.如果Redis缓存中不存在,则从数据库中查询
user = userMap.get(id);
if (Objects.nonNull(user)) {
// 保存Redis缓存,20s后过期
redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
}
System.out.println("从数据库中查询到数据...");
return user;
}
@Override
//1.更新Caffeine缓存
@CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
public User update(User user) {
User oldUser = userMap.get(user.getId());
oldUser.setName(user.getName());
// 2.更新数据库
userMap.put(oldUser.getId(), oldUser);
// 3.更新Redis数据库
redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
return oldUser;
}
@Override
//1.删除Caffeine缓存
@CacheEvict(value = CacheKey.USER_CACHE_KEY, key = "#id")
public void deleteById(String id) {
// 2.删除数据库
userMap.remove(id);
// 3.删除Redis缓存
redisTemplate.delete(id);
}
}
五:API说明
caffeineCache.put(user.getId(), user)
:保存本地缓存;caffeineCache.invalidate(id)
:移除指定的本地缓存;caffeineCache.getIfPresent(id)
: 从本地缓存中获取值,如果缓存中不存指定的值,则方法将返回 null;caffeineCache.get(id, Function<>)
: 从本地缓存中获取值,该方法还支持将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中,如果缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,则返回null。
//手动加载
caffeineCache.get(id, new Function<String, Object>() {
@Override
public Object apply(String s) {
return "hello world";
}
});
// 或者简写成下面这样:
caffeineCache.get(id, s -> "hello world");
caffeine LoadingCache同步加载:
// 同步加载数据指的是,在get不到数据时最终会调用build构造时提供的CacheLoader对象中的load函数
// 如果返回值则将其插入缓存中,并且返回
LoadingCache<Integer, Integer> loadingCache = Caffeine.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.maximumSize(500)
// 在get不到数据时最终会调用build构造时提供的CacheLoader对象中的load函数
.build(new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer key) {
return key;
}
});
caffeine AsyncCache异步加载:
// 使用executor设置线程池
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.maximumSize(500)
.executor(Executors.newFixedThreadPool(5)) //当然也可以使用自定义线程池实现
.buildAsync();
// 具体使用
CompletableFuture<String> completableFuture = asyncCache.get("1", new Function<String, String>() {
@Override
public String apply(String s) {
//执行所在的线程是ForkJoinPool线程池提供的线程
return "hello world";
}
});
completableFuture.get();
六:Caffeine的软引用与弱引用
- 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存;
- 弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
注意:key 支持弱引用,而 value 则支持弱引用和软引用。需要注意的是,AsyncCache 不支持软引用和弱引用。
// 软引用,当进行GC的时候进行回收
Caffeine.newBuilder().softValues().build();
// 弱引用,当key和缓存元素都不再存在其他强引用的时候回收
Caffeine.newBuilder().weakKeys().weakValues().build();