JVM缓存就是将数据缓存到JVM内存中,这种缓存是最快的一种缓存方式。如果缓存的数据不多,并且服务是单机的话,那么就可以考虑使用JVM缓存而非使用第三方的redis
、memcache
等。
JVM缓存我们可以选择将需要缓存的数据存储到一个Map
中,然后启动一个线程去管理这个Map
中的数据。Guava Cache
做的就是这样的一件事。当然它会把这件事做好的同时拓展更多的接口,使开发者使用时更加方便。
安装依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
构建缓存
普通缓存
Guava Cache
提供了Builder
模式构建缓存对象。
@Test
public void cacheTest() throws InterruptedException {
Cache<String, String> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用算法来移除缓存项
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 设置n秒未访问就过期
// .expireAfterAccess(60, TimeUnit.SECONDS)
// 写入数据后n秒过期,只阻塞当前数据加载线程,其他线程返回旧值
// .refreshAfterWrite(60, TimeUnit.SECONDS)
// 缓存移除监听器
.removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
.build();
// 存缓存
cache.put("test", "test");
// 输出缓存的统计信息
System.out.println(cache.stats().toString());
// 获取缓存
String test = cache.getIfPresent("test");
// 再次查看缓存的状态(缓存状态只有在get操作后才会更新)
System.out.println(cache.stats().toString());
Assert.assertEquals("test", test);
// 等待60秒,让缓存过期
TimeUnit.SECONDS.sleep(60);
// 还可以使用invalidate方法手动移除缓存
// cache.invalidate("test");
test = cache.getIfPresent("test");
System.out.println(test);
// 查看缓存的状态
System.out.println(cache.stats().toString());
Assert.assertNull(test);
}
自动加载缓存
LoadingCache
LoadingCache
是Cache
的一个子接口,该接口提供了一个load
方法,通过这个方法可以提供一个加载缓存的方式,比如从数据库获取数据等。
public class DemoCacheLoader extends CacheLoader<String, Object> {
@Override
public Object load(@Nonnull String key) throws Exception {
// 从数据库加载数据,这里模拟从数据库加载数据
return this.loadData().get(key);
}
private Map<String, Object> loadData() {
Map<String, Object> map = new LinkedHashMap<>();
List<String> list = new LinkedList<>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
list.add("666");
list.add("777");
map.put("num", list);
map.put("test", "test");
map.put("name", "Bennett");
map.put("age", 24);
map.put("sex", "man");
return map;
}
}
@Test
public void cacheLoaderTest() throws ExecutionException, InterruptedException {
// 默认监听器是一个同步操作,可以使用下面的代码将其装饰成异步操作
RemovalListener<Object, Object> listener = RemovalListeners.asynchronous(notification -> {
System.out.printf("%s : %s 被移除了\n", notification.getKey(), notification.getValue());
}, Executors.newSingleThreadExecutor());
LoadingCache<String, Object> cacheLoader = CacheBuilder.newBuilder()
// 设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
// 设置缓存容器的初始容量为10
.initialCapacity(10)
// 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 设置n秒未访问就过期
// .expireAfterAccess(60, TimeUnit.SECONDS)
// 写入数据后n秒过期,只阻塞当前数据加载线程,其他线程返回旧值
// .refreshAfterWrite(60, TimeUnit.SECONDS)
// 设置移除监听器
.removalListener(listener)
// build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new DemoCacheLoader());
String test = (String) cacheLoader.get("test");
System.out.println(test);
System.out.println(cacheLoader.stats().toString());
TimeUnit.SECONDS.sleep(60);
// 又通过load方法加载了一遍,所以会觉得好像没过期一样
test = (String) cacheLoader.get("test");
System.out.println(cacheLoader.stats().toString());
System.out.println(test);
}
使用缓存默认值
@Test
public void defaultCacheTest() throws ExecutionException, InterruptedException {
Cache<String, Object> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(60, TimeUnit.SECONDS)
.removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
.build();
String val = (String) cache.get("key1", () -> {
System.out.println(Thread.currentThread().getName());
return "default_value";
});
System.out.println(val);
}
Cache的更多方法
@Test
public void moreCacheTest() throws ExecutionException, InterruptedException {
Cache<String, Object> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 设置key,value的弱引用/软引用
.weakKeys()
// .weakValues()
.softValues()
.removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
.build();
String test = new String("test");
cache.put("test", test);
test = new String("11111");
System.gc();
// 弱引用则输出null,软引用值会依旧存在(软引用只有jvm内存不足时才会将其回收)
System.out.println(cache.getIfPresent("test"));
// 手动移除缓存
cache.invalidate("key");
String[] keys = new String[]{"111", "222"};
// 移除部分/全部缓存
cache.invalidateAll(Arrays.asList(keys));
cache.invalidateAll();
}
缓存的移除时机
redis
中有key的懒惰移除,即不使用get
操作就不会执行移除操作。而Guava Cache
中也是类似的,缓存在未执行get
操作时并不会去将缓存移除,可以通过下面的一个demo来观察效果。
@Test
public void cacheRemoveTest() throws InterruptedException {
Cache<String, Object> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(5, TimeUnit.SECONDS)
.removalListener(notification -> System.out.printf("%s : %s 被移除了", notification.getKey(), notification.getValue()))
.build();
cache.put("testkey", "testval");
new Thread(()->{
while (true) {
System.out.println(LocalTime.now() + ", size: " + cache.size());
// 每隔一秒获取一次缓存大小
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ignored) {
}
}
}).start();
// 睡眠2分钟(缓存过期时间为5秒,为了保证确实应该过期,设置等待时长为2分钟,如果测试时觉得时间太长可以调短一点)
try {
TimeUnit.MINUTES.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==============分隔符==============");
cache.put("hhh", "aaaa");
System.out.println(cache.size());
// 睡眠2分钟(保证第二个key也过期)
try {
TimeUnit.MINUTES.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------------------分隔符--------------");
System.out.println(cache.size());
System.out.println("*****************分隔符***************");
System.out.println("hhh:" + cache.getIfPresent("hhh") + ", cache size: " + cache.size());
}
下面是输出的部分数据,重复的无意义数据没有全部放出来。
22:57:29.142, size: 1
22:57:30.148, size: 1
22:57:31.150, size: 1
22:57:32.151, size: 1
...
==============分隔符==============
2
22:59:29.487, size: 2
22:59:30.489, size: 2
23:01:27.819, size: 2
23:01:28.823, size: 2
...
------------------分隔符--------------
2
*****************分隔符***************
hhh:null, cache size: 1
从上面输出的信息来看,确实是如果不进行get
操作就不会移除缓存,从放入第一个key后缓存大小为1,然后在2分钟后(此时第一个key已过期),放入第二个key,此时缓存大小是2。这说明2分钟后缓存依旧未移除,4分钟后第二个key的缓存也过期后,再次查看缓存大小时,缓存大小依旧2,直至get
操作第二个key后,缓存大小才变为1。这说明确实是只有get
操作后才会进行缓存的移除,与redis
的懒惰移除是一样的,Guava Cache
并未另起线程去检测缓存是否已过期,并在过期后立马将其移除。
参考文章
作者:rickiyang
作者:poype
作者:零壹技术栈
如果文章中有错误的,欢迎在评论区留言指正,谢谢!