<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Guava Cache 實(shí)現(xiàn)原理及最佳實(shí)踐

          共 43693字,需瀏覽 88分鐘

           ·

          2021-06-24 19:04

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

          概要

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

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

          應(yīng)用及使用

          應(yīng)用場景

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

          基本使用

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

          CacheLoader

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

          //創(chuàng)建一個(gè)LoadingCache,并可以進(jìn)行一些簡單的緩存配置
          private static LoadingCache<String, String > loadingCache = CacheBuilder.newBuilder()
                      //最大容量為100(基于容量進(jìn)行回收)
                      .maximumSize(100)
                      //配置寫入后多久使緩存過期-下文會(huì)講述
                      .expireAfterWrite(150, TimeUnit.SECONDS)
                      //配置寫入后多久刷新緩存-下文會(huì)講述
                      .refreshAfterWrite(1, TimeUnit.SECONDS)
                      //key使用弱引用-WeakReference
                      .weakKeys()
                      //當(dāng)Entry被移除時(shí)的監(jiān)聽器
                      .removalListener(notification -> log.info("notification={}", GsonUtil.toJson(notification)))
                      //創(chuàng)建一個(gè)CacheLoader,重寫load方法,以實(shí)現(xiàn)"當(dāng)get時(shí)緩存不存在,則load,放到緩存,并返回"的效果
                      .build(new CacheLoader<String, String>() {
                         //重點(diǎn),自動(dòng)寫緩存數(shù)據(jù)的方法,必須要實(shí)現(xiàn)
                          @Override
                          public String load(String key) throws Exception {
                              return "value_" + key;
                          }
                         //異步刷新緩存-下文會(huì)講述
                          @Override
                          public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                              return super.reload(key, oldValue);
                          }
                      });

              @Test
              public void getTest() throws Exception {
                 //測試?yán)樱{(diào)用其get方法,cache會(huì)自動(dòng)加載并返回
                  String value = loadingCache.get("1");
                 //返回value_1
                  log.info("value={}", value);
              }

          Callable

          在上面的build方法中是可以不用創(chuàng)建CacheLoader的,不管有沒有CacheLoader,都是支持Callable的。Callable在get時(shí)可以指定,效果跟CacheLoader一樣,區(qū)別就是兩者定義的時(shí)間點(diǎn)不一樣,Callable更加靈活,可以理解為Callable是對(duì)CacheLoader的擴(kuò)展。例子代碼如下:

          @Test
          public void callableTest() throws Exception {
              String key = "1";
              //loadingCache的定義跟上一面一樣
              //get時(shí)定義一個(gè)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() 方法,手動(dòng)使緩存失效。

          緩存失效機(jī)制

          Guava Cache有一套十分優(yōu)秀的緩存失效機(jī)制,這里主要介紹的是基于時(shí)間的失效回收。

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

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

          一般用法

          expireAfterWrite/expireAfterAccess

          使用背景

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

          做法

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

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

          refreshAfterWrite

          使用背景

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

          做法

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

          異步刷新

          使用背景

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

          做法

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

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

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

          總結(jié)

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

          expireAfterWrite 是允許一個(gè)線程進(jìn)去load方法,其他線程阻塞等待。

          refreshAfterWrite 是允許一個(gè)線程進(jìn)去load方法,其他線程返回舊的值。

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

          實(shí)現(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的核心類。看看LocalCache的定義:

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

          說明Guava Cache本質(zhì)就是一個(gè)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的強(qiáng)度,即引用類型的強(qiáng)弱
           final Strength keyStrength;
           //value的強(qiáng)度,即引用類型的強(qiáng)弱
           final Strength valueStrength;
           //訪問后的過期時(shí)間,設(shè)置了expireAfterAccess就有
           final long expireAfterAccessNanos;
           //寫入后的過期時(shí)間,設(shè)置了expireAfterWrite就有
           final long expireAfterWriteNa就有nos;
           //刷新時(shí)間,設(shè)置了refreshAfterWrite就有
           final long refreshNanos;
           //removal的事件隊(duì)列,緩存過期后先放到該隊(duì)列
           final Queue<RemovalNotification<K, V>> removalNotificationQueue;
           //設(shè)置的removalListener
           final RemovalListener<K, V> removalListener;
           //時(shí)間器
           final Ticker ticker;
           //創(chuàng)建Entry的工廠,根據(jù)引用類型不同
           final EntryFactory entryFactory;

          Segment

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

          static class Segment<K, V> extends ReentrantLock

          看看Segment的重要屬性:

          //LocalCache
          final LocalCache<K, V> map;
          //segment存放元素的數(shù)量
          volatile int count;
          //修改、更新的數(shù)量,用來做弱一致性
          int modCount;
          //擴(kuò)容用
          int threshold;
          //segment維護(hù)的數(shù)組,用來存放Entry。這里使用AtomicReferenceArray是因?yàn)橐肅AS來保證原子性
          volatile @MonotonicNonNull AtomicReferenceArray<ReferenceEntry<K, V>> table;
          //如果key是弱引用的話,那么被GC回收后,就會(huì)放到ReferenceQueue,要根據(jù)這個(gè)queue做一些清理工作
          final @Nullable ReferenceQueue<K> keyReferenceQueue;
          //跟上同理
          final @Nullable ReferenceQueue<V> valueReferenceQueue;
          //如果一個(gè)元素新寫入,則會(huì)記到這個(gè)隊(duì)列的尾部,用來做expire
          @GuardedBy("this")
          final Queue<ReferenceEntry<K, V>> writeQueue;
          //讀、寫都會(huì)放到這個(gè)隊(duì)列,用來進(jìn)行LRU替換算法
          @GuardedBy("this")
          final Queue<ReferenceEntry<K, V>> accessQueue;
          //記錄哪些entry被訪問,用于accessQueue的更新。
          final Queue<ReferenceEntry<K, V>> recencyQueue;

          ReferenceEntry

          ReferenceEntry就是一個(gè)Entry的引用,有幾種引用類型:

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

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

          源碼分析

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

          本文只對(duì)put和get這兩個(gè)重點(diǎn)操作來進(jìn)行源碼分析,其他源碼如果讀者感興趣請(qǐng)自行閱讀。

          以下源碼基于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定位到是哪個(gè)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 {
                  //這里是進(jìn)行快速判斷,如果count != 0則說明呀已經(jīng)有數(shù)據(jù)
                  if (count != 0) {
                    //根據(jù)hash定位到table的第一個(gè)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)計(jì)
                        statsCounter.recordHits(1);
                        //判斷是否需要刷新,如果需要刷新,那么會(huì)去異步刷新,且返回舊值。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方法,具體讀者可以點(diǎn)進(jìn)去看。
                      if (valueReference.isLoading()) {
                        return waitForLoadingValue(e, key, valueReference);
                      }
                    }
                  }
                  
                  //重點(diǎn)方法。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,用當(dāng)前時(shí)間跟entry的accessTime比較
                if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
                  return true;
                }
                //如果配置了expireAfterWrite,用當(dāng)前時(shí)間跟entry的writeTime比較
                if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
                  return true;
                }
                return false;
              }

          scheduleRefresh

          從get的流程得知,如果entry還沒過期,則會(huì)進(jìn)入此方法,嘗試去刷新數(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判斷是否達(dá)到刷新的時(shí)間
              //3、是否在加載中,如果是則沒必要再進(jìn)行刷新
              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插入一個(gè)LoadingValueReference,實(shí)質(zhì)是把對(duì)應(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。因?yàn)槭强赡芊祷豬mmediateFuture或者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方法。
            //這個(gè)方法雖然看上去有點(diǎn)長,但其實(shí)挺簡單的,如果你熟悉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對(duì)應(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();
                    //如果是在加載中,或者還沒達(dá)到刷新時(shí)間,則返回null
                    //這里對(duì)這個(gè)判斷再進(jìn)行了一次,我認(rèn)為是上鎖lock了,再重新獲取now,對(duì)時(shí)間的判斷更加準(zhǔn)確
                    if (valueReference.isLoading()
                        || (checkTime && (now - e.getWriteTime() < map.refreshNanos))) {
                      
                      return null;
                    }
                    
                    //new一個(gè)LoadingValueReference,然后把entry的valueReference替換掉。
                    ++modCount;
                    LoadingValueReference<K, V> loadingValueReference =
                        new LoadingValueReference<>(valueReference);
                    e.setValueReference(loadingValueReference);
                    return loadingValueReference;
                  }
                }
                
                ////如果key對(duì)應(yīng)的entry不存在,則新建一個(gè)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);
              //對(duì)ListenableFuture添加listener,當(dāng)數(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();
                //這個(gè)oldValue指的是插入LoadingValueReference之前的ValueReference,如果entry是新的,那么oldValue就是unset,即get返回null。
                V previousValue = oldValue.get();
                //這里要注意***
                //如果上一個(gè)value為null,則調(diào)用loader的load方法,這個(gè)load方法是同步的。
                //這里需要使用同步加載的原因是,在上面的“緩存失效機(jī)制”也說了,即使用異步,但是還沒有oldValue也是沒用的。如果在系統(tǒng)啟動(dòng)時(shí)來高并發(fā)請(qǐng)求的話,那么所有的請(qǐng)求都會(huì)阻塞,所以給熱點(diǎn)數(shù)據(jù)預(yù)加熱是很有必要的。
                if (previousValue == null) {
                  V newValue = loader.load(key);
                  return set(newValue) ? futureValue : Futures.immediateFuture(newValue);
                }
                //否則,使用reload進(jìn)行異步加載
                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ù)不是在加載中,則會(huì)調(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;
              //用來判斷是否需要?jiǎng)?chuàng)建一個(gè)新的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
                        //來到這步,是不是覺得有點(diǎn)奇怪,我們分析一下: 
                        //進(jìn)入lockedGetOrLoad方法的條件是數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中,但是在lock之前都有可能發(fā)生并發(fā),進(jìn)而改變entry的狀態(tài),所以在上面中再次判斷了isLoading和isExpired。所以來到這步說明,原來數(shù)據(jù)是過期的且在加載中,lock的前一刻加載完成了,到了這步就有值了。
                        return value;
                      }
                      
                      writeQueue.remove(e);
                      accessQueue.remove(e);
                      this.count = newCount; // write-volatile
                    }
                    break;
                  }
                }
                //創(chuàng)建一個(gè)Entry,且set一個(gè)新的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選項(xiàng)則返回舊值
                      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,則新建一個(gè)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的流程相對(duì)get來說沒有那么復(fù)雜。

          最佳實(shí)踐

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

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

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

          用一張時(shí)間軸圖簡單表示:

          總結(jié)

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

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

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

          覺得不錯(cuò),請(qǐng)點(diǎn)個(gè)在看

          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲精品视频无码 | 毛片学生姝| 亚洲三级免费 | 国产精品人妻人伦a 6 2v久软件 特级西西444www无码视频免费看 | 伊人啪啪网 |