1. Guava Cache 實現(xiàn)原理及最佳實踐

        共 43693字,需瀏覽 88分鐘

         ·

        2021-06-24 19:04

        以下文章來源于武培軒,回復(fù)資源獲取資料

        概要

        Guava Cache是一款非常優(yōu)秀本地緩存,使用起來非常靈活,功能也十分強大。Guava Cache說簡單點就是一個支持LRU的ConcurrentHashMap,并提供了基于容量,時間和引用的緩存回收方式。

        本文詳細的介紹了Guava Cache的使用注意事項,即最佳實踐,以及作為一個Local Cache的實現(xiàn)原理。

        應(yīng)用及使用

        應(yīng)用場景

        • 讀取熱點數(shù)據(jù),以空間換時間,提升時效
        • 計數(shù)器,例如可以利用基于時間的過期機制作為限流計數(shù)

        基本使用

        Guava Cache提供了非常友好的基于Builder構(gòu)建者模式的構(gòu)造器,用戶只需要根據(jù)需求設(shè)置好各種參數(shù)即可使用。Guava Cache提供了兩種方式創(chuàng)建一個Cache。

        CacheLoader

        CacheLoader可以理解為一個固定的加載器,在創(chuàng)建Cache時指定,然后簡單地重寫V load(K key) throws Exception方法,就可以達到當檢索不存在的時候,會自動的加載數(shù)據(jù)的。例子代碼如下:

        //創(chuàng)建一個LoadingCache,并可以進行一些簡單的緩存配置
        private static LoadingCache<String, String > loadingCache = CacheBuilder.newBuilder()
                    //最大容量為100(基于容量進行回收)
                    .maximumSize(100)
                    //配置寫入后多久使緩存過期-下文會講述
                    .expireAfterWrite(150, TimeUnit.SECONDS)
                    //配置寫入后多久刷新緩存-下文會講述
                    .refreshAfterWrite(1, TimeUnit.SECONDS)
                    //key使用弱引用-WeakReference
                    .weakKeys()
                    //當Entry被移除時的監(jiān)聽器
                    .removalListener(notification -> log.info("notification={}", GsonUtil.toJson(notification)))
                    //創(chuàng)建一個CacheLoader,重寫load方法,以實現(xiàn)"當get時緩存不存在,則load,放到緩存,并返回"的效果
                    .build(new CacheLoader<String, String>() {
                       //重點,自動寫緩存數(shù)據(jù)的方法,必須要實現(xiàn)
                        @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 {
               //測試例子,調(diào)用其get方法,cache會自動加載并返回
                String value = loadingCache.get("1");
               //返回value_1
                log.info("value={}", value);
            }

        Callable

        在上面的build方法中是可以不用創(chuàng)建CacheLoader的,不管有沒有CacheLoader,都是支持Callable的。Callable在get時可以指定,效果跟CacheLoader一樣,區(qū)別就是兩者定義的時間點不一樣,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有一套十分優(yōu)秀的緩存失效機制,這里主要介紹的是基于時間的失效回收。

        緩存失效的目的是讓緩存進行重新加載,即刷新,使調(diào)用者可以正常訪問獲取到最新的數(shù)據(jù),而不至于返回null或者直接訪問DB。

        從上面的例子中我們知道與失效/緩存刷新相關(guān)配置有 expireAfterWrite / expireAfterAccess、refreshAfterWrite 還有 CacheLoader的reload方法。

        一般用法

        expireAfterWrite/expireAfterAccess

        使用背景

        如果對緩存設(shè)置過期時間,在高并發(fā)下同時執(zhí)行g(shù)et操作,而此時緩存值已過期了,如果沒有保護措施,則會導(dǎo)致大量線程同時調(diào)用生成緩存值的方法,比如從數(shù)據(jù)庫讀取,對數(shù)據(jù)庫造成壓力,這也就是我們常說的“緩存擊穿”。

        做法

        而Guava cache則對此種情況有一定控制。當大量線程用相同的key獲取緩存值時,只會有一個線程進入load方法,而其他線程則等待,直到緩存值被生成。這樣也就避免了緩存擊穿的危險。這兩個配置的區(qū)別前者記錄寫入時間,后者記錄寫入或訪問時間,內(nèi)部分別用writeQueue和accessQueue維護。

        PS: 但是在高并發(fā)下,這樣還是會阻塞大量線程。

        refreshAfterWrite

        使用背景

        使用 expireAfterWrite 會導(dǎo)致其他線程阻塞。

        做法

        更新線程調(diào)用load方法更新該緩存,其他請求線程返回該緩存的舊值。

        異步刷新

        使用背景

        單個key并發(fā)下,使用refreshAfterWrite,雖然不會阻塞了,但是如果恰巧同時多個key同時過期,還是會給數(shù)據(jù)庫造成壓力,這就是我們所說的“緩存雪崩”。

        做法

        這時就要用到異步刷新,將刷新緩存值的任務(wù)交給后臺線程,所有的用戶請求線程均返回舊的緩存值。

        方法是覆蓋CacheLoader的reload方法,使用線程池去異步加載數(shù)據(jù)

        PS:只有重寫了 reload 方法才有“異步加載”的效果。默認的 reload 方法就是同步去執(zhí)行 load 方法。

        總結(jié)

        大家都應(yīng)該對各個失效/刷新機制有一定的理解,清楚在各個場景可以使用哪個配置,簡單總結(jié)一下:

        expireAfterWrite 是允許一個線程進去load方法,其他線程阻塞等待。

        refreshAfterWrite 是允許一個線程進去load方法,其他線程返回舊的值。

        在上一點基礎(chǔ)上做成異步,即回源線程不是請求線程。異步刷新是用線程異步加載數(shù)據(jù),期間所有請求返回舊的緩存值。

        實現(xiàn)原理

        數(shù)據(jù)結(jié)構(gòu)

        Guava Cache的數(shù)據(jù)結(jié)構(gòu)跟JDK1.7的ConcurrentHashMap類似,如下圖所示:

        LoadingCache

        LoadingCache即是我們API Builder返回的類型,類繼承圖如下:

        LocalCache

        LoadingCache這些類表示獲取Cache的方式,可以有多種方式,但是它們的方法最終調(diào)用到LocalCache的方法,LocalCache是Guava Cache的核心類??纯碙ocalCache的定義:

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

        說明Guava Cache本質(zhì)就是一個Map。

        LocalCache的重要屬性:

         //Map的數(shù)組
         final Segment<K, V>[] segments;
        //并發(fā)量,即segments數(shù)組的大小
         final int concurrencyLevel;
         //key的比較策略,跟key的引用類型有關(guān)
         final Equivalence<Object> keyEquivalence;
         //value的比較策略,跟value的引用類型有關(guān)
         final Equivalence<Object> valueEquivalence;
         //key的強度,即引用類型的強弱
         final Strength keyStrength;
         //value的強度,即引用類型的強弱
         final Strength valueStrength;
         //訪問后的過期時間,設(shè)置了expireAfterAccess就有
         final long expireAfterAccessNanos;
         //寫入后的過期時間,設(shè)置了expireAfterWrite就有
         final long expireAfterWriteNa就有nos;
         //刷新時間,設(shè)置了refreshAfterWrite就有
         final long refreshNanos;
         //removal的事件隊列,緩存過期后先放到該隊列
         final Queue<RemovalNotification<K, V>> removalNotificationQueue;
         //設(shè)置的removalListener
         final RemovalListener<K, V> removalListener;
         //時間器
         final Ticker ticker;
         //創(chuàng)建Entry的工廠,根據(jù)引用類型不同
         final EntryFactory entryFactory;

        Segment

        從上面可以看出LocalCache這個Map就是維護一個Segment數(shù)組。Segment是一個ReentrantLock

        static class Segment<K, V> extends ReentrantLock

        看看Segment的重要屬性:

        //LocalCache
        final LocalCache<K, V> map;
        //segment存放元素的數(shù)量
        volatile int count;
        //修改、更新的數(shù)量,用來做弱一致性
        int modCount;
        //擴容用
        int threshold;
        //segment維護的數(shù)組,用來存放Entry。這里使用AtomicReferenceArray是因為要用CAS來保證原子性
        volatile @MonotonicNonNull AtomicReferenceArray<ReferenceEntry<K, V>> table;
        //如果key是弱引用的話,那么被GC回收后,就會放到ReferenceQueue,要根據(jù)這個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的引用,有幾種引用類型:

        我們拿StrongEntry為例,看看有哪些屬性:

        final K key;
        final int hash;
        //指向下一個Entry,說明這里用的鏈表(從上圖可以看出)
        final @Nullable ReferenceEntry<K, V> next;
        //value
        volatile ValueReference<K, V> valueReference = unset();

        源碼分析

        當我們了解了Guava Cache的結(jié)構(gòu)后,那么進行源碼分析就會簡單很多。

        本文只對put和get這兩個重點操作來進行源碼分析,其他源碼如果讀者感興趣請自行閱讀。

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

        get

        get主流程

        我們從LoadingCache的get(key)方法入手:

        //LocalLoadingCache的get方法,直接調(diào)用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 {
            //根據(jù)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則說明呀已經(jīng)有數(shù)據(jù)
                if (count != 0) {
                  //根據(jù)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);
                      //命中率統(tǒng)計
                      statsCounter.recordHits(1);
                      //判斷是否需要刷新,如果需要刷新,那么會去異步刷新,且返回舊值。scheduleRefresh下面展開
                      return scheduleRefresh(e, key, hash, value, now, loader);
                    }
                    
                    ValueReference<K, V> valueReference = e.getValueReference();
                    //如果entry過期了且數(shù)據(jù)還在加載中,則等待直到加載完成。這里的ValueReference是LoadingValueReference,其waitForValue方法是調(diào)用內(nèi)部的Future的get方法,具體讀者可以點進去看。
                    if (valueReference.isLoading()) {
                      return waitForLoadingValue(e, key, valueReference);
                    }
                  }
                }
                
                //重點方法。lockedGetOrLoad下面展開
                //走到這一步表示: 之前沒有寫入過數(shù)據(jù) || 數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中。
                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還沒過期,則會進入此方法,嘗試去刷新數(shù)據(jù)。

         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()) {
              //異步刷新數(shù)據(jù)。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,實質(zhì)是把對應(yīng)Entry的ValueReference替換為新建的LoadingValueReference。insertLoadingValueReference下面展開
            final LoadingValueReference<K, V> loadingValueReference =
                insertLoadingValueReference(key, hash, checkTime);
            if (loadingValueReference == null) {
              return null;
            }
            //通過loader異步加載數(shù)據(jù),這里返回的是Future。loadAsync下面展開
            ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
            //這里立即判斷Future是否已經(jīng)完成,如果是則返回結(jié)果。否則返回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對應(yīng)的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對應(yīng)的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,當數(shù)據(jù)加載完后的后續(xù)處理。
            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,則調(diào)用loader的load方法,這個load方法是同步的。
              //這里需要使用同步加載的原因是,在上面的“緩存失效機制”也說了,即使用異步,但是還沒有oldValue也是沒用的。如果在系統(tǒng)啟動時來高并發(fā)請求的話,那么所有的請求都會阻塞,所以給熱點數(shù)據(jù)預(yù)加熱是很有必要的。
              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

        如果之前沒有寫入過數(shù)據(jù) || 數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中,則會調(diào)用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;
            //用來判斷是否需要創(chuàng)建一個新的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在加載中則不需要重復(fù)創(chuàng)建entry
                  if (valueReference.isLoading()) {
                    createNewEntry = false;
                  } else {
                    V value = valueReference.get();
                    //value為null說明已經(jīng)過期且被清理掉了
                    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方法的條件是數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中,但是在lock之前都有可能發(fā)生并發(fā),進而改變entry的狀態(tài),所以在上面中再次判斷了isLoading和isExpired。所以來到這步說明,原來數(shù)據(jù)是過期的且在加載中,lock的前一刻加載完成了,到了這步就有值了。
                      return value;
                    }
                    
                    writeQueue.remove(e);
                    accessQueue.remove(e);
                    this.count = newCount; // write-volatile
                  }
                  break;
                }
              }
              //創(chuàng)建一個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();
            }
         //同步加載數(shù)據(jù)。里面的方法都是在上面有提及過的,讀者可自行閱讀。
            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的主流程代碼,我們來畫一下流程圖:

        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);

              //根據(jù)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已經(jīng)過期且被回收或清理掉
                  if (entryValue == null) {
                    ++modCount;
                    if (valueReference.isActive()) {
                      enqueueNotification(
                          key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
                      //設(shè)值
                      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,設(shè)值
                    ++modCount;
                    enqueueNotification(
                        key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
                    setValue(e, key, value, now);
                    evictEntries(e);
                    return entryValue;
                  }
                }
              }
              
              //沒有找到entry,則新建一個Entry并設(shè)值
              ++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來說沒有那么復(fù)雜。

        最佳實踐

        關(guān)于最佳實踐,在上面的“緩存失效機制”中得知,看來使用refreshAfterWrite是一個不錯的選擇,但是從上面get的源碼分析和流程圖看出,或者了解Guava Cache都知道,Guava Cache是沒有定時器或額外的線程去做清理或加載操作的,都是通過get來觸發(fā)的,目的是降低復(fù)雜性和減少對系統(tǒng)的資源消耗。

        那么只使用refreshAfterWrite或配置不當?shù)脑?,會帶來一個問題:如果一個key很長時間沒有訪問,這時來一個請求的話會返回舊值,這個好像不是很符合我們的預(yù)想,在并發(fā)下返回舊值是為了不阻塞,但是在這個場景下,感覺有足夠的時間和資源讓我們?nèi)ニ⑿聰?shù)據(jù)。

        結(jié)合get的流程圖,在get的時候,是先判斷過期,再判斷refresh,即如果過期了會優(yōu)先調(diào)用 load 方法(阻塞其他線程),在不過期情況下且過了refresh時間才去做 reload (異步加載,同時返回舊值),所以推薦的設(shè)置是 refresh < expire,這個設(shè)置還可以解決一個場景就是,如果長時間沒有訪問緩存,可以保證 expire 后可以取到最新的值,而不是因為 refresh 取到舊值。

        用一張時間軸圖簡單表示:

        總結(jié)

        Guava Cache是一個很優(yōu)秀的本地緩存工具,緩存的作用不多說,一個簡單易用,功能強大的工具會使你在開發(fā)中事倍功半。但是跟所有的工具一樣,你要在了解其內(nèi)部原理、機制的情況下,才能發(fā)揮其最大的功效,才能適用到你的業(yè)務(wù)場景中。

        本文通過對Guava Cache的使用、核心機制的講解、核心源代碼的分析以及最佳實踐的說明,相信你會對Guava Cache有更進一步的了解。

        來源:albenw.github.io/posts/df42dc84

        覺得不錯,請點個在看

        瀏覽 46
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 五月亚洲无码 | 一二三区欧美日韩人妻在线 | 欧美三级性爱视频 | 国产女人十八毛片十八精品 | 欧美黑人性猛交 |