前言
使用緩存已經是開發中老生常談的一件事了,常用專門處理緩存的工具比如Redis、MemCache等,但是有些時候可能需要一些簡單的緩存處理,沒必要用上這種專門的緩存工具,那麼自己寫一個緩存類最合適不過了。
一、分析
首先分析一下緩存類該如何設計,這裏我以一種非常簡單的方式來實現一個緩存類,這也是我一直以來使用的設計方案。
爲了明確功能,首先定義一個接口類CacheInt,然後是緩存實現的工具類CacheUtil。然後再看其中的功能,爲了存取方便,緩存應是以鍵值對的形式存取,爲了適應更多的場景,所以在存取的時候可以加一個緩存過期時間,然後再加上其他常見的添加、獲取、刪除、緩存大小、是否存在key、清理過期緩存等方法,整個緩存工具的方法差不多就是這些。
緩存類需要注意的問題:
- 緩存對象應該是唯一的,也就是單例的;
- 緩存的操作方法要同步,在多線程併發條件下防止出錯;
- 緩存的容器應該具有較高的併發性能,ConcurrentHashMap是一個不錯的選擇。
二、具體實現
1. CacheInt接口的定義
CacheInt接口的定義如下:
public interface CacheInt {
/**
* 存入緩存,此過程始終會清除過期緩存
* @param key 鍵
* @param value 值
* @param expire 過期時間,單位秒,如果小於1則表示長期保存
*/
void put(String key, Object value, int expire);
/**
* 獲取緩存,1/3的概率清除過期緩存
* @param key 鍵
* @return Object對象
*/
Object get(String key);
/**
* 獲取緩存大小
* @return int
*/
int size();
/**
* 是否存在緩存
* @param key 鍵
* @return boolean
*/
boolean isContains(String key);
/**
* 獲取所有緩存的鍵
* @return Set集合
*/
Set<String> getAllKeys();
/**
* 刪除某個緩存
* @param key 鍵
*/
void remove(String key);
}
2. CacheUtil的具體實現
緩存實現的核心就是CacheUtil,下面結合註釋進行說明,爲了避免文章篇幅冗雜,以下截圖就是完整源碼截圖,並且保持先後順序。
首先是類定義和其屬性定義,其中本類實例對象用volatile進行修飾提高可見性,初始化緩存容量用於初始化ConcurrentHashMap緩存容器的大小,此大小根據實際應用場景進行優化。
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* Created in 13:24 2019/7/13
*/
public class CacheUtil implements CacheInt{
// 本類實例對象,單例
private static volatile CacheUtil instance;
// 初始化緩存容量
private static final int CACHE_INITIAL_SIZE = 8;
// 緩存容器定義
private static ConcurrentHashMap<String, Entry> cacheMap;
// 然後是內部類Entry的定義,該類是用來存儲實際數據的,爲了方便處理過期時間,添加初始化時間戳、過期時間等屬性。
// 存儲內容的結構定義
static class Entry {
long initTime;// 存儲時間
int expire; // 單位:秒
Object data;// 具體數據
Entry(long initTime, int expire, Object data) {
this.initTime = initTime;
this.expire = expire;
this.data = data;
}
}
// 然後是使用雙檢鎖單例方式獲取本類實例對象,因爲單例只能存在唯一的特點,所以注意構造函數需要設爲private
private CacheUtil() {
cacheMap = new ConcurrentHashMap<>(CACHE_INITIAL_SIZE);
}
public static CacheUtil getInstance() {
if (instance == null) {
synchronized (CacheUtil.class) {
if (instance == null) {
instance = new CacheUtil();
}
return instance;
}
}
return instance;
}
// 接下來是存入緩存數據`put()`方法,
// 這裏的`clearExpiredCache()`是清理過期緩存,
// 後面會看到方法體,因爲在我項目中存入緩存的情況較少,
// 所以這裏我固定了每次存之前先清理一次過期時間緩存,
// 這裏可以根據自己項目實際情況進行優化。
public synchronized void put(String key, Object value, int expire) {
clearExpiredCache();
Entry entry = new Entry(System.currentTimeMillis(), expire, value);
cacheMap.put(key, entry);
}
// 然後是獲取緩存`get()`方法,因爲獲取數據的時間較爲多數,
// 所以這裏我設定了三分之一的概率清理過期緩存,適當地釋放堆內存,
// 並且在獲取時檢測是否過期,如果已過期然而還獲取到了,就刪除並返回空。
public synchronized Object get(String key) {
// 構造三分之一的機率清除過期緩存
if(new Random().nextInt(12) > 8){
clearExpiredCache();
}
if (cacheMap.containsKey(key)) {
Entry entry = cacheMap.get(key);
if (entry.expire > 0 && System.currentTimeMillis() > entry.expire * 1000 + entry.initTime) {
cacheMap.remove(key);
return null;
} else {
return entry.data;
}
} else {
return null;
}
}
// 然後就是比較常規的一些方法,具體可以看代碼
public int size() {
return cacheMap.size();
}
@Override
public boolean isContains(String key) {
return cacheMap.containsKey(key);
}
@Override
public Set<String> getAllKeys() {
return cacheMap.keySet();
}
@Override
public void remove(String key) {
cacheMap.remove(key);
}
// 最後一個方法就是清理過期緩存,這裏你可以選擇啓動一個監聽
// 線程實時地清理緩存,也可以選擇在適當時機進行一次清理,
// 比如我這裏就是在存在put和get操作時固定或概率地清理緩存。
private synchronized void clearExpiredCache() {
Iterator<Map.Entry<String, Entry>> iterator = cacheMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Entry> entry = iterator.next();
if (entry.getValue().expire > 0 &&
System.currentTimeMillis() > entry.getValue().expire * 1000 + entry.getValue().initTime) {
iterator.remove();
}
}
}
}
三、併發測試
普通的實現測試這裏就不展示了,肯定是沒問題的,讀者簡單寫一些測試樣例即可,這裏主要展示一下併發測試,因爲在實際情況中存在併發處理緩存情況,爲了確保其正確性,所以併發測試是必須要做的,下面放出我的測試樣例。
@Test
public void concurrentCacheTest() {
final int LOOP_TIMES = 1000;// 循環次數
// 線程池,啓動10個子線程進行處理
ExecutorService es = Executors.newFixedThreadPool(10);
// 存放隨機生成的key
ConcurrentLinkedQueue<String> clq = new ConcurrentLinkedQueue<>();
// 定義兩個計數器,用於計量兩次併發過程
CountDownLatch count1 = new CountDownLatch(LOOP_TIMES);
CountDownLatch count2 = new CountDownLatch(LOOP_TIMES);
// 緩存操作過程的計數
AtomicInteger atomicInteger = new AtomicInteger(0);
// 測試併發情況下put表現
for (int i = 0; i < LOOP_TIMES; i++) {
es.execute(new Runnable() {
@Override
public void run() {
String key = String.valueOf(new Random().nextInt(1000));
Object value = new Random().nextInt(1000);
int expire = new Random().nextInt(100);
clq.add(key);
cacheUtil.put(key, value, expire);
System.out.println(atomicInteger.incrementAndGet() +
".存入緩存成功,key=" + key +
",value=" + value +
",expire=" + expire);
count1.countDown();
}
});
}
try {
count1.await();// 等待所有的put執行完
} catch (InterruptedException e) {
e.printStackTrace();
}
// 測試併發情況下的get表現
atomicInteger.set(0);
for (int i = 0; i < LOOP_TIMES; i++) {
es.execute(new Runnable() {
@Override
public void run() {
try {
// 隨機等待時間
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
String key = clq.poll();
System.out.println(atomicInteger.incrementAndGet() +
".從緩存中獲取key=" + key +
"的值:" + cacheUtil.get(key));
count2.countDown();
}
});
}
try {
count2.await();// 等待所有的get執行完
} catch (InterruptedException e) {
e.printStackTrace();
}
es.shutdown();
while (true){
if (es.isTerminated()){
System.out.println("所有任務均執行完");
System.out.println("緩存大小:" + cacheUtil.size());
return;
}
}
}
最後測試的表現是很好,沒有出現不正確的情況,部分測試結果截圖如下:
四、拓展
該類只是簡單的實現了緩存的過程,但是在實際應用中不見得能很好地表現,首先它的容量肯定有限,不能存太多緩存,因爲使用的是JVM堆內的內存,優化的話可以使用直接內存進行存儲,其次其功能也較爲簡單,比如不支持LRU淘汰等,這個可以用雙鏈表+Map或者是LinkedHashMap去實現,更多功能都可以拓展。
我的微信公衆號北風IT之路瀏覽體驗更佳,在這裏還有更多優秀文章爲你奉上,快來關注吧!