Guava Cache并发操作、动态加载、自定义LRU、常见问题解决以及源码解析

一、GuavaCache并发操作

1、并发设置

GuavaCache通过设置 concurrencyLevel 使得缓存支持并发的写入和读取

LoadingCache<String,Object> cache = CacheBuilder.newBuilder()
// 最大3个 同时支持CPU核数线程写缓存
.maximumSize(3).concurrencyLevel(Runtime.getRuntime().availableProcessors()).build();

concurrencyLevel=Segment数组的长度

同ConcurrentHashMap类似Guava cache的并发也是通过分离锁实现

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = this.hash(Preconditions.checkNotNull(key));
//通过hash值确定该key位于哪一个segment上,并获取该segment
return this.segmentFor(hash).get(key, hash, loader);
}

LoadingCache采用了类似ConcurrentHashMap的方式,将映射表分为多个segment。segment之间可以并发访问,这样可以大大提高并发的效率,使得并发冲突的可能性降低了。

2、更新锁定

GuavaCache提供了一个refreshAfterWrite定时刷新数据的配置项

如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,会在后台异步去刷新缓存

刷新时只有一个请求回源取数据,其他请求会阻塞(block)在一个固定时间段,如果在该时间段内没有获得新值则返回旧值。

LoadingCache<String,Object> cache = CacheBuilder.newBuilder()
// 最大3个 同时支持CPU核数线程写缓存
.maximumSize(3).concurrencyLevel(Runtime.getRuntime().availableProcessors()).
//3秒内阻塞会返回旧数据
refreshAfterWrite(3,TimeUnit.SECONDS).build();

3、案例分享

public class JobBean {
	private int id;
	private String name;
	private int pid;
	private String type;
	private String url;
	private int isHot;
}

在这里插入图片描述

二、GuavaCache动态加载

动态加载行为发生在获取不到数据或者是数据已经过期的时间点,Guava动态加载使用回调模式

用户自定义加载方式,然后Guava cache在需要加载新数据时会回调用户的自定义加载方式

segmentFor(hash).get(key, hash, loader)

loader即为用户自定义的数据加载方式,当某一线程get不到数据会去回调该自定义加载方式去加载数据

三、GuavaCache自定义LRU算法

public class LinkedHashLRUcache<k, v> {
	/**
	* LinkedHashMap(自身实现了LRU算法)
	* 有序
	* 每次访问一个元素,都会加到尾部
	*/
	int limit;
	LRUcache<k, v> internalLRUcache;
	
	public LinkedHashLRUcache(int limit) {
		this.limit = limit;
		this.internalLRUcache = new LRUcache(limit);
	} 
	public void put(k key, v value) {
		this.internalLRUcache.put(key, value);
	} 
	public v get(k key) {
		return this.internalLRUcache.get(key);
	} 
	public static void main(String[] args) {
		LinkedHashLRUcache lru=new LinkedHashLRUcache(3);
		lru.put(1,"zhangfei1");
		lru.put(2,"zhangfei2");
		lru.put(3,"zhangfei3");
		lru.get(1);
		lru.put(4,"zhangfei4");
		for(Object o:lru.internalLRUcache.values()){
			System.out.println(o.toString());
		}
	}
	} 
	public class LRUcache<k, v> extends LinkedHashMap<k, v> {
		private final int limit;
		public LRUcache(int limit) {
			//初始化 accessOrder : true 改变尾结点
			super(16, 0.75f, true);
			this.limit = limit;
		} 
	
	//是否删除最老的数据
	@Override
	protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
		return size() > limit;
	}	
}

四、GuavaCache问题

1、GuavaCache会oom(内存溢出)吗?

会,当我们设置缓存永不过期(或者很长),缓存的对象不限个数(或者很大)时,比如:

Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(100000, TimeUnit.SECONDS)
.build();

不断向GuavaCache加入大字符串,最终将会oom

解决方案:缓存时间设置相对小些,使用弱引用方式存储对象

Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.weakValues().build();

2、GuavaCache缓存到期就会立即清除?

不是的,GuavaCache是在每次进行缓存操作的时候,如get()或者put()的时候,判断缓存是否过期

void evictEntries(ReferenceEntry<K, V> e) { 
	drainRecencyQueue();
	while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
		if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
		throw new AssertionError();
		}
	} 
	while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
		if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
		throw new AssertionError();
		}
	}
}

如果一个对象放入缓存以后,不在有任何缓存操作(包括对缓存其他key的操作),那么该缓存不会主动过期的。

3、GuavaCache如何找出最久未使用的数据

用accessQueue,这个队列是按照LRU的顺序存放的缓存对象(ReferenceEntry)的。会把访问过的对象放到队列的最后。

并且可以很方便的更新和删除链表中的节点,因为每次访问的时候都可能需要更新该链表,放入到链表的尾部。

这样,每次从access中拿出的头节点就是最久未使用的。

对应的writeQueue用来保存最久未更新的缓存队列,实现方式和accessQueue一样

五、Guava Cache源码剖析

1、GuavaCache实现框架

在这里插入图片描述

  • CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
  • CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算
  • CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作
  • Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作
  • LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据
  • LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法
  • LocalManualCache:LocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量localCache(LocalCache类型)的相应方法
  • LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法
  1. LocalCache
    LoadingCache这些类表示获取Cache的方式,可以有多种方式,但是它们的方法最终调用到LocalCache的方法,LocalCache是Guava Cache的核心类。
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

LocalCache为Guava Cache的核心类 LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各segment相对独立,互不影响,所以能支持并行操作。

//Map的数组
final Segment<K, V>[] segments;
//并发量,即segments数组的大小
final int concurrencyLevel;
...
//访问后的过期时间,设置了expireAfterAccess就有
final long expireAfterAccessNanos;
//写入后的过期时间,设置了expireAfterWrite就有
final long expireAfterWriteNa就有nos;
//刷新时间,设置了refreshAfterWrite就有
final long refreshNanos;
//removal的事件队列,缓存过期后先放到该队列
final Queue<RemovalNotification<K, V>> removalNotificationQueue;
//设置的removalListener
final RemovalListener<K, V> removalListener;
...

每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray。

static class Segment<K, V> extends ReentrantLock{
	/**
	* segments 维护一个entry列表的table,确保一致性状态。所以可以不加锁去读。节点的
	next field是不可修改的final,因为所有list的增加操作
	*/
	final LocalCache<K, V> map;
	/**
	* 该segment区域内所有存活的元素个数
	*/
	volatile int count;
	/**
	* 改变table大小size的更新次数。这个在批量读取方法期间保证它们可以看到一致性的快照:
	* 如果modCount在我们遍历段加载大小或者核对containsValue期间被改变了,然后我们会看
	到一个不一致的状态视图,以至于必须去重试。
	* count+modCount 保证内存一致性
	* *
	感觉这里有点像是版本控制,比如数据库里的version字段来控制数据一致性
	*/
	int modCount;
	/**
	* 每个段表,使用乐观锁的Array来保存entry The per-segment table.
	*/
	volatile AtomicReferenceArray<ReferenceEntry<K, V>> table; // 这里和
	concurrentHashMap不一致,原因是这边元素是引用,直接使用不会线程安全
	/**
	* A queue of elements currently in the map, ordered by write time.
	Elements are added to the tail of the queue
	* on write.
	*/
	@GuardedBy("Segment.this")
	final Queue<ReferenceEntry<K, V>> writeQueue;
	...

2、GuavaCache的CacheBuilder

缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
主要采用builder的模式,CacheBuilder的每一个方法都返回这个CacheBuilder知道build方法的调用。注意build方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数
据加载功能的缓存。

LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
	super(new LocalCache<K, V>(builder,
	checkNotNull(loader)));//LocalLoadingCache构造函数需要一个	LocalCache作为参数
}
//构造LocalCache
LocalCache(CacheBuilder<? super K, ? super V> builder,@Nullable CacheLoader<? super K, V> loader) {
	concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);//默认并发水平是4
	keyStrength = builder.getKeyStrength();//key的强引用
	valueStrength = builder.getValueStrength();
	keyEquivalence = builder.getKeyEquivalence();//key比较器
	valueEquivalence = builder.getValueEquivalence();
	maxWeight = builder.getMaximumWeight();
	weigher = builder.getWeigher();
	expireAfterAccessNanos = builder.getExpireAfterAccessNanos();//读写后有效期,超时重载 
	expireAfterWriteNanos = builder.getExpireAfterWriteNanos();//写后有效期,超时重载
	refreshNanos = builder.getRefreshNanos();
	removalListener = builder.getRemovalListener();//缓存触发失效 或者 GC回收软/弱引用,触发监听器
	removalNotificationQueue =//移除通知队列
	(removalListener == NullListener.INSTANCE) ? LocalCache.<RemovalNotification<K, V>>discardingQueue() : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();
	ticker = builder.getTicker(recordsTime());
	entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(),
	usesWriteEntries());
	globalStatsCounter = builder.getStatsCounterSupplier().get();
	defaultLoader = loader;//缓存加载器
	int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
	if (evictsBySize() && !customWeigher()) {
		initialCapacity = Math.min(initialCapacity, (int) maxWeight);
	}

3、GuavaCachePut流程

  1. 上锁
  2. 清除队列元素
    清理的是keyReferenceQueue和valueReferenceQueue这两个队列,这两个队列是引用队列如果发现key或者value被GC了,那么会在put的时候触发清理
  3. setvalue方法了,它做的是将value写入Entry
 V put(K key, int hash, V value, boolean onlyIfAbsent) {
//保证线程安全,加锁
        lock();
        try {
//获取当前的时间
            long now = map.ticker.read();
//清除队列中的元素
            preWriteCleanup(now);
...
//获取当前Entry中的HashTable的Entry数组
            AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
//定位
            int index = hash & (table.length() - 1);
//获取第一个元素
            ReferenceEntry<K, V> first = table.get(index);
//遍历整个Entry链表
// Look for an existing entry.
            for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
                K entryKey = e.getKey();
                if (e.getHash() == hash
                        && entryKey != null
                        && map.keyEquivalence.equivalent(key, entryKey)) {
// We found an existing entry.
//如果找到相应的元素
                    ValueReference<K, V> valueReference = e.getValueReference();
//获取value
                    V entryValue = valueReference.get();
//如果entry的value为null,可能被GC掉了
                    if (entryValue == null) {
                        ++modCount;
                        if (valueReference.isActive()) {
                            enqueueNotification( //减小锁时间的开销
                                    key, hash, entryValue, valueReference.getWeight(),
                                    RemovalCause.COLLECTED);
//利用原来的key并且刷新value
//存储数据,并且将新增加的元素写入两个队列中,一个Write队列、一个Access
                            队列
                            setValue(e, key, value, now);
                            newCount = this.count; // count remains unchanged
                        } else {
                            setValue(e, key, value, now);//存储数据,并且将新增加的元素写入两个队列
                            中
                                    newCount = this.count + 1;
                        } t
                        his.count = newCount; // write-volatile,保证内存可见性
//淘汰缓存
                        evictEntries(e);
                        return null;
                    } else if (onlyIfAbsent) {//原来的Entry中包含指定key的元素,所以读取一次,
                        读取操作需要更新Access队列
.......
                        setValue(e, key, value, now);//存储数据,并且将新增加的元素写入两个队列中
//数据的淘汰
                        evictEntries(e);
                        return entryValue;
                    }
                }
            }
            //如果目标的entry不存在,那么新建entry
            // Create a new entry.
            ++modCount;
            ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
            setValue(newEntry, key, value, now);
......
        } finally {
//解锁
            unlock();
//处理刚刚的remove Cause
            postWriteCleanup();
        }
    }

4、GuavaCache Get流程

  1. 获取对象引用(引用可能是非alive的,比如是需要失效的、比如是loading的);
  2. 判断对象引用是否是alive的(如果entry是非法的、部分回收的、loading状态、需要失效的,则
    认为不是alive)。
  3. 如果对象是alive的,如果设置refresh,则异步刷新查询value,然后等待返回最新value。
  4. 针对不是alive的,但却是在loading的,等待loading完成(阻塞等待)。
  5. 这里如果value还没有拿到,则查询loader方法获取对应的值(阻塞获取)。
 // LoadingCache methods
//local cache的代理
    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = hash(checkNotNull(key));//hash——>rehash
        return segmentFor(hash).get(key, hash, loader);
    } /
        / loading
//进行指定key对应的value的获取,读取不加锁
        V get(K key, int hash, CacheLoader<? super K, V> loader) throws
        ExecutionException {
        ....
        try {
        if (count != 0) { // read-volatile volatile读会刷新缓存,尽量保证可见性,如果为0那么直接load
    // don't call getLiveEntry, which would ignore loading values
            ReferenceEntry<K, V> e = getEntry(key, hash);
    //如果对应的Entry不为Null,证明值还在
            if (e != null) {
                long now = map.ticker.read();//获取当前的时间,根据当前的时间进行Live的数据
                的读取
                V value = getLiveValue(e, now); // 判断是否为alive(此处是懒失效,在每
                次get时才检查是否达到失效时机)
                ......
            }
        } 
        //如果取不到值,那么进行统一的加锁get
// at this point e is either null or expired; 此处或者为null,或者已经被失效。
            return lockedGetOrLoad(key, hash, loader);
        } catch (ExecutionException ee) {
            Throwable cause = ee.getCause();
        if (cause instanceof Error) {
            throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
            throw new UncheckedExecutionException(cause);
        } 
            throw ee;
        } finally {
            postReadCleanup();//每次Put和get之后都要进行一次Clean
        }
        }

5、GuavaCache过期重载

数据过期不会自动重载,而是通过get操作时执行过期重载。具体就是CacheBuilder构造的LocalLoadingCache

static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
        implements LoadingCache<K, V> {
    LocalLoadingCache(
            CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V>
            loader) {
        super(new LocalCache<K, V>(builder, checkNotNull(loader)));
    }
    // LoadingCache methods
    @Override
    public V get(K key) throws ExecutionException {
        return localCache.getOrLoad(key);
    }
    @Override
    public V getUnchecked(K key) {
        try {
            return get(key);
        } catch (ExecutionException e) {
            throw new UncheckedExecutionException(e.getCause());
        }
    }
    @Override
    public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws
            ExecutionException {
        return localCache.getAll(keys);
    } 
    @Override
    public void refresh(K key) {
        localCache.refresh(key);
    } 
    @Override
    public final V apply(K key) {
        return getUnchecked(key);
    } 
    // Serialization Support
    private static final long serialVersionUID = 1;
    @Override
    Object writeReplace() {
        return new LoadingSerializationProxy<K, V>(localCache);
    } 
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Guava Cache是Google提供的一套Java工具包中的一部分,它是一套非常完善的本地缓存机制(JVM缓存)。它的设计灵感来源于ConcurrentHashMap,可以按照多种策略来清理存储在其中的缓存值,同时保持很高的并发读写性能。在使用Guava Cache时,可以通过get()或者put()等方法进行缓存操作,当进行这些操作时,Guava Cache会进行惰性删除,即在获取或者放置缓存的时候判断缓存是否过期并进行删除。在Guava Cache的核心原理中,使用Segment来进行缓存值的定位和管理。在创建Guava Cache对象时,可以使用CacheLoader来自动加载数据到缓存中,当缓存不存在时,CacheLoader会负责获取数据并将其放置到缓存中。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [[由零开始]Guava Cache介绍和用法](https://blog.csdn.net/qq497811258/article/details/108260969)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Guava Cache简介、应用场景分析、代码实现以及核心的原理](https://blog.csdn.net/weixin_44795847/article/details/123702038)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值