前言
本次将介绍常用的内存缓存框架,主要围绕以下两点
- 常见开源的内存缓存框架介绍及使用
- 常见开源的内存缓存框架对比
常用的内存缓存框架
-
Guava Cache
-
Ehcache
-
Caffeine
Guava Cache
Google Guava Cache是一种非常优秀的本地缓存解决方案,提供了基于容量、时间、引用的缓存回收方式
内部实现采用LRU算法,
基于引用回收
很好的利用了java虚拟机的垃圾回收机制
Guava Cache和ConcurrentHashMap很相似,但也不完全一样.最基本的区别是ConcurrentHashMap会一直保存所有添加的元素,
直至显示地移除.Guava Cache为了限制内存占用,通常都
设定为自动回收元素
使用场景
-
愿意消耗一些内存空间来提升速度
-
预料到某些键会被多次查询
-
缓存中存放的数据总量不会超出内存容量
注意事项
Guava Cache是运行在JVM的本地缓存,并不能把数据存放到外部服务器上
如果有这样的要求,可以使用Memcached或Redis这样的分布式缓存
使用
加载方式
Guava Cache主要有两种加载方式,下面将通过代码演示.详细说明两种加载方式的使用
- CacheLoader
- Callable
CacheLoader
loadingCache是附带CacheLoader构建而成的缓存实现.创建自己的CacheLoader通常只需要简单实现V load(K key)方法
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @author 昊天锤
* @date 2020/8/10 0010 15:06
* @description GuavaCache缓存使用缓存加载器示例
*/
public class GuavaCacheByLoaderDemo {
/**
* 模拟数据库数据
*/
private static List<Map<String, String>> dataBase = new ArrayList<>();
static {
for (int i = 1; i <= 10; i++) {
Map<String, String> map = new HashMap<>();
map.put(String.valueOf(i), "张三" + i);
dataBase.add(map);
}
}
public static void main(String[] args) throws ExecutionException {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
// 设置缓存容量
.maximumSize(5)
// 设置超时时间
.expireAfterWrite(10, TimeUnit.MINUTES)
// 提供移除监听器
.removalListener(removalListener())
// 提供缓存加载器
.build(cacheLoader());
// 使用缓存插入数据
// 由于缓存的容量只设置了5个,存入6个就会由guava基于容量回收掉1个
for (int i = 1; i <= 6; i++) {
cache.put(String.valueOf(i), "张三" + i);
}
// 刻意获取一个不存在缓存中的key,让它去调用缓存加载器加载数据到缓存中
System.out.println(cache.get("10"));
}
/**
* 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
*/
private static CacheLoader<String, String> cacheLoader() {
return new CacheLoader<String, String>() {
@Override
public String load(String id) {
System.out.println("调用缓存加载器加载缓存:key = " + id);
for (Map<String, String> map : dataBase) {
String name = map.get(id);
if (Objects.nonNull(name)) {
return name;
}
}
return null;
}
};
}
/**
* 移除监听器: 当key被移除时调用该方法
*/
private static RemovalListener<String, String> removalListener() {
return removalNotification -> {
String key = removalNotification.getKey();
String value = removalNotification.getValue();
System.out.println("监听到移除操作:key = " + key + ",value = " + value);
};
}
}
注意两个点:
如果超过了容量大小便会移除key,并调用removalListener的removalNotification方法
如果get一个不存在缓存的key便会调用cacheLoader的load方法进行运算、缓存、返回
Callable
所有类型的guava cache,不管有没有自动加载功能,都支持get(K ,Callable)方法.这个方法返回缓存中相应的值
或者用给定的Callable运算并把结果加入缓存中.在整个加载方法完成前,缓存项相关的可观察状态都不会更改
这个方法简便的地实现了-如果有缓存返回.否则运算、缓存、再返回
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* @author 昊天锤
* @date 2020/8/10 0010 15:41
* @description GuavaCache缓存使用Callable示例
*/
public class GuavaCacheByCallableDemo {
/**
* 缓存
*/
private static Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(5).build();
/**
* 模拟数据库数据
*/
private static List<Map<String, String>> dataBase = new ArrayList<>();
static {
for (int i = 1; i <= 10; i++) {
Map<String, String> map = new HashMap<>();
map.put(String.valueOf(i), "张三" + i);
dataBase.add(map);
}
}
public static void main(String[] args) throws ExecutionException {
// 此时缓存中并没有任何数据,所有现在加载啥都需要运算、缓存、再返回
String key = "1";
System.out.println("加载一个不存在的key = " + getByCallable(key));
// 该key之前已经加载过并缓存起来了,所以本次获取并不需要运算.可以直接从缓存中读取
System.out.println("加载一个已存在的key = " + getByCallable(key));
}
/**
* 如果有缓存返回.否则运算、缓存、再返回
*
* @param key 需要查找额key
* @return key对应的值
*/
private static String getByCallable(String key) throws ExecutionException {
return cache.get(key, () -> {
System.out.println("运算加载 key = " + key);
for (Map<String, String> map : dataBase) {
String name = map.get(key);
if (!StringUtils.isEmpty(name)) {
return name;
}
}
return null;
});
}
}
注意一个点: 运算加载的过程是:运算、缓存、再返回.所以第一次获取时进行运算 第二次获取时直接取缓存中的数据
缓存回收
Guava Cache支持三种缓存回收策略
容量回收
maximumSize(long ) 当缓存中的元素数量超过指定值时就会进行回收
CacheBuilder
.newBuilder()
// 指定容量大小
.maximumSize(5)
.build();
定时回收
expireAfterWrite(long,TimeUnit) 缓存项在给定时间内没有被写入则回收
expireAfterAccess(long,TimeUnit) 缓存项在给定时间内没有被访问则回收
CacheBuilder
.newBuilder()
// 十分钟之内没有被写入,则回收
.expireAfterWrite(10, TimeUnit.MINUTES)
// 十分钟之内没有被访问,则回收
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
引用回收 (Reference-based Eviction)
CacheBuilder.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收
CacheBuilder
.newBuilder()
// 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
.weakKeys()
// 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
.weakValues()
// 使用软引用存储值.按照全局最近最少使用的顺序回收
.softValues()
.build();
显示清除
任何时候都可以显示地清除缓存项,而不是等到它被回收
Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(5).build();
// 单个清除
cache.invalidate("key");
// 批量清除
cache.invalidateAll(Arrays.asList("key", "key1"));
// 清空
cache.invalidateAll();
由于GuavaCache它是存在就读取不存在就加载
所以在修改key的时候可以显示清除缓存,当这个key在下一次被读取的时候就会去加载数据源数据
这样就最大程度的保证了数据源数据和缓存数据是一致的
统计
GuavaCache还提供了缓存统计功能可以调用recordStats来开启统计功能
// 用来开启统计功能
Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(5).recordStats().build();
然后可以调用cache.stats()方法会返回一个CacheStats对象,该对象提供如下统计信息
CacheStats stats = cache.stats();
// 缓存命中率
stats.hitRate();
// 加载新值的平均时间,单位为纳秒
stats.averageLoadPenalty();
// 缓存项被回收的总数,不包括显示清除
stats.evictionCount();
Ehcache
Ehcache是一个纯java进程内存缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider
主要特性
- 快速、简单、支持多种缓存策略
- 支持内存和磁盘缓存数据,因为无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过RMI、可插入API等方式进行分布式缓存(比较弱)
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理实例,以及一个实例的多个缓存区域
- 提供Hibernate的缓存实现
Ehcache架构图
从这个图中可以看到CacheManager和CacheManager Listener SPI的扩展
同时包含了Cache核心、存储、磁盘存储还支持多种缓存淘汰策略:LRU、LFU、FIFO
适用场景
- 单个应用或者对缓存访问要求很高的应用
- 简单的共享可以,但是不合适涉及缓存恢复、大数据缓存
- 如果系统比较大型,存在缓存共享,分布式部署,缓存内容大这几点便不合适使用该缓存
- 在实际工作中,更多是将Ehcache作为与Redis配合的二级缓存
使用
ehcache.xml
准备ehcache.xml文件,存放在resources目录下,如图所示
ehcache.xml内容如下
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!-- 指定一个文件目录,当EhCache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
<diskStore path="java.io.tmpdir"/>
<!--
cache元素的属性:
name:缓存名称
maxElementsInMemory:内存中最大缓存对象数
maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
diskPersistent:是否缓存虚拟机重启期数据
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
-->
<!-- 设定缓存的默认数据过期策略 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="20"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
<cache name="simpleCache"
maxElementsInMemory="1000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="20"/>
</ehcache>
文件注释有对设置缓存参数的详细说明:最大缓存对象数、磁盘缓存区大小等等
Ehcache使用示例
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
/**
* @author 昊天锤
* @date 2020/8/10 0010 17:00
* @description Ehcache使用示例
*/
public class EhcacheDemo {
public static void main(String[] args) {
// 初始化CacheManager对象
CacheManager cacheManager = new CacheManager();
// 加载自定义cache对象
Cache cache = cacheManager.getCache("simpleCache");
// 把集合放入缓存 存放键值对集合 类似map
cache.put(new Element("user", "张三"));
// 取出集合根据key获取值
System.out.println("key=user,value=" + cache.get("user").getObjectValue());
// 更新缓存的值
cache.put(new Element("user", "李四"));
System.out.println("key=user,value=" + cache.get("user").getObjectValue());
// 获取缓存中的元素个数
System.out.println("集合个数:" + cache.getSize());
// 移除cache某个值
cache.remove("user");
System.out.println("集合个数:" + cache.getSize());
// 关闭当前cacheManager对象
cacheManager.shutdown();
}
}
存入、取出、删除等方法使用都很简单,对于缓存参数配置主要在ehcache.xml里面.这样ehcache的简单使用便就完成了
Ehcache缓存集群
对!刚刚还提到Ehcache支持分布式缓存集群,其实也是在xml中加入配置,如下
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!--EHCache分布式缓存集群环境配置-->
<!--rmi手动配置-->
<cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>
</ehcache>
caffeine
caffeine是Google基于java8对Guava Cache的重写升级版本,支持丰富的缓存过期策略,尤其是
TinyLfu淘汰算法
,提供了一个几乎最佳的命中率
从性能上(读、写、读/写)也足以秒杀其他一堆进程内缓存框架.Spring5更是直接放弃了使用多年的Guava而采用caffeine
下面是caffeine官方读写性能测试报告
caffeine的API操作功能和Guava 是基本保持一致的
并且caffeine为了兼容之前使用Guava 的用户,做了一个Guava的Adapter给大家使用.这一点也是十分的贴心
caffeine是一个
非常不错的缓存框架
,无论在
性能方面还是API方面都要比Guava Cache要优秀一些
如果在新的项目中要使用local cache的话,可以
优先考虑使用caffeine
对于老项目,如果使用了Guava Cache,想要升级为caffeine的话,可以使用caffeine提供的Guava Cache适配器方便的进行切换
使用
caffeine使用和Guava Cache非常相似
加载方式
caffeine主要有3种加载方式,下面将通过代码演示.详细说明三种加载方式的使用
- 手动加载
- 同步加载
- 异步加载
手动加载Cache
手动加载的方式也还是在get方法的基础上,加上一个function运算函数.还是那句话,缓存中找不到既运算、缓存、再返回
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 手动加载
*/
public class CaffeineDemo {
public static void main(String[] args) throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
.build();
// 使用java8 Lambda表达式声明一个方法,get不到缓存中的值调用这个方法运算、缓存、返回
Function function = key -> key + "_" + System.currentTimeMillis();
String value = cache.get("key", function);
System.out.println(value);
//让缓存到期
Thread.sleep(2001);
// 存在就取,不存在就返回空
System.out.println(cache.getIfPresent("key"));
// 重新存值
cache.put("key", "hello world");
String key = cache.get("key", function);
System.out.println(key);
// 获取所有值打印出来
ConcurrentMap<String, String> concurrentMap = cache.asMap();
System.out.println(concurrentMap);
// 删除key
cache.invalidate("key");
// 获取所有值打印出来
System.out.println(cache.asMap());
}
}
同步加载LoadingCache
同步加载的方式是需要在build的时候指定运算缓存的CacheLoader并实现其load方法
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 同步加载
*/
public class CaffeineDemo {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
// 可以使用java8函数式接口的方式,这里其实是重写CacheLoader类的load方法
.build(key -> key + "_" + System.currentTimeMillis());
// 获取一个不存在的kay,让它去调用CacheLoader的load方法
System.out.println(cache.get("key"));
// 等待2秒让key失效
TimeUnit.SECONDS.sleep(2);
System.out.println(cache.getIfPresent("key"));
// 批量获取key,让他批量去加载
Map<String, String> all = cache.getAll(Arrays.asList("key1", "key2", "key3"));
System.out.println(all);
}
}
异步加载AsyncLoadingCache
异步加载的需要使用的build方式是buildAsync,同时需要指定运算缓存的CacheLoader并实现其load方法
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 异步加载
*/
public class CaffeineDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
AsyncLoadingCache<Object, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
// 可以使用java8函数式接口的方式,这里其实是重写CacheLoader的load方法
.buildAsync(key -> key + "_" + System.currentTimeMillis());
// 获取一个不存在的kay,让它异步去调用CacheLoader的load方法。这时候他会返回一个CompletableFuture
// 既:我已经帮你异步去运算key的值了,你什么时候要再什么时候调用CompletableFuture的get方法就好了
CompletableFuture<String> future = cache.get("key");
// 为了证明是异步调用,可以将时间戳打印出来和value中的时间戳比较.
future.thenAccept(s -> System.out.println("当前的时间为:" + System.currentTimeMillis() + " -> 异步加载的值为:" + s));
// 睡眠2秒让它的key失效
TimeUnit.SECONDS.sleep(2);
// 注意:当使用getIfPresent时,也是返回的CompletableFuture
// 因为getIfPresent从缓存中找不到是不会去运算key既不会调用(CacheLoader.load)方法
// 所以得到的CompletableFuture可能会为null,如果想从CompletableFuture中取值的话.先判断CompletableFuture是否会为null
CompletableFuture<String> completableFuture = cache.getIfPresent("key");
if (Objects.nonNull(completableFuture)) {
System.out.println(completableFuture.get());
}
}
}
可以从结果看到,调用运算key的时候是比当前时间要早.但是又没有阻塞当前线程,所以可以判定确实是异步加载
异步调用的好处就在于在高并发场景下会有显著的性能提升,有对这一块比较感兴趣的可以学习一下JUC包里面内容
驱逐策略
caffeine主要有三种缓存回收的方式,下面将通过代码演示说明三种加载方式的使用
容量大小驱逐
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 基于缓存容量大小驱逐缓存
*/
public class CaffeineDemo {
public static void main(String[] args) {
// 基于缓存容量大小
Cache<String, String> cache = Caffeine
.newBuilder()
.maximumSize(5)
.removalListener((RemovalListener) (k, v, removalCause) -> {
System.out.println("移除: key = " + k + ",cause = " + removalCause);
})
.build();
for (int i = 1; i <= 6; i++) {
cache.put(String.valueOf(i), "张三" + i);
}
cache.cleanUp();
}
}
由于我们设置的缓存大小是5个,可是缓存了6个,所以必然有一个要被回收
权重驱逐
Caffeine.weigher(Weigher) 函数来指定权重,Caffeine.maximumWeight(long) 函数来指定缓存最大权重值
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 基于权重驱逐
*/
public class CaffeineDemo {
public static void main(String[] args) {
// 基于权重驱逐
Cache<String, String> cache = Caffeine
.newBuilder()
.maximumWeight(15)
.weigher((k, v) -> Integer.valueOf((String) v))
.removalListener((RemovalListener) (k, v, removalCause) -> {
System.out.println("移除: key = " + k + ",cause = " + removalCause);
})
.build();
for (int i = 10; i <= 15; i++) {
cache.put(String.valueOf(i), String.valueOf(i));
}
cache.cleanUp();
}
}
权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐
注意:权重驱逐和容量大小驱逐两种方式不能同时使用
时间驱逐
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author 昊天锤
* @date 2020/8/10 0010 20:17
* @description Caffeine 基于时间驱逐
*/
public class CaffeineDemo {
public static void main(String[] args) throws InterruptedException {
// 基于时间驱逐
Cache cache = Caffeine
.newBuilder()
.maximumSize(10)
// 写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 访问之后开始计时失效
.expireAfterAccess(2000, TimeUnit.MILLISECONDS)
// 自定义线程池执行RemovalListener
.executor(Executors.newSingleThreadExecutor())
.removalListener((RemovalListener) (k, v, removalCause) -> {
System.out.println("移除: key = " + k + ",cause = " + removalCause);
})
.build();
cache.put("key", "hello world");
System.out.println(cache.getIfPresent("key"));
TimeUnit.SECONDS.sleep(2);
System.out.println(cache.getIfPresent("key"));
}
}
总结
到这就已经介绍完常用的内存缓存框架了,最后在这里做一个比较
比较项 | ConcurrentMap | Ehcache | GuavaCache | caffeine |
---|---|---|---|---|
读写性能 | 很好,分段锁 | 好 | 好 | 很好 |
淘汰算法 | 无 | LRU、LFU、FIFO | LRU | W-TinyLFU |
功能丰富度 | 功能简单 | 功能丰富 | 功能丰富,支持刷新和虚引用 | 功能和GuavaCache很相似 |
工具大小 | jdk自带,很小 | 一般 | 较小 | 一般 |
是否支持持久化 | 否 | 是 | 否 | 否 |
是否支持集群 | 否 | 是 | 否 | 否 |
综合来说如果不考虑分布式集群缓存还有持久化的功能,就纯粹的内存缓存.
caffeine无疑是最好的选择
为了更好地掌握caffeine这个优秀的缓存框架,下一篇文章将详细讲解caffeine的实现原理(源码分析)