緩存,在我們日常開發中是必不可少的一種解決性能問題的方法。簡單的說,cache 就是爲了提升系統性能而開闢的一塊內存空間。
緩存的主要作用是暫時在內存中保存業務系統的數據處理結果,並且等待下次訪問使用。在日常開發的很多場合,由於受限於硬盤IO的?
緩存在很多系統和架構中都用廣泛的應用,例如:
1.CPU緩存
2.操作系統緩存
3.本地緩存
4.分佈式緩存
5.HTTP緩存
6.數據庫緩存
等等,可以說在計算機和網絡領域,緩存無處不在。可以這麼說,只要有硬件性能不對等,涉及到網絡傳輸的地方都會有緩存的身影。
1.生成一個LoadingCache對象
LoadingCache userCache = CacheBuilder.newBuilder()
.maximumSize(10000))//設置緩存上線
.expireAfterAccess(10, TimeUnit.MINUTES)//設置時間對象沒有被讀/寫訪問則對象從內存中刪除
.expireAfterWrite(10, TimeUnit.MINUTES)//設置時間對象沒有被寫訪問則對象從內存中刪除
//移除監聽器,緩存項被移除時會觸發
.removalListener(new RemovalListener<String, UserProfile>() {
@Override
public void onRemoval(RemovalNotification<String, UserProfile> notification) {
//邏輯
}
}
})
.recordStats()
//CacheLoader類 實現自動加載
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) {
//從SQL或者NoSql 獲取對象
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
2.CacheBuilder方法
1) LoadingCache build(CacheLoader loader) : LoadingCache對象創建
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
}
- 1
- 2
- 3
- 4
- 5
2)CacheBuilder.maximumSize(long size)方法:配置緩存數量上限,快達到上限或達到上限,處理了時間最長沒被訪問過的對象或者根據配置的被釋放的對象
3)expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣
4)expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認爲緩存數據總是在固定時候後變得陳舊不可用,這種回收方式是可取的。
5)refreshAfterWrite(long duration, TimeUnit unit): 定時刷新,可以爲緩存增加自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新,即只有刷新間隔時間到了你再去get(key)纔會重新去執行Loading否則就算刷新間隔時間到了也不會執行loading操作。因此,如果你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存並不會因爲刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過期時間後也變得可以回收。還有一點比較重要的是refreshAfterWrite和expireAfterWrite兩個方法設置以後,重新get會引起loading操作都是同步串行的。這其實可能會有一個隱患,當某一個時間點剛好有大量檢索過來而且都有刷新或者回收的話,是會產生大量的請求同步調用loading方法,這些請求佔用線程資源的時間明顯變長。如正常請求也就20ms,當刷新以後加上同步請求loading這個功能接口可能響應時間遠遠大於20ms。爲了預防這種井噴現象,可以不設refreshAfterWrite方法,改用LoadingCache.refresh(K)因爲它是異步執行的,不會影響正在讀的請求,同時使用ScheduledExecutorService可以幫助你很好地實現這樣的定時調度,配上cache.asMap().keySet()返回當前所有已加載鍵,這樣所有的key定時刷新就有了。如果訪問量沒有這麼大則直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。這個可以評估自己的項目實際情況來決策。
統計相關:
CacheBuilder.recordStats()用來開啓Guava Cache的統計功能。統計打開後,Cache.stats()方法會返回CacheStats對象以提供如下統計信息:
hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時間,單位爲納秒;
evictionCount():緩存項被回收的總數,不包括顯式清除
此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。
asMap視圖
asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:
cache.asMap()包含當前所有加載到緩存的項。因此相應地,cache.asMap().keySet()包含當前所有已加載鍵;
asMap().get(key)實質上等同於cache.getIfPresent(key),而且不會引起緩存項的加載。這和Map的語義約定一致。
所有讀寫操作都會重置相關緩存項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會重置緩存項的讀取時間。
3.LoadingCache方法的使用
1)V get(K k): 內部調用getOrLoad(K key)方法,緩存中有對應的值則返回,沒有則使用CacheLoader load方法
getOrLoad(K key)方法爲線程安全方法,內部加鎖
2)V getIfPresent(Object key):緩存中有對應的值則返回,沒有則返回NULL
@Nullable
public V getIfPresent(Object key) {
int hash = hash(checkNotNull(key));
V value = segmentFor(hash).get(key, hash);
if (value == null) {
globalStatsCounter.recordMisses(1);
} else {
globalStatsCounter.recordHits(1);
}
return value;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
3)ImmutableMap getAll(Iterable keys) :提供一組keys篩選出符合條件的所有值。內部調用遍歷keys調用get(K key)方法獲得已經緩存的對象,沒有緩存的對象則通過調用CacheLoader.loadAll方法加載,如果沒實現loadAll方法則會拋出UnsupportedLoadingOperationException異常,處理這個異常最終會遍歷每個key通過lockedGetOrLoad(key, hash, loader)方法調用CacheLoader.load方法,實現加載成功
ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
int hits = 0;
int misses = 0;
Map<K, V> result = Maps.newLinkedHashMap();
Set<K> keysToLoad = Sets.newLinkedHashSet();
for (K key : keys) {
V value = get(key);
if (!result.containsKey(key)) {
result.put(key, value);
if (value == null) {
misses++;
keysToLoad.add(key);
} else {
hits++;
}
}
}
try {
if (!keysToLoad.isEmpty()) {
try {
Map<K, V> newEntries = loadAll(keysToLoad, defaultLoader);
for (K key : keysToLoad) {
V value = newEntries.get(key);
if (value == null) {
throw new InvalidCacheLoadException("loadAll failed to return a value for " + key);
}
result.put(key, value);
}
} catch (UnsupportedLoadingOperationException e) {
// loadAll not implemented, fallback to load
for (K key : keysToLoad) {
misses--; // get will count this miss
result.put(key, get(key, defaultLoader));
}
}
}
return ImmutableMap.copyOf(result);
} finally {
globalStatsCounter.recordHits(hits);
globalStatsCounter.recordMisses(misses);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
4) ImmutableMap getAll(Iterable keys): 提供一組keys篩選出符合條件緩存中存在的所有值
ImmutableMap<K, V> getAllPresent(Iterable<?> keys) {
int hits = 0;
int misses = 0;
Map<K, V> result = Maps.newLinkedHashMap();
for (Object key : keys) {
V value = get(key);
if (value == null) {
misses++;
} else {
// TODO(fry): store entry key instead of query key
@SuppressWarnings("unchecked")
K castKey = (K) key;
result.put(castKey, value);
hits++;
}
}
globalStatsCounter.recordHits(hits);
globalStatsCounter.recordMisses(misses);
return ImmutableMap.copyOf(result);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
5) long size() : 緩存對象數量
6)put(K key,V value): 直接顯示地向緩存中插入值,這會直接覆蓋掉已有鍵之前映射的值。
7)invalidate(Object key):顯式地清除指定key的緩存對象
public void invalidate(Object key) {
checkNotNull(key);
localCache.remove(key);
}
- 1
- 2
- 3
- 4
- 5
8) invalidateAll(Iterable keys) : 清除批量緩存對象
public void invalidateAll(Iterable<?> keys) {
localCache.invalidateAll(keys);
}
void invalidateAll(Iterable<?> keys) {
// TODO(fry): batch by segment
for (Object key : keys) {
remove(key);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
9)invalidateAll(): 清除所有緩存對象
public void invalidateAll() {
localCache.clear();
}
- 1
- 2
- 3
- 4
10) public void refresh(K key) :刷新指定key的緩存對象,刷新和回收不太一樣。刷新表示爲鍵加載新值,這個過程可以是異步的。在刷新操作進行時,緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。重載CacheLoader.reload可以擴展刷新時的行爲,這個方法允許開發者在計算新值時使用舊的值
11)ConcurrentMap asMap():獲取緩存數據轉換成Map類型
關於guava Cache數據移除:
guava做cache時候數據的移除方式,在guava中數據的移除分爲被動移除和主動移除兩種。
被動移除數據的方式,guava默認提供了三種方式:
1.基於大小的移除:看字面意思就知道就是按照緩存的大小來移除,如果即將到達指定的大小,那就會把不常用的鍵值對從cache中移除。
定義的方式一般爲 CacheBuilder.maximumSize(long),還有一種一種可以算權重的方法,個人認爲實際使用中不太用到。就這個常用的來看有幾個注意點,
其一,這個size指的是cache中的條目數,不是內存大小或是其他;
其二,並不是完全到了指定的size系統纔開始移除不常用的數據的,而是接近這個size的時候系統就會開始做移除的動作;
其三,如果一個鍵值對已經從緩存中被移除了,你再次請求訪問的時候,如果cachebuild是使用cacheloader方式的,那依然還是會從cacheloader中再取一次值,如果這樣還沒有,就會拋出異常
2.基於時間的移除:guava提供了兩個基於時間移除的方法
expireAfterAccess(long, TimeUnit) 這個方法是根據某個鍵值對最後一次訪問之後多少時間後移除
expireAfterWrite(long, TimeUnit) 這個方法是根據某個鍵值對被創建或值被替換後多少時間移除
3.基於引用的移除:
這種移除方式主要是基於java的垃圾回收機制,根據鍵或者值的引用關係決定移除
主動移除數據方式,主動移除有三種方法:
1.單獨移除用 Cache.invalidate(key)
2.批量移除用 Cache.invalidateAll(keys)
3.移除所有用 Cache.invalidateAll()
如果需要在移除數據的時候有所動作還可以定義Removal Listener,但是有點需要注意的是默認Removal Listener中的行爲是和移除動作同步執行的,如果需要改成異步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)