一、本地缓存 GuavaCache 介绍
Guava
是Java
工具包,而Guava Cache
是一套非常完善的本地缓存机制(JVM
缓存)。
Guava Cache
的设计来源于CurrentHashMap
,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。
二、应用场景与优势
1、本地缓存的应用场景
- 对性能有非常高的要求
- 不经常变化
- 占用内存不大
- 有访问整个集合的需求
- 数据允许不实时一致
2、Guava Cache 的优势
- 缓存过期和淘汰机制
在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期。
GuavaCache在缓存容量达到指定大小时,采用
LRU + FIFO
的方式,将不常使用的键值从Cache
中删除。
- 并发处理能力
GuavaCache
类似CurrentHashMap
,是线程安全的。提供了设置并发级别的
api
,使得缓存支持并发的写入和读取。采用分离锁机制,分离锁能够减小锁力度,提升并发能力。
分离锁是分拆锁定,把一个集合看分成若干
partition
, 每个partiton
一把锁。ConcurrentHashMap
就是分了16
个区域,这16
个区域之间是可以并发的。GuavaCache
采用Segment
做分区。
- 更新锁定
一般情况下,在缓存中查询某个
key
,如果不存在,则查源数据,并回填缓存。(Cache Aside Pattern
)在高并发下会出现,多次查源并重复回填缓存,可能会造成源(
DB
)的性能下降甚至宕机。而GuavaCache
可以在CacheLoader
中加以控制,对同一个key
,只让一个请求去读源并回填缓存,其他请求阻塞等待。
- 集成数据源
一般我们在业务中操作缓存,都会操作缓存和数据源两部分。而
GuavaCache
的get
可以集成数据源,在从缓存中读取不到时可以从数据源中读取数据并回填缓存。
- 监控缓存加载/命中情况
recordStats
统计
三、代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.idol</groupId>
<artifactId>guava-cache</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
MyDataSource.java
import java.util.HashMap;
import java.util.Map;
/**
* @author SupremeSir
* @description 模拟数据库
* @className com.idol.datasource.MyDataSource
* @date 2021/4/5 19:08
**/
public class MyDataSource {
public static Map<String, String> dataSourc = new HashMap<String, String>();
static {
dataSourc.put("1", "idol");
dataSourc.put("2", "sir");
dataSourc.put("3", "supreme");
dataSourc.put("4", "happy");
dataSourc.put("5", "lucky");
}
}
MyGuavaCache.java
import com.google.common.cache.*;
import com.idol.datasource.MyDataSource;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* @author SupremeSir
* @description GuavaCache 本地缓存对象
* @className com.idol.guava.GuavaCacheDemo
* @date 2021/4/5 18:59
**/
public class MyGuavaCache {
private LoadingCache<String, Object> cache;
public MyGuavaCache() {
this.cache = CacheBuilder.newBuilder()
/* 设置缓存大小
缓存淘汰策略:LRU(Least Recently Used,最近最少使用) + FIFO (First In First Out,先进先出)
*/
.maximumSize(3)
// 添加缓存删除监听
.removalListener(new RemovalListener<Object, Object>() {
public void onRemoval(RemovalNotification<Object, Object> removalNotification) {
System.out.println("缓存 " + removalNotification.getKey()
+ " 被删除,原因是:" + removalNotification.getCause());
}
})
// 缓存状态统计
.recordStats()
// 太久没访问,则删除缓存
.expireAfterAccess(3, TimeUnit.SECONDS)
// 写入缓存太久,则删除缓存
.expireAfterWrite(2, TimeUnit.SECONDS)
/* 弱值删除
如果缓存中的 key 所对应的 value 的指针生了变化,则删除缓存
eg:
Object val = new Object();
guavaCache.getCache().put("6", val);
val = new Object();
*/
.weakValues()
/* 弱键删除
如果缓存中的 value 所对应的 key 的指针生了变化,则删除缓存
*/
.weakKeys()
// 同时支持 CPU 核数个线程写缓存
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
/* 更新锁定,防止缓存穿透/击穿
GuavaCache 使用的是惰性删除。所以,如果配置了缓存过期(expireAfterWrite/expireAfterAccess),
则缓存过期后被再次获取已过期的缓存时,refreshAfterWrite 配置会允许一个线程回数据源进行数据查询并更新缓存。
如果超出指定时间(这里设置的为3秒)缓存仍未被更新,则返回缓存中的旧值。
*/
.refreshAfterWrite(3, TimeUnit.SECONDS)
.build(new CacheLoader<String, Object>() {
/* 回调方法
如果通过 cache 的 get 方法(不带 Callable 参数)获取缓存为空,则会执行 load 回调。
可以在该回调中执行连接数据库并完成缓存回填。
*/
public Object load(String key) {
System.out.println("---GuavaCache--- 缓存没有命中" + key
+ ",构造将执行数据库连接,从数据库中查询");
// 模拟数据库连接查询数据
return MyDataSource.dataSourc.get(key);
}
});
}
/**
* 获取缓存状态
*/
public CacheStats getCacheStatus() {
return cache.stats();
}
/**
* 带有回调的缓存取值方法
* 注意:当前方法由于自带回调,故构造函数中的数据库连接方法 load 将不再执行
* @param key
* @throws Exception
*/
public Object getWithCallBack(final String key) throws Exception {
return cache.get(key, new Callable<Object>() {
public Object call() {
System.out.println("没有命中,将执行 get 回调连接数据源");
return MyDataSource.dataSourc.get(key);
}
});
}
/**
* 查看当前缓存中的内容
*/
public void display() {
System.out.print("缓存大小:" + cache.size() + "\r\n");
Set<Map.Entry<String, Object>> entries = cache.asMap().entrySet();
for (Map.Entry<String, Object> entry : entries) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
System.out.println();
}
/**
* 初始化本地缓存(JVM缓存)
* @throws Exception
*/
public void initData() throws Exception {
for (int i = 1; i < 4; i++) {
// 通过 cache 对象调用 get 方法后会自动放入本地缓存
cache.get(String.valueOf(i));
}
}
public LoadingCache<String, Object> getCache() {
return cache;
}
}
CacheTest.java
import com.idol.guava.MyGuavaCache;
import java.util.Arrays;
/**
* @author SupremeSir
* @description 测试用例
* @className CacheTest
* @date 2021/4/5 19:35
**/
public class CacheTest {
public static void main(String[] args) throws Exception {
// 创建缓存对象
MyGuavaCache guavaCache = new MyGuavaCache();
// 初始化缓存
guavaCache.initData();
System.out.println("\r\n&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&\r\n");
System.out.println("命中缓存:" + guavaCache.getCache().get("1") + "\r\n");
// 删除单个缓存
guavaCache.getCache().invalidate("1");
guavaCache.display();
// 删除多个缓存
guavaCache.getCache().invalidateAll(Arrays.asList("2", "3"));
guavaCache.display();
// 删除所有缓存
guavaCache.initData();
guavaCache.getCache().invalidateAll();
System.out.println();
// 验证 expireAfterWrite 设置
guavaCache.initData();
guavaCache.display();
Thread.sleep(3000);
// 查看当前缓存中的内容
guavaCache.display();
System.out.println("\r\n脱靶缓存:\r\n" + guavaCache.getCache().get("4"));
// 验证 expireAfterAccess 设置
Thread.sleep(4000);
// 查看当前缓存中的内容
guavaCache.display();
// 只从缓存中获取,没有则返回 null
System.out.println("\r\n没有命中直接返回:" + guavaCache.getCache().getIfPresent("5") + "\r\n");
// 验证带有回调功能的 get 方法
System.out.println(guavaCache.getWithCallBack("5") + "\r\n");
// 查看当前缓存中的内容
guavaCache.display();
/* 查看缓存状态
CacheStats{hitCount=1, missCount=6, loadSuccessCount=5, loadExceptionCount=0, totalLoadTime=2715600, evictionCount=2}
hitCount:命中数
missCount:脱靶数
loadSuccessCount:回调成功数
loadExceptionCount:回调异常数
totalLoadTime:总耗时
evictionCount:驱逐个数(设置缓存大小后,被 LRU+FIFO 算法剔除的缓存个数)
*/
System.out.println("\r\n" + guavaCache.getCacheStatus());
}
}