Guava Cache本地缓存
目录LoadingCache是什么?怎么使用?缓存过期机制Guava cache实现LRU回收机制缓存三大问题Guava cache与分布式缓存的区别?
目录
本地缓存
实现:CurrentHashMap、Guava Cache
缓存在应用服务器,全局变量,JVM缓存
回顾
JVM内存
- 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,线程共享
- Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
- Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。
- Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。
- 程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
进程与线程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
Guava Cache介绍
一个最简单的本地缓存,就是使用List、Map等对象实例,会存储在Java堆上,也可以理解为JVM缓存。
Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制。
功能实现
- 缓存过期和淘汰机制
- 并发处理能力
- 更新锁定:类似于分布式锁,作用体现于对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。
- 集成数据源:get可以集成数据源,在从缓存中读取不到时从数据源中读取数据并回填缓存
- 监控缓存加载/命中情况
本地缓存的应用场景
- 对性能有非常高的要求
- 不经常变化
- 占用内存不大
- 有访问整个集合的需求
- 数据允许不时时一致
Guava Cache使用
com.google.common.cache.LoadingCache
引入包:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
存储结构,底层实现类似于ConcurrentHashMap
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>
创建
创建cache对象时,采用CacheLoader来获取数据,当缓存不存在时能够自动加载数据到缓存中。
public class Main {
public static final Map<Integer, String> 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<Integer, String> cache = CacheBuilder.newBuilder()
//缓存存储最大数量
.maximumSize(3)
//访问过期时间3s
.expireAfterAccess(3, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, String>() {
@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视图 |
删除
主动删除,见操作方法,删除单个、批量删除、删除所有
被动删除
- 超过最大个数删除:LRU+FIFO => 访问次数一样少的情况下使用FIFO
- 过期删除:访问时间过期、写入时间过期
- 引用删除:通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以垃圾回收
删除监控
public static void main(String[] args) {
LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
//缓存存储最大数量
.maximumSize(3)
//访问过期时间3s
.expireAfterAccess(3, TimeUnit.SECONDS)
//监听删除
.removalListener(notification -> System.out.println("删除监听:" + notification.getKey() + "=" + notification.getCause()))
.build(new CacheLoader<Integer, String>() {
@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()的时候,判断缓存是否过期
Guava Cache底层实现
体系类图
LocalCache为Guava Cache的核心类,实现与ConcurrentHashMap相似,核心是一个Segement数组,也引入了段的概念
- Segement数组的长度决定了cache的并发数
- 每一个Segment使用了单独的锁,每个Segment继承了ReentrantLock,对Segment的写操作需要先拿到锁
final Segment<K, V>[] segments;
static class Segment<K, V> 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<K, V> 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<K, V> 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<K, V> 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<Integer, String> numberCache = CacheBuilder.newBuilder()
.maximumSize(10)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(8, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, String>() {
@Override
public String load(Integer key) throws Exception {
return key + "数字测试";
}
});
更多推荐
所有评论(0)