一、传统应用的多级缓存
大型网站都会有很多缓存,常用的缓存EhCache、Caffeine等内存缓存,一般的服务的架构如图所示:
引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
二、统计
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通过使用Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个CacheStats对象,其将会含有一些统计指标,比如:
hitRate(): 查询缓存的命中率
evictionCount(): 被驱逐的缓存数量
averageLoadPenalty(): 新值被载入的平均耗时
配合SpringBoot提供的RESTful Controller,能很方便的查询Cache的使用情况。
三、四种缓存添加策略
Caffeine提供了四种缓存添加策略
- 手动加载cache
- 自动加载LoadingCache
- 手动异步加载AsyncCache
- 自动异步加载AsyncLoadingCache
四、三种驱逐策略
Caffeine 提供了三种驱动策略,分别是基于容量,时间和引用三种类型。
2.1基于内容
缓存将会尝试通过基于【Window TinyLfu】驱逐掉不会再被使用到的元素内容也有两种实现:
- 基于个数:如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)
- 基于权重:你的缓存可能中的元素可能存在不同的“权重”–打个比方,你的缓存中的元素可能有不同的内存占用–你也许需要借助Caffeine.weigher(Weigher) 方法来界定每个元素的权重并通过 Caffeine.maximumWeight(long)方法来界定缓存中元素的总权重来实现上述的场景。
2.2基于时间
基于时间也有三种实现:
- expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。理想选择: 当session因为不活跃而使元素过期的情况下使用此
- expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。理想选择: 对被缓存的元素的时效性存在要求的场景下使用此
- expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。理想选择: 当被缓存的元素过期时间收到外部资源影响的时候使用此。
2.3基于引用
基于引用也有三种实现:
- Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收
- Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收
- Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用
注意: ● 异步不支持 ● key支持:弱引用 ● value支持:软引用、弱引用。
五、常用的缓存框架对比
六、存储空间
W-TinyLFU将缓存存储空间分为两个大的区域:Window Cache(1%)和Main Cache(99%).
- Main Cache[SLRU(Segmented LRU,即分段 LRU)]进一步划分为Protected Cache(保护区 80%)和Probation Cache(考察区 20%)两个区域,这两个区域都是基于LRU的Cache。
- 猜测:1%、99%(20%、80%)这样的配置应该是实验得来。不过这个比例 Caffeine 会在运行时根据统计数据(statistics)去动态调整
运行过程如下:
1,当有新的缓存项写(item)入缓存时,会先写入Window Cache区域,当Window Cache空间满时,最旧的缓存数据根据LRU策略,会被移出Window Cache,放到 probation(观察) 区。
2,如果 probation 区也满了,就把 item 和 probation 将要淘汰的元素 victim,两个进行“PK”使用TinyLFU算法,胜者留在 probation,输者就要被淘汰了。
3,Probation(观察区) 中的缓存项如果访问频率达到一定次数,会提升到Protected(保护区);
4,如果Protected也满了,最旧的缓存项也会移出Protected Cache,然后根据TinyLFU算法确定是丢弃(淘汰)还是写入Probation Cache。
【tips】每一个缓存项,都由 2 部分组成
● 数据:缓存数据本身;
● 访问频次:被访问的次数
七、SpringBoot的实战
按照Caffeine Github官网文档的描述,Caffeine是基于Java8的高性能缓存库。并且在Spring5(SpringBoot2.x)官方放弃了Guava(潘石榴),而使用了性能更优秀的Caffeine(咖啡因)作为默认的缓存方案。
SpringBoot使用Caffeine有两种方式:
方式一:直接引入Caffeine依赖,然后使用Caffeine的函数实现缓存
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
其次,设置缓存的配置选项
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
}
最后给服务添加缓存功能
@Slf4j
@Service
public class UserInfoServiceImpl {
/**
* 模拟数据库存储数据
*/
private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();
@Autowired
Cache<String, Object> caffeineCache;
public void addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
// 加入缓存
caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
}
public UserInfo getByName(Integer id) {
// 先从缓存读取
caffeineCache.getIfPresent(id);
UserInfo userInfo = (UserInfo) caffeineCache.asMap().get(String.valueOf(id));
if (userInfo != null){
return userInfo;
}
// 如果缓存中不存在,则从库中查找
userInfo = userInfoMap.get(id);
// 如果用户信息不为空,则加入缓存
if (userInfo != null){
caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
}
return userInfo;
}
public UserInfo updateUserInfo(UserInfo userInfo) {
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取旧的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替换内容
if (!StringUtils.isEmpty(oldUserInfo.getAge())) {
oldUserInfo.setAge(userInfo.getAge());
}
if (!StringUtils.isEmpty(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
// 将新的对象存储,更新旧对象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 替换缓存中的值
caffeineCache.put(String.valueOf(oldUserInfo.getId()),oldUserInfo);
return oldUserInfo;
}
@Override
public void deleteById(Integer id) {
userInfoMap.remove(id);
// 从缓存中删除
caffeineCache.asMap().remove(String.valueOf(id));
}
}
方式二:引入Caffeine和Spring Cache依赖,使用SpringCache注解方法实现缓存。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
其次,配置缓存管理类
@Configuration
public class CacheConfig {
/**
* 配置缓存管理器
*
* @return 缓存管理器
*/
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterAccess(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000));
return cacheManager;
}
}
最后给服务添加缓存功能
@Slf4j
@Service
@CacheConfig(cacheNames = "caffeineCacheManager")
public class UserInfoServiceImpl {
/**
* 模拟数据库存储数据
*/
private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();
@CachePut(key = "#userInfo.id")
public void addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
}
@Cacheable(key = "#id")
public UserInfo getByName(Integer id) {
return userInfoMap.get(id);
}
@CachePut(key = "#userInfo.id")
public UserInfo updateUserInfo(UserInfo userInfo) {
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取旧的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替换内容
if (!StringUtils.isEmpty(oldUserInfo.getAge())) {
oldUserInfo.setAge(userInfo.getAge());
}
if (!StringUtils.isEmpty(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
// 将新的对象存储,更新旧对象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 返回新对象信息
return oldUserInfo;
}
@CacheEvict(key = "#id")
public void deleteById(Integer id) {
userInfoMap.remove(id);
}
}