什麼是緩存?
在計算中,緩存是一個高速數據存儲層,其中存儲了數據子集,且通常是短暫性存儲,這樣日後在此請求此數據時,速度要比訪問數據的主存儲位置快。通過緩存,你可以高效地重用之前檢索或計算的數據。
爲什麼要用緩存
自研究Java內存緩存
場景
在Java應用中,對於訪問頻率高,更新少的數據,通常的方案是將這類數據加入緩存中。相對從數據庫中讀取來說,讀緩存的銷量會有很大提升。
在集羣環境下,常用的分佈式緩存有Redis、Memcached等。但在某些業務場景上,可能不需要去搭建一套複雜的分佈式緩存系統,在單機環境下,通常是會希望使用內部的緩存(LocalCache)。
方案
- 基於JSR107規範自研
- 基於ConcurrentHashMap實現數據緩存
實際代碼演示
Pojo:
public class User implements Serializable {
private String userName;
private String userId;
public User(String userName, String userId) {
this.userName = userName;
this.userId = userId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
@Override
public String toString() {
return userId + " --- " + userName;
}
}
// 用map實現一個簡單的緩存功能
public class MapCacheDemo {
// 我使用了 ConcurrentHashMap,線程安全的要求。
//我使用SoftReference <Object> 作爲映射值,因爲軟引用可以保證在拋出OutOfMemory之前,如果缺少內存,將刪除引用的對象。
//在構造函數中,我創建了一個守護程序線程,每5秒掃描一次並清理過期的對象。
private static final int CLEAN_UP_PERIOD_IN_SEC = 5;
private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();
public MapCacheDemo() {
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(CacheObject::isExpired).orElse(false));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
}
}
public void remove(String key) {
cache.remove(key);
}
public Object get(String key) {
return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);
}
public void clear() {
cache.clear();
}
public long size() {
return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();
}
// 緩存對象value
private static class CacheObject {
private Object value;
private long expiryTime;
private CacheObject(Object value, long expiryTime) {
this.value = value;
this.expiryTime = expiryTime;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
}
谷歌Guava緩存
Guava Cache介紹
Guava Cache代碼演示
導入Maven包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
代碼演示:
// https://github.com/google/guava
public class GuavaCacheDemo {
public static void main(String[] args) throws ExecutionException {
//緩存接口這裏是LoadingCache,LoadingCache在緩存項不存在時可以自動加載緩存
LoadingCache<String, User> userCache
//CacheBuilder的構造函數是私有的,只能通過其靜態方法newBuilder()來獲得CacheBuilder的實例
= CacheBuilder.newBuilder()
//設置併發級別爲8,併發級別是指可以同時寫緩存的線程數
.concurrencyLevel(8)
//設置寫緩存後8秒鐘過期
.expireAfterWrite(8, TimeUnit.SECONDS)
//設置寫緩存後1秒鐘刷新
.refreshAfterWrite(1, TimeUnit.SECONDS)
//設置緩存容器的初始容量爲10
.initialCapacity(10)
//設置緩存最大容量爲100,超過100之後就會按照LRU最近雖少使用算法來移除緩存項
.maximumSize(100)
//設置要統計緩存的命中率
.recordStats()
//設置緩存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " 被移除了,原因: " + notification.getCause());
}
})
//build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實現自動加載緩存
.build(
new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
System.out.println("緩存沒有時,從數據庫加載" + key);
// TODO jdbc的代碼~~忽略掉
return new User("tony" + key, key);
}
}
);
// 第一次讀取
for (int i = 0; i < 20; i++) {
User user = userCache.get("uid" + i);
System.out.println(user);
}
// 第二次讀取
for (int i = 0; i < 20; i++) {
User user = userCache.get("uid" + i);
System.out.println(user);
}
System.out.println("cache stats:");
//最後打印緩存的命中率等 情況
System.out.println(userCache.stats().toString());
}
}
結語
建議使用現成的工具包Guava。