緩存技術內部交流_01_Ehcache3簡介


參考資料:
http://www.ehcache.org/documentation/3.2/getting-started.html
http://www.ehcache.org/documentation/3.2/eviction-advisor.html

示例代碼:
https://github.com/gordonklg/study,cache module

A. HelloWorld

gordon.study.cache.ehcache3.basic.HelloWorld.java

public class HelloWorld {

    public static void main(String[] args) {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
        cacheManager.init();
        Cache<Long, String> myCache = cacheManager.createCache("myCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(10)));
        myCache.put(1L, "Hello World!");
        String value = myCache.get(1L);
        System.out.println(value);
        cacheManager.close();
    }
}

代碼第4行通過 Builder 模式創建了一個不可變的 CacheManager,用於管理所有的 Cache 及相關 Service。接着調用 init() 方法初始化 CacheManager。

代碼第6行創建了一個 Cache,名字為 myCache。通過 CacheConfigurationBuilder 構建出的 CacheConfiguration 限制 myCache 只能接受 key 類型為 Long,value 類型為 String 的數據,同時 ResourcePoolsBuilder 構建出的 ResourcePools 決定了 myCache 可以使用 JVM 堆內存緩存最多10條數據。

代碼第8行使用 myCache 緩存了一條數據,第9行從 myCache 中讀取緩存數據。

代碼第10行關閉 CacheManager,這會順帶着關閉所有的 Cache。

B. Cache 配置項

Cache 的特性通過 CacheConfigurationBuilder 創建出的 CacheConfiguration 決定。通過 CacheConfiguration 接口定義,可以看出 Cache 支持以下特性:

  • key type,設定允許的 key 類型,默認為 Object, 類型不匹配會拋出 ClassCastException
  • value type,設定允許的 value 類型,默認為 Object, 類型不匹配會拋出 ClassCastException
  • resource pools,設定緩存區的空間大小
  • expiry,設定過期策略
  • eviction advisor,調整回收策略

resource pools 設定緩存區的空間大小,Ehcache 總共有4種緩存區,分別為:

  • heap,堆內存,最常用,目前團隊只要掌握這種就可以了
  • offheap,堆外內存,優勢是不用GC
  • disk,磁盤空間,可以持久化
  • cluster,Ehcahce 已經支持集群了,具體內容不詳

對於堆內存,resource pools 指定了 Cache 實例可以存多少條目以及最大占用多大內存空間。

過期策略有三種:

  • 不過期,條目一直存在於緩存中,除非顯式從緩存中 remove,或者因回收策略被移除
  • TTL,time to live,指定條目的存活時間
  • TTI,time to idle,條目在指定時間段內未被使用,則過期

默認回收策略比較復雜,大致可視為 LRU 算法(最長時間未被訪問的條目優先被回收)。可以通過 EvictionAdvisor 接口調整回收策略

示例代碼如下:
gordon.study.cache.ehcache3.basic.CacheFeatures.java

public class CacheFeatures {
 
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static void main(String[] args) throws Exception {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
        CacheConfiguration config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(Integer.class, String.class, ResourcePoolsBuilder.heap(5))
                .withEvictionAdvisor(new TopThreeAreImportantEvictionAdvisor())
                .withExpiry(Expirations.timeToIdleExpiration(Duration.of(3, TimeUnit.SECONDS))).build();
        Cache myCache = cacheManager.createCache("myCache", config);
        try { // key type 錯誤
            myCache.put("Invalid key type", "sad");
        } catch (Exception e) {
            System.out.println("Invalid key type");
        }
        try { // value type 錯誤
            myCache.put(1L, 9527L);
        } catch (Exception e) {
            System.out.println("Invalid value type");
        }
        for (int i = 1; i <= 10; i++) { // 超出數量上限,回收策略開始生效:key為1、2、3的條目不被回收
            myCache.put(i, "No. " + i);
        }
        printAllCacheEntries(myCache);
 
        System.out.println("-------------------------------");
        for (int i = 0; i < 3; i++) { // 等待3秒,3秒內一直沒被使用的條目全部過期移除。只有key為1的條目還在。
            myCache.get(1);
            Thread.sleep(1050);
        }
        printAllCacheEntries(myCache);
    }
 
    private static void printAllCacheEntries(Cache<Integer, String> cache) {
        cache.forEach(new Consumer<Cache.Entry<Integer, String>>() {
 
            @Override
            public void accept(Cache.Entry<Integer, String> entry) {
                System.out.println(entry.getKey());
            }
        });
    }
 
    private static class TopThreeAreImportantEvictionAdvisor implements EvictionAdvisor<Integer, String> {
 
        @Override
        public boolean adviseAgainstEviction(Integer key, String value) {
            return (key.intValue() < 4);
        }
    }
}

代碼第8行將回收策略設定為自定義的 TopThreeAreImportantEvictionAdvisor 回收策略,即 key 值小於4的條目盡量不回收。第24行打印的所有 Cache 條目證明了key為1、2、3的條目沒有被回收。EvictionAdvisor 實現類的 adviseAgainstEviction 回調方法返回 true 表示不建議回收該條目,返回 false 表示可以回收。

代碼第9行將過期策略設定為 TTI,3秒未被訪問的條目會過期。第31行的打印結果證明了只有key為1的條目由於一直被訪問還存在於 Cache 中。

C. 回收算法略讀

Ehcache 默認回收算法是一種近似 LRU 算法,Ehcache 並沒有將所有的條目按照最后訪問時間放在一個有序的Map中,或者利用額外的數據結構來完成排序,而是通過遍歷的方式在一個小范圍內尋找 LRU 條目。回收的觸發時機是在新的條目需要放置到 Cache 中時。

Cache 類的 Store store 屬性用於 Cache 的后端存儲實現。在本例中,store 類型為 OnHeapStore。對 Cache 類的 put 操作實際上就是對 OnHeapStore 的 put 操作。

OnHeapStore 的 put 操作會先創建條目,放入內部的 Map 中(本質上是 ConcurrentHashMap),然后判斷當前條目數量是否超過允許的條目上限。如果超過上限,則先按照指定的 EvictionAdvisor 尋找一條可回收條目,如果找不到,則無視 EvictionAdvisor 再次尋找一條可回收條目(因為可能 Cache 中所有條目都置為不建議回收)。這段邏輯由 OnHeapStore 的 evict 方法實現(Line 1605)

    @SuppressWarnings("unchecked")
    Map.Entry<K, OnHeapValueHolder<V>> candidate = map.getEvictionCandidate(random, SAMPLE_SIZE, EVICTION_PRIORITIZER, EVICTION_ADVISOR);
 
    if (candidate == null) {
      // 2nd attempt without any advisor
      candidate = map.getEvictionCandidate(random, SAMPLE_SIZE, EVICTION_PRIORITIZER, noAdvice());
    }

尋找可回收條目的邏輯略復雜。宏觀上說,是遍歷 Cache 中的條目,分析最多8條可以回收的條目,選中 lastAccessTime 值最小的那個條目。時間對比通過 OnHeapStore 類中預先定義好的 Comparator EVICTION_PRIORITIZER 完成。顯然,這並不是 LRU,只是小范圍中的 LRU

  private static final Comparator<ValueHolder<?>> EVICTION_PRIORITIZER = new Comparator<ValueHolder<?>>() {
    @Override
    public int compare(ValueHolder<?> t, ValueHolder<?> u) {
      if (t instanceof Fault) {
        return -1;
      } else if (u instanceof Fault) {
        return 1;
      } else {
        return Long.signum(u.lastAccessTime(TimeUnit.NANOSECONDS) - t.lastAccessTime(TimeUnit.NANOSECONDS));
      }
    }
  };

由於算法的特性,所以遍歷 Cache 條目需要從一個隨機點開始以優化算法的整體准確性。getEvictionCandidate 方法從一個隨機起始點開始遍歷 ConcurrentHashMap,分析可回收條目,如果分析完8個條目,則返回最長時間未被訪問的條目;如果從隨機起始點到最后一個條目之間不滿8個可回收條目,則調用 getEvictionCandidateWrap 方法從0開始遍歷 ConcurrentHashMap,直到分析完8個條目(包含getEvictionCandidate 方法中分析的條目)或到達上面的隨機開始點為止(相當於遍歷了全部的 Cache 條目),最終返回最長時間未被訪問的條目。

按照這個邏輯,如果 Cache 空間已滿且全是不建議回收條目,這時如果插入可回收條目,該條目立即會被回收;如果插入不建議回收條目,則隨機回收一條。見下例:

gordon.study.cache.ehcache3.basic.CacheFeatures_Eviction.java

public class CacheFeatures_Eviction {
 
    private static final int CACHE_SIZE = 50;
 
    public static void main(String[] args) throws Exception {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
        CacheConfiguration<Integer, String> config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(Integer.class, String.class, ResourcePoolsBuilder.heap(50))
                .withEvictionAdvisor(new MyEvictionAdvisor()).build();
        Cache<Integer, String> myCache = cacheManager.createCache("myCache", config);
        for (int i = 1; i <= CACHE_SIZE; i++) {
            myCache.put(i, "No. " + i);
            Thread.sleep(10);
        }
        myCache.put(CACHE_SIZE + 1, "No. " + (CACHE_SIZE + 1));
 
        BitSet bitSet = new BitSet(CACHE_SIZE + 1);
        for (Cache.Entry<Integer, String> entry : myCache) {
            bitSet.set(entry.getKey() - 1);
        }
        System.out.println("Eviction number: " + (bitSet.nextClearBit(0) + 1));
    }
 
    private static class MyEvictionAdvisor implements EvictionAdvisor<Integer, String> {
 
        @Override
        public boolean adviseAgainstEviction(Integer key, String value) {
            return (key.intValue() <= CACHE_SIZE); // 最后一條是可回收條目,則必定回收這一條
            // return (key.intValue() <= (CACHE_SIZE + 1)); // 最后一條是不建議回收條目,則隨機回收一條
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM