目录
本地缓存
回顾
Guava Cache介绍
Guava Cache使用
创建
删除
Guava Cache底层实现
本地缓存与分布式缓存对比
缓存三大问题
实现:CurrentHashMap、Guava Cache
缓存在应用服务器,全局变量,JVM缓存
JVM内存
进程与线程的区别
一个最简单的本地缓存,就是使用List、Map等对象实例,会存储在Java堆上,也可以理解为JVM缓存。
Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制。
功能实现
本地缓存的应用场景
com.google.common.cache.LoadingCache
引入包:
com.google.guava
guava
26.0-jre
存储结构,底层实现类似于ConcurrentHashMap
class LocalCacheextends AbstractMap implements ConcurrentMap
创建cache对象时,采用CacheLoader来获取数据,当缓存不存在时能够自动加载数据到缓存中。
public class Main {
public static final Map TEST_DATA_MAP = Maps.newHashMap();
static {
TEST_DATA_MAP.put(1, "张三");
TEST_DATA_MAP.put(2, "里斯");
TEST_DATA_MAP.put(3, "王五");
TEST_DATA_MAP.put(4, "赵六");
}
public static void main(String[] args) {
LoadingCache cache = CacheBuilder.newBuilder()
//缓存存储最大数量
.maximumSize(3)
//访问过期时间3s
.expireAfterAccess(3, TimeUnit.SECONDS)
.build(new CacheLoader() {
@Override
public String load(Integer key) throws Exception {
//当缓存不存在时能够自动加载数据到缓存中
return TEST_DATA_MAP.get(key);
}
});
try {
System.out.println(cache.get(1));
} catch (Exception e) {
e.printStackTrace();
}
}
}
LoadingCache定义,CacheBuilder参数
maximumSize() | Specifies the maximum number of entries the cache may contain. 最大缓存上限 |
expireAfterWrite() | 写过期。在put或者load的时候更新缓存的时间戳,在get过程中去判断当前时间与时间戳的差值,若大于过期时间,就会进行load操作 |
expireAfterAccess() | 读写过期。写/读都会更新新的时间戳,所以不会很快导致缓存过期,所以当读的时候,会和最新的时间戳进行对比,最新的时间戳可能是因为写或者读而更改 |
refreshAfterWrite() | 是指在创建缓存后,如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,默认同步去刷新缓存,如果新的缓存值还没有load到时,则会先返回旧值。 |
LoadingCache操作方法
get(K) | 去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会抛出异常 |
getIfPresent(key) getAllPresent(keys) |
去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会返回null,不会抛出异常。 |
put(key, value) | 显式写入缓存,如果原来缓存里面已经存在则会覆盖原有的值 |
invalidate(key) | 清除单个 |
invalidateAll(keys) | 批量清除 |
invalidateAll() | 清除所有缓存 |
asMap() | 返回ConcurrentMap视图 |
主动删除,见操作方法,删除单个、批量删除、删除所有
被动删除
删除监控
public static void main(String[] args) {
LoadingCache cache = CacheBuilder.newBuilder()
//缓存存储最大数量
.maximumSize(3)
//访问过期时间3s
.expireAfterAccess(3, TimeUnit.SECONDS)
//监听删除
.removalListener(notification -> System.out.println("删除监听:" + notification.getKey() + "=" + notification.getCause()))
.build(new CacheLoader() {
@Override
public String load(Integer key) throws Exception {
//当缓存不存在时能够自动加载数据到缓存中
return TEST_DATA_MAP.get(key);
}
});
try {
cache.get(1);
Thread.sleep(3000);
printAll(cache);
cache.get(1);
cache.get(2);
cache.get(3);
cache.get(4);
cache.invalidate(3);
printAll(cache);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 输出
* @param cache
*/
public static void printAll(LoadingCache cache){
System.out.println("\n输出全部");
Iterator iterator = cache.asMap().entrySet().iterator();
while (iterator.hasNext()){
System.out.println(iterator.next().toString());
}
}
执行结果
可见,将1写入缓存后,线程睡眠3秒,监控到了因为访问超时的删除,type=EXPIRED
然后写入4个元素到最大个数为3的缓存,根据LRU+FIFO,监控到了元素1因为超过最大个数的删除,type=SIZE
最后,手动删除元素3,监控了主动删除,type=EXPLICIT
回收策略
常用的被动删除方式:1、基于size回收;2、基于过期时间
1、基于size回收,触发回收是在缓存项达到了maxsize后,继续添加缓存项时,会根据LRU+FIFO策略回收缓存项保证不超过maxsize
2、基于过期时间回收,Guava Cache不会专门维护一个线程来回收这些过期的缓存项,是在每次进行缓存操作的时候惰性删除,如get()或者put()的时候,判断缓存是否过期
体系类图
LocalCache为Guava Cache的核心类,实现与ConcurrentHashMap相似,核心是一个Segement数组,也引入了段的概念
final Segment[] segments;
static class Segment extends ReentrantLock {//...}
与之不同的是,LocalCache的Segement由一个table和5个队列组成
get源码剖析
com.google.common.cache.LocalCache.Segment#get(K, int, com.google.common.cache.CacheLoader super K,V>)
@Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
V getOrLoad(K key) throws ExecutionException {
return get(key, defaultLoader);
}
V get(K key, CacheLoader super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
return segmentFor(hash).get(key, hash, loader);
}
V get(K key, int hash, CacheLoader super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
// 获取存储的kv对象
ReferenceEntry e = getEntry(key, hash);
if (e != null) {
// 对应的entry不为null,证明值还在
// 获取当前的时间,判断是否过期
long now = map.ticker.read();
// 判断是否为alive(此处是懒失效,在每次get时才检查是否达到失效时机)
V value = getLiveValue(e, now);
if (value != null) {
// 元素是alive的,更新元素访问时间
recordRead(e, now);
// 记录缓存命中
statsCounter.recordHits(1);
// 如果设置refresh,则异步刷新查询value,然后等待返回最新value
// 否则 返回旧value
return scheduleRefresh(e, key, hash, value, now, loader);
}
//元素不是alive的,但是在loading的,等待loading完成(阻塞等待)。
ValueReference valueReference = e.getValueReference();
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
// value还没有拿到,则查询loader方法获取对应的值(阻塞获取)。
return lockedGetOrLoad(key, hash, loader);
} //...
}
V getLiveValue(ReferenceEntry entry, long now) {
// key是否存在,不存在则尝试回收
if (entry.getKey() == null) {
tryDrainReferenceQueues();
return null;
}
// value是否存在,不存在则尝试回收
V value = entry.getValueReference().get();
if (value == null) {
tryDrainReferenceQueues();
return null;
}
// 元素是否过期,过期则尝试回收
if (map.isExpired(entry, now)) {
tryExpireEntries(now);
return null;
}
return value;
}
缓存命中记录,怎么获取
本地缓存
优点:应用和cache是在同一进程,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;
缺点:容量小,每个JVM有一份,有数据冗余,因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
分布式缓存
优点:空间优势、高可用(主从)、高扩展(分区)、集群,自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
缺点:资源、网络开销,因为自身是一个独立的应用,本地节点都需要与其进行通信,导致依赖网络,同时如果缓存服务崩溃可能会影响所有依赖节点
1、缓存穿透(缓存中查不到)
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。
refreshAfterWrite:只阻塞加载数据的线程,其余线程返回旧数据
如果缓存过期,恰好有多个线程读取同一个key的值,那么guava只允许一个线程去加载数据,其余线程阻塞。这虽然可以防止大量请求穿透缓存,但是效率低下。使用refreshAfterWrite可以做到:只阻塞加载数据的线程,其余线程返回旧数据。(注:如果没有旧数据,那么其余线程会阻塞)
refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。可以去实现CacheLoader.reload()完成异步刷新
2、缓存雪崩(集中失效)
数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
3、缓存击穿(一个key的请求量太大,缓存过期)
指一个key非常热点,大并发集中对这个key进行访问,当这个key在失效的瞬间,仍然持续的大并发访问就穿破缓存,转而直接请求数据库。
在缓存失效前指定让缓存刷新
guava cache提供了重新刷新与重新加载的方法,为防止缓存击穿,我们可以在缓存失效前指定让缓存刷新
定义一个本地缓存,同时设置reload与refresh机制,注:refreshAfterWrite的时间设置需要小于expireAfterWrite的时间
private static final LoadingCache numberCache = CacheBuilder.newBuilder()
.maximumSize(10)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(8, TimeUnit.SECONDS)
.build(new CacheLoader() {
@Override
public String load(Integer key) throws Exception {
return key + "数字测试";
}
});