Guava Cache实现原理及最佳实践

news2024/11/15 4:46:29

本文内容包括Guava Cache的使用、核心机制的讲解、核心源代码的分析以及最佳实践的说明。

概要

Guava Cache是一款非常优秀本地缓存,使用起来非常灵活,功能也十分强大。Guava Cache说简单点就是一个支持LRU的ConcurrentHashMap,并提供了基于容量,时间和引用的缓存回收方式。

本文详细的介绍了Guava Cache的使用注意事项,即最佳实践,以及作为一个Local Cache的实现原理。

应用及使用

应用场景

  • 读取热点数据,以空间换时间,提升时效
  • 计数器,例如可以利用基于时间的过期机制作为限流计数

基本使用

Guava Cache提供了非常友好的基于Builder构建者模式的构造器,用户只需要根据需求设置好各种参数即可使用。Guava Cache提供了两种方式创建一个Cache。

CacheLoader

CacheLoader可以理解为一个固定的加载器,在创建Cache时指定,然后简单地重写V load(K key) throws Exception方法,就可以达到当检索不存在的时候,会自动的加载数据的。例子代码如下:

//创建一个LoadingCache,并可以进行一些简单的缓存配置
private static LoadingCache<String, String > loadingCache = CacheBuilder.newBuilder()
    //最大容量为100(基于容量进行回收)
    .maximumSize(100)
    //配置写入后多久使缓存过期-下文会讲述
    .expireAfterWrite(150, TimeUnit.SECONDS)
    //配置写入后多久刷新缓存-下文会讲述
    .refreshAfterWrite(1, TimeUnit.SECONDS)
    //key使用弱引用-WeakReference
    .weakKeys()
    //当Entry被移除时的监听器
    .removalListener(notification -> log.info("notification={}", GsonUtil.toJson(notification)))
    //创建一个CacheLoader,重写load方法,以实现"当get时缓存不存在,则load,放到缓存,并返回"的效果
    .build(new CacheLoader<String, String>() {
        //重点,自动写缓存数据的方法,必须要实现
        @Override
        public String load(String key) throws Exception {
            return "value_" + key;
        }
        //异步刷新缓存-下文会讲述
        @Override
        public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
            return super.reload(key, oldValue);
        }
    });
    
    @Test
    public void getTest() throws Exception {
    //测试例子,调用其get方法,cache会自动加载并返回
    String value = loadingCache.get("1");
    //返回value_1
    log.info("value={}", value);
}
Callable

在上面的build方法中是可以不用创建CacheLoader的,不管有没有CacheLoader,都是支持Callable的。Callable在get时可以指定,效果跟CacheLoader一样,区别就是两者定义的时间点不一样,Callable更加灵活,可以理解为Callable是对CacheLoader的扩展。例子代码如下:

@Test
public void callableTest() throws Exception {
    String key = "1";
    //loadingCache的定义跟上一面一样
    //get时定义一个Callable
    String value = loadingCache.get(key, new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "call_" + key;
        }
    });
    log.info("call value={}", value);
}
其他用法

显式插入:

支持loadingCache.put(key, value)方法直接覆盖key的值。

显式失效:

支持loadingCache.invalidate(key) loadingCache.invalidateAll() 方法,手动使缓存失效。

缓存失效机制

Guava Cache有一套十分优秀的缓存失效机制,这里主要介绍的是基于时间的失效回收。

缓存失效的目的是让缓存进行重新加载,即刷新,使调用者可以正常访问获取到最新的数据,而不至于返回null或者直接访问DB。

从上面的例子中我们知道与失效/缓存刷新相关配置有 expireAfterWrite / expireAfterAccess、refreshAfterWrite 还有 CacheLoader的reload方法。

一般用法

expireAfterWrite/expireAfterAccess
使用背景

如果对缓存设置过期时间,在高并发下同时执行get操作,而此时缓存值已过期了,如果没有保护措施,则会导致大量线程同时调用生成缓存值的方法,比如从数据库读取,对数据库造成压力,这也就是我们常说的“缓存击穿”。

做法

而Guava cache则对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。这两个配置的区别前者记录写入时间,后者记录写入或访问时间,内部分别用writeQueue和accessQueue维护。

PS: 但是在高并发下,这样还是会阻塞大量线程。

refreshAfterWrite
使用背景

使用 expireAfterWrite 会导致其他线程阻塞。

做法

更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。

异步刷新
使用背景

单个key并发下,使用refreshAfterWrite,虽然不会阻塞了,但是如果恰巧同时多个key同时过期,还是会给数据库造成压力,这就是我们所说的“缓存雪崩”。

做法

这时就要用到异步刷新,将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值。

方法是覆盖CacheLoader的reload方法,使用线程池去异步加载数据

PS:只有重写了 reload 方法才有“异步加载”的效果。默认的 reload 方法就是同步去执行 load 方法。

总结

大家都应该对各个失效/刷新机制有一定的理解,清楚在各个场景可以使用哪个配置,简单总结一下:

  1. expireAfterWrite 是允许一个线程进去load方法,其他线程阻塞等待。
  2. refreshAfterWrite 是允许一个线程进去load方法,其他线程返回旧的值。
  3. 在上一点基础上做成异步,即回源线程不是请求线程。异步刷新是用线程异步加载数据,期间所有请求返回旧的缓存值。

实现原理

数据结构

Guava Cache的数据结构跟JDK1.7的ConcurrentHashMap类似,如下图所示:

img

LoadingCache

LoadingCache即是我们API Builder返回的类型,类继承图如下:

img

LocalCache

LoadingCache这些类表示获取Cache的方式,可以有多种方式,但是它们的方法最终调用到LocalCache的方法,LocalCache是Guava Cache的核心类。看看LocalCache的定义:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

说明Guava Cache本质就是一个Map。

LocalCache的重要属性:

//Map的数组
final Segment<K, V>[] segments;
//并发量,即segments数组的大小
final int concurrencyLevel;
//key的比较策略,跟key的引用类型有关
final Equivalence<Object> keyEquivalence;
//value的比较策略,跟value的引用类型有关
final Equivalence<Object> valueEquivalence;
//key的强度,即引用类型的强弱
final Strength keyStrength;
//value的强度,即引用类型的强弱
final Strength valueStrength;
//访问后的过期时间,设置了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;
//时间器
final Ticker ticker;
//创建Entry的工厂,根据引用类型不同
final EntryFactory entryFactory;
Segment

从上面可以看出LocalCache这个Map就是维护一个Segment数组。Segment是一个ReentrantLock

static class Segment<K, V> extends ReentrantLock

看看Segment的重要属性:

//LocalCache
final LocalCache<K, V> map;
//segment存放元素的数量
volatile int count;
//修改、更新的数量,用来做弱一致性
int modCount;
//扩容用
int threshold;
//segment维护的数组,用来存放Entry。这里使用AtomicReferenceArray是因为要用CAS来保证原子性
volatile @MonotonicNonNull AtomicReferenceArray<ReferenceEntry<K, V>> table;
//如果key是弱引用的话,那么被GC回收后,就会放到ReferenceQueue,要根据这个queue做一些清理工作
final @Nullable ReferenceQueue<K> keyReferenceQueue;
//跟上同理
final @Nullable ReferenceQueue<V> valueReferenceQueue;
//如果一个元素新写入,则会记到这个队列的尾部,用来做expire
@GuardedBy("this")
final Queue<ReferenceEntry<K, V>> writeQueue;
//读、写都会放到这个队列,用来进行LRU替换算法
@GuardedBy("this")
final Queue<ReferenceEntry<K, V>> accessQueue;
//记录哪些entry被访问,用于accessQueue的更新。
final Queue<ReferenceEntry<K, V>> recencyQueue;
ReferenceEntry

ReferenceEntry就是一个Entry的引用,有几种引用类型:

img

我们拿StrongEntry为例,看看有哪些属性:

final K key;
final int hash;
//指向下一个Entry,说明这里用的链表(从上图可以看出)
final @Nullable ReferenceEntry<K, V> next;
//value
volatile ValueReference<K, V> valueReference = unset();

源码分析

当我们了解了Guava Cache的结构后,那么进行源码分析就会简单很多。

本文只对put和get这两个重点操作来进行源码分析,其他源码如果读者感兴趣请自行阅读。

以下源码基于guava-26.0-jre版本。

get
get主流程

我们从LoadingCache的get(key)方法入手:

//LocalLoadingCache的get方法,直接调用LocalCache
public V get(K key) throws ExecutionException {
    return localCache.getOrLoad(key);
}

LocalCache:

V getOrLoad(K key) throws ExecutionException {
    return get(key, defaultLoader);
}

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    //根据key获取hash值
    int hash = hash(checkNotNull(key));
    //通过hash定位到是哪个Segment,然后是Segment的get方法
    return segmentFor(hash).get(key, hash, loader);
}

Segment:

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    checkNotNull(key);
    checkNotNull(loader);
    try {
        //这里是进行快速判断,如果count != 0则说明呀已经有数据
        if (count != 0) {
            //根据hash定位到table的第一个Entry
            ReferenceEntry<K, V> e = getEntry(key, hash);
            if (e != null) {
                //跟currentTimeMillis类似
                long now = map.ticker.read();
                //获取还没过期的value,如果过期了,则返回null。getLiveValue下面展开
                V value = getLiveValue(e, now);
                //Entry还没过期
                if (value != null) {
                    //记录被访问过
                    recordRead(e, now);
                    //命中率统计
                    statsCounter.recordHits(1);
                    //判断是否需要刷新,如果需要刷新,那么会去异步刷新,且返回旧值。scheduleRefresh下面展开
                    return scheduleRefresh(e, key, hash, value, now, loader);
                }

                ValueReference<K, V> valueReference = e.getValueReference();
                //如果entry过期了且数据还在加载中,则等待直到加载完成。这里的ValueReference是LoadingValueReference,其waitForValue方法是调用内部的Future的get方法,具体读者可以点进去看。
                if (valueReference.isLoading()) {
                    return waitForLoadingValue(e, key, valueReference);
                }
            }
        }

        //重点方法。lockedGetOrLoad下面展开
        //走到这一步表示: 之前没有写入过数据 || 数据已经过期 || 数据不是在加载中。
        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();
    }
}

//getLiveValue
V getLiveValue(ReferenceEntry<K, V> entry, long now) {
    //被GC回收了
    if (entry.getKey() == null) {
        //
        tryDrainReferenceQueues();
        return null;
    }
    V value = entry.getValueReference().get();
    //被GC回收了
    if (value == null) {
        tryDrainReferenceQueues();
        return null;
    }
    //判断是否过期
    if (map.isExpired(entry, now)) {
        tryExpireEntries(now);
        return null;
    }
    return value;
}

//isExpired,判断Entry是否过期
boolean isExpired(ReferenceEntry<K, V> entry, long now) {
    checkNotNull(entry);
    //如果配置了expireAfterAccess,用当前时间跟entry的accessTime比较
    if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
        return true;
    }
    //如果配置了expireAfterWrite,用当前时间跟entry的writeTime比较
    if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
        return true;
    }
    return false;
}
scheduleRefresh

从get的流程得知,如果entry还没过期,则会进入此方法,尝试去刷新数据。

V scheduleRefresh(
    ReferenceEntry<K, V> entry,
    K key,
    int hash,
    V oldValue,
    long now,
    CacheLoader<? super K, V> loader) {
    //1、是否配置了refreshAfterWrite
    //2、用writeTime判断是否达到刷新的时间
    //3、是否在加载中,如果是则没必要再进行刷新
    if (map.refreshes()
        && (now - entry.getWriteTime() > map.refreshNanos)
        && !entry.getValueReference().isLoading()) {
        //异步刷新数据。refresh下面展开
        V newValue = refresh(key, hash, loader, true);
        //返回新值
        if (newValue != null) {
            return newValue;
        }
    }
    //否则返回旧值
    return oldValue;
}

//refresh
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
    //为key插入一个LoadingValueReference,实质是把对应Entry的ValueReference替换为新建的LoadingValueReference。insertLoadingValueReference下面展开
    final LoadingValueReference<K, V> loadingValueReference =
    insertLoadingValueReference(key, hash, checkTime);
    if (loadingValueReference == null) {
        return null;
    }
    //通过loader异步加载数据,这里返回的是Future。loadAsync下面展开
    ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
    //这里立即判断Future是否已经完成,如果是则返回结果。否则返回null。因为是可能返回immediateFuture或者ListenableFuture。
    //这里的官方注释是: Returns the newly refreshed value associated with key if it was refreshed inline, or null if another thread is performing the refresh or if an error occurs during
    if (result.isDone()) {
        try {
            return Uninterruptibles.getUninterruptibly(result);
        } catch (Throwable t) {
            // don't let refresh exceptions propagate; error was already logged
        }
    }
    return null;
}

//insertLoadingValueReference方法。
//这个方法虽然看上去有点长,但其实挺简单的,如果你熟悉HashMap的话。
LoadingValueReference<K, V> insertLoadingValueReference(
    final K key, final int hash, boolean checkTime) {
    ReferenceEntry<K, V> e = null;
    //把segment上锁
    lock();
    try {
        long now = map.ticker.read();
        //做一些清理工作
        preWriteCleanup(now);

        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        //如果key对应的entry存在
        for (e = first; e != null; e = e.getNext()) {
            K entryKey = e.getKey();
            //通过key定位到entry
            if (e.getHash() == hash
                && entryKey != null
                && map.keyEquivalence.equivalent(key, entryKey)) {

                ValueReference<K, V> valueReference = e.getValueReference();
                //如果是在加载中,或者还没达到刷新时间,则返回null
                //这里对这个判断再进行了一次,我认为是上锁lock了,再重新获取now,对时间的判断更加准确
                if (valueReference.isLoading()
                    || (checkTime && (now - e.getWriteTime() < map.refreshNanos))) {

                    return null;
                }

                //new一个LoadingValueReference,然后把entry的valueReference替换掉。
                ++modCount;
                LoadingValueReference<K, V> loadingValueReference =
                new LoadingValueReference<>(valueReference);
                e.setValueReference(loadingValueReference);
                return loadingValueReference;
            }
            }

                如果key对应的entry不存在,则新建一个Entry,操作跟上面一样。
                ++modCount;
                LoadingValueReference<K, V> loadingValueReference = new LoadingValueReference<>();
                e = newEntry(key, hash, first);
                e.setValueReference(loadingValueReference);
                table.set(index, e);
                return loadingValueReference;
            } finally {
                unlock();
                postWriteCleanup();
            }
            }

                //loadAsync
                ListenableFuture<V> loadAsync(
                final K key,
                final int hash,
                final LoadingValueReference<K, V> loadingValueReference,
                CacheLoader<? super K, V> loader) {
                //通过loadFuture返回ListenableFuture。loadFuture下面展开
                final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
                //对ListenableFuture添加listener,当数据加载完后的后续处理。
                loadingFuture.addListener(
                new Runnable() {
                @Override
                public void run() {
                try {
                //这里主要是把newValue set到entry中。还涉及其他一系列操作,读者可自行阅读。
                getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
            } catch (Throwable t) {
                logger.log(Level.WARNING, "Exception thrown during refresh", t);
                loadingValueReference.setException(t);
            }
            }
            },
                directExecutor());
                return loadingFuture;
            }

                //loadFuture
                public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) {
                try {
                stopwatch.start();
                //这个oldValue指的是插入LoadingValueReference之前的ValueReference,如果entry是新的,那么oldValue就是unset,即get返回null。
                V previousValue = oldValue.get();
                //这里要注意***
                //如果上一个value为null,则调用loader的load方法,这个load方法是同步的。
                //这里需要使用同步加载的原因是,在上面的“缓存失效机制”也说了,即使用异步,但是还没有oldValue也是没用的。如果在系统启动时来高并发请求的话,那么所有的请求都会阻塞,所以给热点数据预加热是很有必要的。
                if (previousValue == null) {
                V newValue = loader.load(key);
                return set(newValue) ? futureValue : Futures.immediateFuture(newValue);
            }
                //否则,使用reload进行异步加载
                ListenableFuture<V> newValue = loader.reload(key, previousValue);
                if (newValue == null) {
                return Futures.immediateFuture(null);
            }

                return transform(
                newValue,
                new com.google.common.base.Function<V, V>() {
                @Override
                public V apply(V newValue) {
                LoadingValueReference.this.set(newValue);
                return newValue;
            }
            },
                directExecutor());
            } catch (Throwable t) {
                ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t);
                if (t instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
                return result;
            }
            }
lockedGetOrLoad

如果之前没有写入过数据 || 数据已经过期 || 数据不是在加载中,则会调用lockedGetOrLoad

V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    ReferenceEntry<K, V> e;
    ValueReference<K, V> valueReference = null;
    LoadingValueReference<K, V> loadingValueReference = null;
    //用来判断是否需要创建一个新的Entry
    boolean createNewEntry = true;
    //segment上锁
    lock();
    try {
        // re-read ticker once inside the lock
        long now = map.ticker.read();
        //做一些清理工作
        preWriteCleanup(now);

        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        //通过key定位entry
        for (e = first; e != null; e = e.getNext()) {
            K entryKey = e.getKey();
            if (e.getHash() == hash
                && entryKey != null
                && map.keyEquivalence.equivalent(key, entryKey)) {
                //找到entry
                valueReference = e.getValueReference();
                //如果value在加载中则不需要重复创建entry
                if (valueReference.isLoading()) {
                    createNewEntry = false;
                } else {
                    V value = valueReference.get();
                    //value为null说明已经过期且被清理掉了
                    if (value == null) {
                        //写通知queue
                        enqueueNotification(
                            entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
                        //过期但还没被清理
                    } else if (map.isExpired(e, now)) {
                        //写通知queue
                        // This is a duplicate check, as preWriteCleanup already purged expired
                        // entries, but let's accomodate an incorrect expiration queue.
                        enqueueNotification(
                            entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
                    } else {
                        recordLockedRead(e, now);
                        statsCounter.recordHits(1);
                        //其他情况则直接返回value
                        //来到这步,是不是觉得有点奇怪,我们分析一下: 
                        //进入lockedGetOrLoad方法的条件是数据已经过期 || 数据不是在加载中,但是在lock之前都有可能发生并发,进而改变entry的状态,所以在上面中再次判断了isLoading和isExpired。所以来到这步说明,原来数据是过期的且在加载中,lock的前一刻加载完成了,到了这步就有值了。
                        return value;
                    }

                    writeQueue.remove(e);
                    accessQueue.remove(e);
                    this.count = newCount; // write-volatile
                }
                break;
            }
        }
        //创建一个Entry,且set一个新的LoadingValueReference。
        if (createNewEntry) {
            loadingValueReference = new LoadingValueReference<>();

            if (e == null) {
                e = newEntry(key, hash, first);
                e.setValueReference(loadingValueReference);
                table.set(index, e);
            } else {
                e.setValueReference(loadingValueReference);
            }
        }
    } finally {
        unlock();
        postWriteCleanup();
    }
    //同步加载数据。里面的方法都是在上面有提及过的,读者可自行阅读。
    if (createNewEntry) {
        try {
            synchronized (e) {
                return loadSync(key, hash, loadingValueReference, loader);
                                }
                                } finally {
                                statsCounter.recordMisses(1);
                                }
                                } else {
                                // The entry already exists. Wait for loading.
                                return waitForLoadingValue(e, key, valueReference);
                                }
                                }
流程图

通过分析get的主流程代码,我们来画一下流程图:

img

put

看懂了get的代码后,put的代码就显得很简单了。

Segment的put方法:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //Segment上锁
    lock();
    try {
        long now = map.ticker.read();
        preWriteCleanup(now);

        int newCount = this.count + 1;
        if (newCount > this.threshold) { // ensure capacity
            expand();
            newCount = this.count + 1;
        }

        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        //根据key找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)) {

                //定位到entry
                ValueReference<K, V> valueReference = e.getValueReference();
                V entryValue = valueReference.get();
                //value为null说明entry已经过期且被回收或清理掉
                if (entryValue == null) {
                    ++modCount;
                    if (valueReference.isActive()) {
                        enqueueNotification(
                            key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
                        //设值
                        setValue(e, key, value, now);
                        newCount = this.count; // count remains unchanged
                    } else {
                        setValue(e, key, value, now);
                        newCount = this.count + 1;
                    }
                    this.count = newCount; // write-volatile
                    evictEntries(e);
                    return null;
                } else if (onlyIfAbsent) {
                    //如果是onlyIfAbsent选项则返回旧值
                    recordLockedRead(e, now);
                    return entryValue;
                } else {
                    //不是onlyIfAbsent,设值
                    ++modCount;
                    enqueueNotification(
                        key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
                    setValue(e, key, value, now);
                    evictEntries(e);
                    return entryValue;
                }
            }
        }

        //没有找到entry,则新建一个Entry并设值
        ++modCount;
        ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
        setValue(newEntry, key, value, now);
        table.set(index, newEntry);
        newCount = this.count + 1;
        this.count = newCount; // write-volatile
        evictEntries(newEntry);
        return null;
    } finally {
        unlock();
        postWriteCleanup();
    }
}

put的流程相对get来说没有那么复杂。

最佳实践

关于最佳实践,在上面的“缓存失效机制”中得知,看来使用refreshAfterWrite是一个不错的选择,但是从上面get的源码分析和流程图看出,或者了解Guava Cache都知道,Guava Cache是没有定时器或额外的线程去做清理或加载操作的,都是通过get来触发的,目的是降低复杂性和减少对系统的资源消耗。

那么只使用refreshAfterWrite或配置不当的话,会带来一个问题:如果一个key很长时间没有访问,这时来一个请求的话会返回旧值,这个好像不是很符合我们的预想,在并发下返回旧值是为了不阻塞,但是在这个场景下,感觉有足够的时间和资源让我们去刷新数据。

结合get的流程图,在get的时候,是先判断过期,再判断refresh,即如果过期了会优先调用 load 方法(阻塞其他线程),在不过期情况下且过了refresh时间才去做 reload (异步加载,同时返回旧值),所以推荐的设置是 refresh < expire,这个设置还可以解决一个场景就是,如果长时间没有访问缓存,可以保证 expire 后可以取到最新的值,而不是因为 refresh 取到旧值。

用一张时间轴图简单表示:

img

总结

Guava Cache是一个很优秀的本地缓存工具,缓存的作用不多说,一个简单易用,功能强大的工具会使你在开发中事倍功半。但是跟所有的工具一样,你要在了解其内部原理、机制的情况下,才能发挥其最大的功效,才能适用到你的业务场景中。

本文通过对Guava Cache的使用、核心机制的讲解、核心源代码的分析以及最佳实践的说明,相信你会对Guava Cache有更进一步的了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2089921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java面试宝典-java基础08

Java面试宝典-java基础08 71、BIO、NIO、AIO有哪些应用场景72、简述一下BIO的编程流程73、NIO的三大核心部分是什么&#xff1f;74、NIO中buffer的四大属性是什么&#xff1f;75、对比一下BIO和NIO&#xff1f;76、FileChannel是做什么的&#xff1f;77、简述一下Selector选择器…

51单片机-矩阵键盘(基于LC602)

时间&#xff1a;2024.8.30 作者&#xff1a;Whappy 目的&#xff1a;手撕51&#xff08;第二遍&#xff09; 代码&#xff1a; main.c #include <REGX52.H> #include "LCD1602.h" #include "Delay.h" #include "MatrixKey.h"unsigned…

【Canvas与艺术】录王昌龄诗《从军行之四》

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>出塞青海长云暗雪山</title><style type"text/css&quo…

opencv实战项目十六:kmeans图像颜色聚类:

文章目录 前言K-means介绍效果 前言 在数字化时代&#xff0c;图像处理技术已成为计算机视觉领域的重要组成部分。其中&#xff0c;图像颜色聚类作为一项关键技术在众多应用场景中发挥着重要作用&#xff0c;如图像分割、物体识别、色彩调整等。K-means算法作为一种经典的聚类…

Java性能优化传奇之旅--Java万亿级性能优化之电商平台高峰时段性能大作战:策略与趋势洞察

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Redis基础知识学习(入门篇)

文章目录 五大数据结构一. String: 字符串二. Hash: 散列概念性质 三. List: 列表四. Set: 集合特点 五. Sorted Set: 有序集合 五大数据结构 一. String: 字符串 数据结构中&#xff0c;字符串要单独用一种存储结构来存储&#xff0c;称为串存储结构。这里的串指的就是字符串…

性能分析之使用 Jvisualvm dump 分析示例

一、前言 在 JMeter 入门系列中相信大家对工具使用已经没问题&#xff0c;今天开起性能测试进阶系列之 jvisualvm 工具简单学习&#xff0c;目标是通过演示 Jvisualvm 工具定位代码&#xff0c;帮助性能测试工程师直接定位代码位置&#xff0c;协助开发解决性能问题&#xff1…

Flink CDC MySQL数据同步到Doris表同步配置生成工具类

工具类 生成的配置 要同步表为&#xff1a; customer_user.tb_business_user_info express.route_push_service 请提前自行到doris中建好目标数据库&#xff0c;如果没有会报错 同步的配置文件如下&#xff1a;&#xff08;将配置内容保存为xxx.yaml文件到flink cdc提交任务&…

昇腾 Ascend 概念澄清 Host、Device、AI core、AI CPU、DVPP、AIPP、AscendCL、AscendC

昇腾 Ascend 概念澄清 Host、Device、AI core、AI CPU、DVPP、AIPP、AscendCL、AscendC flyfish Ascend C开发算子&#xff0c;偏低。 AscendCL开发应用&#xff0c;偏高。 AI core、AI CPU、DVPP都属于计算资源。 Ascend C开发的算子运行在AI Core上。 AIPP用于在AI Core上完…

TimeWheel算法介绍及在应用上的探索

作者&#xff1a;来自 vivo 互联网服务器团队- Li Fan 本文从追溯时间轮算法的出现&#xff0c;介绍了时间轮算法未出现前&#xff0c;基于队列的定时任务实现&#xff0c;以及基于队列的定时任务实现所存在的缺陷。接着我们介绍了时间轮算法的算法思想及其数据结构&#xff0c…

手撕数据结构与算法——拓扑排序

拓扑排序是图论中的一个重要概念&#xff0c;它在许多领域如任务调度、课程规划等都有广泛的应用。在这篇文章中&#xff0c;我们将探讨拓扑排序的基本概念、算法实现以及在C/C中的实现方法。 拓扑排序简介 拓扑排序是针对有向无环图&#xff08;DAG&#xff09;的一种排序算法…

二叉树(数据结构)

1.两种特殊的二叉树 1. 满二叉树 : 一棵二叉树&#xff0c;如果 每层的结点数都达到最大值&#xff0c;则这棵二叉树就是满二叉树 。也就是说&#xff0c; 如果一棵 二叉树的层数为 K &#xff0c;且结点总数是2^k-1 &#xff0c;则它就是满二叉树 。 2. 完全二叉树 : 完…

为你的LLM应用增加记忆能力

1. 记忆系统的重要性 我们都知道&#xff0c;大模型本身是无状态、无记忆的。默认情况下&#xff0c;我们向大模型发起的每次提问&#xff0c;在其内部都会被视为一次全新的调用。尽管诸如 ChatGPT 等聊天应用内置了部分记忆功能&#xff0c;可以记录用户最近几轮的聊天信息&a…

ChatTTS容器构建教程

一、模型介绍 ChatTTS 是专门为对话场景设计的文本转语音模型&#xff0c;例如 LLM 助手对话任务。它支持英文和中文两种语言。最大的模型使用了 10 万小时以上的中英文数据进行训练。在 HuggingFace 中开源的版本为 4 万小时训练且未 SFT 的版本。 ChatTTS WebUI如下&#x…

【单片机原理及应用】实验:LED循环控制

目录 一、实验目的 二、实验内容 三、实验步骤 四、记录与处理 五、思考 六、成果文件提取链接 一、实验目的 熟悉Proteus x8原理图与C51程序的联合仿真调试方法&#xff0c;掌握C51延时函数和循环控制的方法 二、实验内容 【参照图表】 &#xff08;1&#xff09;创建一…

晚宴扫码查询桌号

在现代社交活动中&#xff0c;晚宴的组织和管理越来越依赖于技术手段。为了提高晚宴的效率和参与者的体验&#xff0c;我们可以通过一个简单的扫码查询系统来实现快速查找桌号和座位号。以下是详细步骤&#xff1a; 1. 电脑端上传查询信息 访问云分组官网。 使用微信扫码登录…

AcrelEMS3.0企业微电网智慧能源平台的设计与应用-安科瑞 蒋静

1系统概述 1.1 概述 2020年9月&#xff0c;我国明确提出2030年“碳达峰”与2060年“碳中和”目标。2022年6月&#xff0c;科技部、国家发展改革委、工业和信息化部、生态环境部、住房城乡建设部、交通运输部、中科院、工程院、国家能源局共同研究制定了《科技支撑碳达峰碳中和…

netty编程之实现断点续传(分片发送)功能

写在前面 在我们使用各种网盘的时候&#xff0c;可以随时的暂停上传&#xff0c;然后继续上传&#xff0c;这其实就是断点续传的功能&#xff0c;本文就看下在netty中如何实现断点续传的功能。 1&#xff1a;核心点介绍 1.1&#xff1a;RandomAccessFile RandomAccessFile类…

汽车信息安全--MCU Flash读保护真的没有后门吗?

目录 1.修bug修出的灵感 2.串行编程接口协议 3.毛刺攻击 4.RH850 串行编程模式 5.小结 1.修bug修出的灵感 ECU量产后通过密码控制来防止通过Debug口读取Flash的程序和数据。 这是应该是共识了&#xff0c;但是这样做真的就万无一失了吗&#xff1f; 最近解决了个问题&…

Linux操作系统在虚拟机VM上的安装【CentOS版本】

目录 准备工作 "CPU虚拟化"的方法 VMware的安装 Linux镜像文件的下载 开始安装 声明 新建虚拟机 安装CentOS7.6 配置Linux(CentOS7.6)操作系统 配置分区【学习者可以直接点击自动配置分区&#xff0c;不过还是建议学习一下手动分区】 分区原则 添加分区 …