用Java寫一個簡單的緩存操作類

前言

使用緩存已經是開發中老生常談的一件事了,常用專門處理緩存的工具比如Redis、MemCache等,但是有些時候可能需要一些簡單的緩存處理,沒必要用上這種專門的緩存工具,那麼自己寫一個緩存類最合適不過了。

一、分析

首先分析一下緩存類該如何設計,這裏我以一種非常簡單的方式來實現一個緩存類,這也是我一直以來使用的設計方案。

爲了明確功能,首先定義一個接口類CacheInt,然後是緩存實現的工具類CacheUtil。然後再看其中的功能,爲了存取方便,緩存應是以鍵值對的形式存取,爲了適應更多的場景,所以在存取的時候可以加一個緩存過期時間,然後再加上其他常見的添加、獲取、刪除、緩存大小、是否存在key、清理過期緩存等方法,整個緩存工具的方法差不多就是這些。

緩存類需要注意的問題:

  1. 緩存對象應該是唯一的,也就是單例的;
  2. 緩存的操作方法要同步,在多線程併發條件下防止出錯;
  3. 緩存的容器應該具有較高的併發性能,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之路瀏覽體驗更佳,在這裏還有更多優秀文章爲你奉上,快來關注吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章