《深入分散式快取 》第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
先貼一下官網:
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可以把快取設定為允許垃圾回收:
主動刪除:
- 單個刪除 individually, using
Cache.invalidate(key)
- 批量刪除 in bulk, using
Cache.invalidateAll(keys)
- 刪除所有資料 to all entries, using
Cache.invalidateAll()
書上的併發場景使用:多個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
可以看出第一次是模擬呼叫服務,後面的都是從快取獲取。