1. 程式人生 > >《深入分散式快取 》第4章Ehcache 與guava cache

《深入分散式快取 》第4章Ehcache 與guava cache

一 序

   本文屬於《深入分散式快取 》讀書筆記,第一章:快取為王主要介紹快取概念,以及引入快取的背景:提升使用者體驗。還介紹了快取的分類,第二章主要介紹分散式理論。個人覺得第二章可以去掉,畢竟是泛泛的介紹。還是專門去看有主題的書比較好,比如《<從PAXOS到ZOOKEEPER分散式一致性原理與實踐》。第4章主要介紹EHcache。因為實際專案採用了guava +redis. 所以本文打算重點看看guava cache。

二 Ehcache 

  先說下官網:http://www.ehcache.org/ 貌似需要翻牆。

EhCache 是一個純Java的程序內快取框架,具有快速、精幹等特點,是Hibernate中預設CacheProvider。Ehcache是一種廣泛使用的開源Java分散式快取。主要面向通用快取,Java EE和輕量級容器。它具有記憶體和磁碟儲存,快取載入器,快取擴充套件,快取異常處理程式,一個gzip快取servlet過濾器,支援REST和SOAP api等特點。

    Spring 提供了對快取功能的抽象:即允許繫結不同的快取解決方案(如Ehcache),但本身不直接提供快取功能的實現。它支援註解方式使用快取,非常方便。

 Ehcache的特點:

  • 快速
  • 簡單
  • 多種快取策略
  • 快取資料有兩級:記憶體和磁碟,因此無需擔心容量問題
  • 快取資料會在虛擬機器重啟的過程中寫入磁碟
  • 可以通過RMI、可插入API等方式進行分散式快取
  • 具有快取和快取管理器的偵聽介面
  • 支援多快取管理器例項,以及一個例項的多個快取區域
  • 提供Hibernate的快取實現

適用場景:

1 資料更新比較少的情況,

2 對併發、一致性要求不高的情況。本地快取的特性,不適合解決不同伺服器間快取同步的問題,更推薦redis等集中式快取。

優化方案:1 定時輪詢。2.主動通知。

三 guava cache

先貼一下官網:

https://github.com/google/guava/wiki/CachesExplained

  3.1 jvm快取

  就是堆快取。其實就是建立一些全域性變數,如 Map、List 之類的容器用於存放資料。
優點:簡單,佔用記憶體小。
缺點:不具備快取的常見操作,如:
只能顯式的寫入,清除資料。
不能按照一定的規則淘汰資料,如 LRU,LFU,FIFO 等。
清除資料時的回撥通知。
其他一些定製功能等。

   3.2  guava cache試用場景

Guava 的 Cache跟ehcache 一樣也是對內快取,適用單節點使用。

  • 你願意消耗一些記憶體空間來提升速度。
  • 你預料到某些鍵會被查詢一次以上。
  • 快取中存放的資料總量不會超出記憶體容量。(Guava Cache是單個應用執行時的本地快取。它不把資料存放到檔案或外部伺服器。如果這不符合你的需求,請嘗試Memcached這類工具)

3.3 guava cache的建立方式

  • CacheLoader
  • Callable callback

     使用快取前,首先問自己一個問題:有沒有合理的預設方法來載入或計算與鍵關聯的值?如果有的話,你應當使用CacheLoader。如果沒有,或者你想要覆蓋預設的載入運算,同時保留"獲取快取-如果沒有-則計算"[get-if-absent-compute]的原子語義,你應該在呼叫get時傳入一個Callable例項。快取元素也可以通過Cache.put方法直接插入,但自動載入是首選的,因為它可以更容易地推斷所有快取內容的一致性。

   本篇後面例子將以cacheloader方式。

3.4 快取資料的刪除

    Guava provides three basic types of eviction: size-based eviction, time-based eviction, and reference-based eviction

基於容量回收、定時回收和基於引用回收。

基於容量回收(size-based eviction:用CacheBuilder.maximumSize(long)

定時回收(Timed Eviction):CacheBuilder提供兩種定時回收的方法:

  • expireAfterAccess(long, TimeUnit):快取項在給定時間內沒有被讀/寫訪問,則回收。請注意這種快取的回收順序和基於大小回收一樣。
  • expireAfterWrite(long, TimeUnit):快取項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。如果認為快取資料總是在固定時候後變得陳舊不可用,這種回收方式是可取的。

基於引用的回收(Reference-based Eviction)

通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把快取設定為允許垃圾回收:

主動刪除:

書上的併發場景使用:多個key併發請求的case,為了緩解後端壓力,對同一個key只允許一個請求去回源獲取資料,其他請求阻塞等待結果。

四 demo

   需求:維護一個已開通服務的城市列表。從業務場景來說,讀資料是遠大於變更的,通常新開通城市會以天為單位。

所以快取到本地就行,使用guava cache適合。

city pojo

package com.daojia.guava.cache;

import java.io.Serializable;

public class City implements Serializable {

	private static final long serialVersionUID = 1L;

	private int id;
	
	private String name;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}	
	
}

 cache util

package com.daojia.guava.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;



public class CacheUtil {

	  private static final String CACHE_KEY = "LocalCache";
	  
	  private LoadingCache<String, Map<Integer, City>> cache =
              CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Map<Integer, City>>() {
                  @Override
                  public Map<Integer, City> load(String key) throws Exception {
                	  System.out.println("load city from service:");
                      List<City> allCity = new ArrayList<City>();
                      City citytmp = new City();
                      citytmp.setId(1);
                      citytmp.setName("beijing");
                      allCity.add(citytmp);
                      citytmp = new City();
                      citytmp.setId(2);
                      citytmp.setName("qingdao");                      
                      
                      Map<Integer, City> cityMap = new HashMap<>();
                      for (City city : allCity) {
                          cityMap.put(city.getId(), city);
                      }
                      return cityMap;
                  }
              });

      public void refresh() {
          cache.refresh(CACHE_KEY);
      }

      
      private Map<Integer, City> getCityMap() {
          try {
			return cache.get(CACHE_KEY);
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
      }

      public Collection<City> getAllCity() {
          return getCityMap().values();
      }

      public City getCityById(int cityId) {
          return getCityMap().get(cityId);
      }

      public String getCityName(int cityId) {
          City city = getCityById(cityId);
          return null == city ? "" : city.getName();
      }	
	
}

測試類:

package com.daojia.guava.cache;

public class CacheTest {

	public static void main(String[] args) {		

		CacheUtil cacheUtil = new CacheUtil();

		for (int i = 0; i < 3; i++) {
			System.out.println("vist num--- " + i + " ---");
			String cname = cacheUtil.getCityName(1);
			System.out.println("name:" + cname);
		}

	}

}

結果輸出:

vist num--- 0 ---
load city from service:
name:beijing
vist num--- 1 ---
name:beijing
vist num--- 2 ---
name:beijing

可以看出第一次是模擬呼叫服務,後面的都是從快取獲取。