Guava Cache 简介
Google Guava Cache 是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用 LRU 算法,基于引用回收很好的利用了 Java 虚拟机的垃圾回收机制。
Guava Cache 与 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。Guava Cache 为了限制内存占用,通常都设定为自动回收元素。
Guava Cache 加载
加载方式1 - CacheLoader
LoadingCache 是附带 CacheLoader 构建而成的缓存实现。创建自己的 CacheLoader 通常只需要简单地实现 V load(K key) throws Exception 方法。
加载方式2 - Callable
所有类型的Guava Cache,不管有没有自动加载功能,都支持 get(K, Callable<V>) 方法。这个方法返回缓存中相应的值,或者用给定的 Callable 运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
Guava Cache 缓存回收
回收方式1 - 基于容量回收
maximumSize(long):当缓存中的元素数量超过指定值时。
回收方式2 - 定时回收
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。
回收方式3 - 基于引用回收(Reference-based Eviction)
CacheBuilder.weakKeys():使用弱引用存储键。当key没有其它引用时,缓存项可以被垃圾回收。
CacheBuilder.weakValues():使用弱引用存储值。当value没有其它引用时,缓存项可以被垃圾回收。
CacheBuilder.softValues():使用软引用存储值,按照全局最近最少使用的顺序回收。
Guava Cache 显示清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
1、个别清除:Cache.invalidate(key)
2、批量清除:Cache.invalidateAll(keys)
3、清除所有缓存项:Cache.invalidateAll()
代码示例
package com.study.guava;
import com.google.common.cache.*;
import java.util.concurrent.TimeUnit;
/**
* 加载方式1:CacheLoader
* 1.设置缓存容量
* 2.设置超时时间
* 3.提供移除监听器
* 4.提供缓存加载器
* 5.构建缓存
*
* @Version 1.0
*/
public class GuavaCacheDemo1 {
public static void main(String[] args) {
// 提供缓存加载器
CacheLoader<String, String> loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
if("key".equals(key)){
return null;
}
System.out.println(key + " is loaded from a cacheLoader!");
return key + "'s value";
}
};
RemovalListener<String, String> removalListener = new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> removal) {
System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");
}
};
LoadingCache<String, String> testCache = CacheBuilder.newBuilder()
// 设置缓存容量
.maximumSize(5)
// 提供移除监听器
.removalListener(removalListener)
// 提供缓存加载器 loader;构建缓存
.build(loader);
// 由于缓存的容量只设置了5个,存入10个就会由guava基于容量回收掉5个
for (int i = 0; i < 10; i++) {
String key = "key" + i;
String value = "value" + i;
testCache.put(key, value);
System.out.println("[" + key + ":" + value + "] is put into cache!");
}
// 如果存在就获取 不存在返回null
System.out.println(testCache.getIfPresent("key6"));
try {
// 不存在的key,会报错
System.out.println(testCache.get("key"));
} catch (Exception e) {
e.printStackTrace();
System.out.println("不存在的key,会报错");
}
}
}
package com.study.guava;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 加载方式2:Callable
* 所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable<V>)方法。
* 这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。
* 在整个加载方法完成前,缓存项相关的可观察状态都不会更改。
* 这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
*
* @Version 1.0
*/
public class GuavaCacheDemo2 {
static Cache<String, String> testCache = CacheBuilder.newBuilder()
// 设置超时时间
.expireAfterWrite(3, TimeUnit.SECONDS)
.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
public static void main(String[] args) throws InterruptedException {
testCache.put("key1", "我是存在的");
try {
// 获取key为key2的缓存数据,如果有就返回,没有就返回call方法的返回值
System.out.println(testCache.get("key2", new Callable<String>() {
@Override
public String call() throws Exception {
return "运算、缓存、然后返回";
}
}));
// 获取key为key1的缓存数据,如果有就返回,没有就返回call方法的返回值。注意这里key是存在的
System.out.println(testCache.get("key1", new Callable<String>() {
@Override
public String call() throws Exception {
return "我是打酱油的";
}
}));
} catch (ExecutionException e) {
e.printStackTrace();
}
Thread.sleep(4000);
System.out.println(testCache.getIfPresent("key1"));
}
}
package com.study.guava;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.ExecutionException;
/**
* 统计
* @Version 1.0
*/
public class GuavaCacheDemo3 {
static Cache<String, Object> testCache = CacheBuilder.newBuilder()
// 当值没有其它(强或软)引用时,缓存项可以被垃圾回收
.weakValues()
// 开启Guava Cache的统计功能
.recordStats()
.build();
public static void main(String[] args) throws ExecutionException {
Object obj1 = new Object();
testCache.put("key1", obj1);
obj1 = new String("123");
System.out.println(testCache.getIfPresent("key1"));
System.out.println(testCache.get("key2",() -> "key2 value"));
// 主动gc
System.gc();
System.out.println(testCache.getIfPresent("key1"));
System.out.println(testCache.stats());
}
}
Ehcache
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
主要特性
1、快速、简单、支持多种缓存策略
2、支持内存和磁盘缓存数据,因此无需担心容量问题
3、缓存数据会在虚拟机重启的过程中写入磁盘
4、可以通过RMI、可插入API等方式进行分布式缓存(比较弱)
5、具有缓存和缓存管理器的侦听接口
6、支持多缓存管理器实例,以及一个实例的多个缓存区域
7、提供Hibernate的缓存实现
架构图
适用场景
1、单个应用或者对缓存访问要求很高的应用
2、简单的共享可以,但是不适合涉及缓存恢复、大数据缓存
3、大型系统,存在缓存共享、分布式部署、缓存内容大不适合使用
4、在实际工作中,更多是将Ehcache作为与Redis配合的二级缓存
代码示例
package com.study.ehcache.dao;
import com.study.ehcache.bean.Student;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;
/**
* Spring对缓存的支持类似于对事务的支持。
* 首先使用注解标记方法,相当于定义了切点,然后使用Aop技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。
*
* @Param
* @return
**/
@Repository
public class StudentDaoImpl implements StudentDao {
/**
* 表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。
* 这个注解可以用condition属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。
* 可以使用key属性来指定key的生成规则。
*
* 参数:
* value:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name, 指明将值缓存到哪个Cache中
* key:缓存的key,默认为空,既表示使用方法的参数类型及参数值作为key,支持SpEL,如果要引用参数值使用井号加参数名,如:#userId,
* 一般来说,我们的更新操作只需要刷新缓存中某一个值,所以定义缓存的key值的方式就很重要,最好是能够唯一,因为这样可以准确的清除掉特定的缓存,而不会影响到其它缓存值 ,
* 本例子中使用实体加冒号再加ID组合成键的名称,如"user:1"、"order:223123"等
* condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持SpEL
*
**/
@Cacheable(value = "simpleCache", key = "#stuid")
@Override
public Student testEhcache(String stuid) {
// 模拟假数据
System.out.println("开始访问数据库");
Student sdf = new Student();
sdf.setStuid(stuid);
return sdf;
}
/**
* 与@Cacheable不同,@CachePut不仅会缓存方法的结果,还会执行方法的代码段。它支持的属性和用法都与@Cacheable一致。一个缓存后就不执行代码了,一个还要执行)
**/
@Override
@CachePut(value = "simpleCache", key = "#stuid")
public Student testEhcache2(String stuid) {
// 模拟假数据
System.out.println("@CachePut不仅会缓存方法的结果,还会执行方法的代码段。");
Student sdf = new Student();
sdf.setStuid(stuid);
return sdf;
}
/**
* 与@Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。
*
* 参数:
* value:缓存位置名称,不能为空,同上
* key:缓存的key,默认为空,同上
* condition:触发条件,只有满足条件的情况才会清除缓存,默认为空,支持SpEL
* allEntries:true表示清除value中的全部缓存,默认为false
*/
// @CacheEvict(value = "simpleCache", allEntries=true)
// 清除掉UserCache中某个指定key的缓存
@Override
@CacheEvict(value = "simpleCache", key = "#stuid")
public Student testEhcache3(String stuid) {
// 模拟假数据
System.out.println("@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。");
Student sdf = new Student();
sdf.setStuid(stuid);
return sdf;
}
}
caffeine
Caffeine 是 Google 基于 Java8 对 GuavaCache 的重写升级版本,支持丰富的缓存过期策略,尤其是 TinyLfu 淘汰算法,提供了一个近乎最佳的命中率。从性能上(读、写、读/写)也足以秒杀其他一堆进程内缓存框架。Spring5 更是直接放弃了使用了多年的 Guava,而采用了 Caffeine。
Caffeine的 API 的操作功能和 Guava 是基本保持一致的,并且 Caffeine 为了兼容之前是Guava 的用户,做了一个 Guava 的 Adapter 给大家使用也是十分的贴心。
Caffeine 是一个非常不错的缓存框架,无论是在性能方面,还是在 API 方面,都要比 Guava cache 要优秀一些。如果在新的项目中要使用 local cache 的话,可以优先考虑使用Caffeine。对于老的项目,如果使用了 Guava cache,想要升级为 Caffeine 的话,可以使用 Caffeine 提供的 Guava cache 适配器,方便的进行切换。
代码示例
package com.study.caffeine.eviction;
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @Description 基于时间
* @Version 1.0
*/
public class TimeBaseTest1 {
public static void main(String[] args) throws Exception {
LoadingCache<Object, Object> cache = Caffeine.newBuilder()
//基于时间失效->写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
//or 基于时间失效->访问之后开始计时失效
//.expireAfterAccess(10, TimeUnit.SECONDS)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(Object k, Object v, RemovalCause removalCause) {
System.out.println("缓存失效了 removed " + k + " cause " + removalCause.toString());
}
})
//同步加载和手动加载的区别就是在构建缓存时提供一个同步的加载方法
.build(new CacheLoader<Object, Object>() {
//单个 key 的值加载
@Override
public Object load(Object key) throws Exception {
System.out.println("---exec load---");
return key + "_" + System.currentTimeMillis();
}
});
//放入缓存
cache.put("k1", "v1");
//准备失效
Thread.sleep(2001);
System.out.println("sleep done");
System.out.println("开始取失效的缓存");
Object v1 = cache.get("k1");
System.out.println("新值 " + v1);
}
}
package com.study.caffeine.eviction;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
/**
* @Description 基于大小-缓存大小
* @Version 1.0
*/
public class SizeBaseTest1 {
public static void main(String[] args) {
Cache<Object, Object> cache = Caffeine.newBuilder()
//缓存最大条数,超过这个条数就是驱逐缓存
.maximumSize(20)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(Object k, Object v, RemovalCause removalCause) {
System.out.println("removed " + k + " cause " + removalCause.toString());
}
})
.build();
for (int i = 0; i < 25; i++) {
cache.put(i, i + "_value");
}
cache.cleanUp();
}
}
package com.study.caffeine.eviction;
import com.github.benmanes.caffeine.cache.*;
import com.sun.corba.se.impl.orbutil.graph.Graph;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @Description 基于引用;Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
* @Version 1.0
*/
public class ReferenceBaseTest1 {
public static void main(String[] args) throws Exception {
// Evict when neither the key nor value are strongly reachable
// 当key和value都没有引用时驱逐缓存
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
// 使用弱引用存储key
.weakKeys()
// 使用弱引用存储value
.weakValues()
// 开启统计功能
.recordStats()
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(Object k, Object v, RemovalCause removalCause) {
System.out.println("cache1 removed " + k + " cause " + removalCause.toString());
}
})
.build(key -> createTestValue(key));
// Evict when the garbage collector needs to free memory
// 当垃圾收集器需要释放内存时驱逐
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
// 使用软引用存储value
.softValues()
// 开启统计功能
.recordStats()
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(Object k, Object v, RemovalCause removalCause) {
System.out.println("cache2 removed " + k + " cause " + removalCause.toString());
}
})
.build(key -> createTestValue(key));
Object obj1 = new Object();
Object obj2 = new Object();
cache1.put("1234", obj1);
cache2.put("1234", obj2);
obj1 = new String("123");
obj2 = new String("123");
// 主动gc
System.gc();
System.out.println(cache1.getIfPresent("1234"));
System.out.println(cache2.getIfPresent("1234"));
System.out.println(cache1.stats());
System.out.println(cache2.stats());
}
private static Object createTestValue(String key) {
return key + "_" + System.currentTimeMillis();
}
}
内存缓存对比