天天看點

如何實作本地緩存和分布式緩存?如何實作本地緩存和分布式緩存?EhCache 和 Guava 的使用及特點分析手動實作一個緩存系統

如何實作本地緩存和分布式緩存?

緩存(

Cache

) 是指 将程式或系統中常用的資料對象存儲在像記憶體這樣特定的媒體中,以避免在每次程式調用時,重新建立或組織資料所帶來的性能損耗,進而提高了系統的整體運作速度

以目前的系統架構來說,使用者的請求一般會先經過緩存系統,如果緩存中沒有相關的資料,就會在其他系統中查詢到相應的資料并儲存在緩存中,最後傳回給調用方

本地緩存是指程式級别的緩存元件,它的特點是本地緩存和應用程式會運作在同一個程序中,是以本地緩存的操作會非常快,因為在同一個程序内也意味着不會有網絡上的延遲和開銷

本地緩存适用于單節點非叢集的應用場景,它的優點是快,缺點是多程式無法共享緩存,比如分布式使用者 Session 會話資訊儲存,由于每次使用者通路的伺服器可能是不同的,如果不能共享緩存,那麼就意味着每次的請求操作都有可能被系統阻止,因為會話資訊隻儲存在某一個伺服器上,當請求沒有被轉發到這台存儲了使用者資訊的伺服器時,就會被認為是非登入的違規操作,除此之外,無法共享緩存可能會造成系統資源的浪費,這是因為每個系統都單獨維護了一份屬于自己的緩存,而同一份緩存有可能被多個系統單獨進行存儲,進而浪費了系統資源

分布式緩存是指将應用系統和緩存元件進行分離的緩存機制,這樣多個應用系統就可以共享一套緩存資料了,它的特點是共享緩存服務和可叢集部署,為緩存系統提供了高可用的運作環境,以及緩存共享的程式運作機制

本地緩存可以使用 

EhCache

 和 

Google

 的 

Guava

 來實作,而分布式緩存可以使用 

Redis

 或 

Memcached

 來實作

由于 

Redis

 本身就是獨立的緩存系統,是以可以作為第三方來提供共享的資料緩存,而 

Redis

 的分布式支援主從、哨兵和叢集的模式,是以它就可以支援分布式的緩存,而 

Memcached

 的情況也是類似的

EhCache

 和 

Guava

 的使用及特點分析

EhCache

 是目前比較流行的開源緩存架構,是用

純 Java 語言實作

的簡單、快速的 

Cache

 元件。

EhCache

 支援記憶體緩存和磁盤緩存,支援 

LRU(Least Recently Used,最近很少使用)

LFU(Least Frequently Used,最近不常被使用)

和 

FIFO(First In First Out,先進先出)

等多種淘汰算法,并且支援分布式的緩存系統,

EhCache

 最初是獨立的本地緩存架構元件,在後期的發展中(從 1.2 版)開始支援分布式緩存,分布式緩存主要支援 

RMI、JGroups、EhCache Server

 等方式

LRU(最近很少使用) 和 LFU(最近不常被使用) 的差別?

LRU

 算法有一個缺點,比如說很久沒有使用的一個鍵值,如果最近被通路了一次,那麼即使它是使用次數最少的緩存,它也不會被淘汰;

而 

LFU

 算法解決了偶爾被通路一次之後,資料就不會被淘汰的問題,它是根據總通路次數來淘汰資料的,其核心思想是“如果資料過去被通路多次,那麼将來它被通路次數也會比較多”。是以 LFU 可以了解為比 LRU 更加合理的淘汰算法

EhCache 基礎使用

首先,需要在項目中添加 EhCache 架構,如果為 Maven 項目,則需要在 pom.xml 中添加如下配置

<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.8.1</version>
</dependency>
           

無配置參數的 EhCache 3.x 使用代碼如下

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;

public class EhCacheExample {
    public static void main(String[] args) {
        // 建立緩存管理器
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
        // 初始化 EhCache
        cacheManager.init();
        // 建立緩存(存儲器)
        Cache<String, String> myCache = cacheManager.createCache("MYCACHE",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                        String.class, String.class,
                        ResourcePoolsBuilder.heap(10))); // 設定緩存的最大容量
        // 設定緩存
        myCache.put("key", "Hello,Java.");
        // 讀取緩存
        String value = myCache.get("key");
        // 輸出緩存
        System.out.println(value);
        // 關閉緩存
        cacheManager.close();
    }
}
           

其中

  • CacheManager

    :是緩存管理器,可以通過單例或者多例的方式建立,也是 

    Ehcache

     的入口類;
  • Cache

    :每個 

    CacheManager

     可以管理多個 

    Cache

    ,每個 Cache 可以采用 

    hash

     的方式存儲多個元素

它們的關系如下圖所示

如何實作本地緩存和分布式緩存?如何實作本地緩存和分布式緩存?EhCache 和 Guava 的使用及特點分析手動實作一個緩存系統

EhCache

 的特點是,它使用起來比較簡單,并且本身的 jar 包不是不大,簡單的配置之後就可以正常使用了。EhCache 的使用比較靈活,它支援多種緩存政策的配置,它同時支援記憶體和磁盤緩存兩種方式,在 

EhCache 1.2

 之後也開始支援分布式緩存了

Guava Cache

 是 

Google

 開源的 

Guava

 裡的一個子功能,它是一個記憶體型的本地緩存實作方案,提供了線程安全的緩存操作機制

Guava Cache

 的架構設計靈感來源于 

ConcurrentHashMap

,它使用了多個 

segments

 方式的細粒度鎖,在保證線程安全的同時,支援了高并發的使用場景。Guava Cache 類似于 Map 集合的方式對鍵值對進行操作,隻不過多了過期淘汰等處理邏輯

在使用 

Guava Cache

 之前,我們需要先在 

pom.xml

 中添加 

Guava

 架構,配置如下

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>
           

Guava Cache

 的建立有兩種方式,一種是 

LoadingCache

,另一種是 

Callable

,代碼示例如下

import com.google.common.cache.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class GuavaExample {
    public static void main(String[] args) throws ExecutionException {
        // 建立方式一:LoadingCache
        LoadingCache<String, String> loadCache = CacheBuilder.newBuilder()
                // 并發級别設定為 5,是指可以同時寫緩存的線程數
                .concurrencyLevel(5)
                // 設定 8 秒鐘過期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                //設定緩存容器的初始容量為 10
                .initialCapacity(10)
                // 設定緩存最大容量為 100,超過之後就會按照 LRU 算法移除緩存項
                .maximumSize(100)
                // 設定要統計緩存的命中率
                .recordStats()
                // 設定緩存的移除通知
                .removalListener(new RemovalListener<Object, Object>() {
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                    }
                })
                // 指定 CacheLoader,緩存不存在時,可自動加載緩存
                .build(
                        new CacheLoader<String, String>() {
                            @Override
                            public String load(String key) throws Exception {
                                // 自動加載緩存的業務
                                return "cache-value:" + key;
                            }
                        }
                );
        // 設定緩存
        loadCache.put("c1", "Hello, c1.");
        // 查詢緩存
        String val = loadCache.get("c1");
        System.out.println(val);
        // 查詢不存在的緩存
        String noval = loadCache.get("noval");
        System.out.println(noval);

        // 建立方式二:Callable
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(2) // 設定緩存最大長度
                .build();
        // 設定緩存
        cache.put("k1", "Hello, k1.");
        // 查詢緩存
        String value = cache.get("k1", new Callable<String>() {
            @Override
            public String call() {
                // 緩存不存在時,執行
                return "nil";
            }
        });
        // 輸出緩存值
        System.out.println(value);
        // 查詢緩存
        String nokey = cache.get("nokey", new Callable<String>() {
            @Override
            public String call() {
                // 緩存不存在時,執行
                return "nil";
            }
        });
        // 輸出緩存值
        System.out.println(nokey);
    }
}
           

以上程式的執行結果為

Hello, c1.
cache-value:noval

Hello, k1.
nil
           

可以看出 

Guava Cache

 使用了程式設計式的 

build

 生成器進行建立和管理,讓使用者可以更加靈活地操縱代碼,并且 

Guava Cache

 提供了靈活多樣的個性化配置,以适應各種使用場景

手動實作一個緩存系統

需求:要自定義一個緩存,首先要考慮的是資料類型,我們可以使用 

Map

 集合中的 

HashMap

Hashtable

 或 

ConcurrentHashMap

 來實作,非并發情況下我們可以使用 

HashMap

,并發情況下可以使用 

Hashtable

 或 

ConcurrentHashMap

,由于 

ConcurrentHashMap

 的性能比 

Hashtable

 的高,是以在高并發環境下我們可以傾向于選擇 

ConcurrentHashMap

,不過它們對元素的操作都是類似的

標明了資料類型之後,我們還需要考慮緩存過期和緩存淘汰等問題,在這裡我們可以借鑒 

Redis

 對待過期鍵的處理政策,目前比較常見的過期政策有以下三種

  • 定時删除
定時删除是指在設定鍵值的過期時間時,建立一個定時事件,當到達過期時間後,事件處理器會執行删除過期鍵的操作。它的優點是可以及時的釋放記憶體空間,缺點是需要開啟多個延遲執行事件來處理清除任務,這樣就會造成大量任務事件堆積,占用了很多系統資源
  • 惰性删除
惰性删除不會主動删除過期鍵,而是在每次請求時才會判斷此值是否過期,如果過期則删除鍵值,否則就傳回 null。它的優點是隻會占用少量的系統資源,缺點是清除不夠及時,會造成一定的空間浪費
  • 定期删除
定期删除是指每隔一段時間檢查一次資料庫,随機删除一些過期鍵值

Redis

 使用的是定期删除和惰性删除這兩種政策

自定義緩存的實作思路:首先需要定義一個存放緩存值的實體類,這個類裡包含了緩存的相關資訊,比如緩存的 

key 和 value

,緩存的存入時間、最後使用時間和命中次數(預留字段,用于支援 LFU 緩存淘汰),再使用 

ConcurrentHashMap

 儲存緩存的 key 和 value 對象(緩存值的實體類),然後再新增一個緩存操作的工具類,用于添加和删除緩存,最後再緩存啟動時,開啟一個無限循環的線程用于檢測并删除過期的緩存

首先,定義一個緩存值實體類,代碼如下

import lombok.Getter;
import lombok.Setter;

/**
 * 緩存實體類
 */
@Getter
@Setter
public class CacheValue implements Comparable<CacheValue> {
    // 緩存鍵
    private Object key;
    // 緩存值
    private Object value;
    // 最後通路時間
    private long lastTime;
    // 建立時間
    private long writeTime;
    // 存活時間
    private long expireTime;
    // 命中次數
    private Integer hitCount;

    @Override
    public int compareTo(CacheValue o) {
        return hitCount.compareTo(o.hitCount);
    }
}
           

然後定義一個全局緩存對象,代碼如下

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Cache 全局類
 */
public class CacheGlobal {
    // 全局緩存對象
    public static ConcurrentMap<String, MyCache> concurrentMap = new ConcurrentHashMap<>();
}
           

定義過期緩存檢測類的代碼如下

import java.util.concurrent.TimeUnit;

/**
 * 過期緩存檢測線程
 */
public class ExpireThread implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                // 每十秒檢測一次
                TimeUnit.SECONDS.sleep(10);
                // 緩存檢測和清除的方法
                expireCache();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 緩存檢測和清除的方法
     */
    private void expireCache() {
        System.out.println("檢測緩存是否過期緩存");
        for (String key : CacheGlobal.concurrentMap.keySet()) {
            MyCache cache = CacheGlobal.concurrentMap.get(key);
            // 目前時間 - 寫入時間
            long timoutTime = TimeUnit.NANOSECONDS.toSeconds(
                    System.nanoTime() - cache.getWriteTime());
            if (cache.getExpireTime() > timoutTime) {
                // 沒過期
                continue;
            }
            // 清除過期緩存
            CacheGlobal.concurrentMap.remove(key);
        }
    }
}
           

接着,我們要新增一個緩存操作的工具類,用于查詢和存入緩存,實作代碼如下

import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;

/**
 * 緩存操作工具類
 */
public class CacheUtils {

    /**
     * 添加緩存
     * @param key
     * @param value
     * @param expire
     */
    public void put(String key, Object value, long expire) {
        // 非空判斷,借助 commons-lang3
        if (StringUtils.isBlank(key)) return;
        // 當緩存存在時,更新緩存
        if (CacheGlobal.concurrentMap.containsKey(key)) {
            MyCache cache = CacheGlobal.concurrentMap.get(key);
            cache.setHitCount(cache.getHitCount() + 1);
            cache.setWriteTime(System.currentTimeMillis());
            cache.setLastTime(System.currentTimeMillis());
            cache.setExpireTime(expire);
            cache.setValue(value);
            return;
        }
        // 建立緩存
        MyCache cache = new MyCache();
        cache.setKey(key);
        cache.setValue(value);
        cache.setWriteTime(System.currentTimeMillis());
        cache.setLastTime(System.currentTimeMillis());
        cache.setHitCount(1);
        cache.setExpireTime(expire);
        CacheGlobal.concurrentMap.put(key, cache);
    }

    /**
     * 擷取緩存
     * @param key
     * @return
     */
    public Object get(String key) {
        // 非空判斷
        if (StringUtils.isBlank(key)) return null;
        // 字典中不存在
        if (CacheGlobal.concurrentMap.isEmpty()) return null;
        if (!CacheGlobal.concurrentMap.containsKey(key)) return null;
        MyCache cache = CacheGlobal.concurrentMap.get(key);
        if (cache == null) return null;
        // 惰性删除,判斷緩存是否過期
        long timoutTime = TimeUnit.NANOSECONDS.toSeconds(
                System.nanoTime() - cache.getWriteTime());
        // 緩存過期
        if (cache.getExpireTime() <= timoutTime) {
            // 清除過期緩存
            CacheGlobal.concurrentMap.remove(key);
            return null;
        }
        cache.setHitCount(cache.getHitCount() + 1);
        cache.setLastTime(System.currentTimeMillis());
        return cache.getValue();
    }
}
           

最後是調用緩存的測試代碼

public class MyCacheTest {
    public static void main(String[] args) {
        CacheUtils cache = new CacheUtils();
        // 存入緩存
        cache.put("key", "老王", 10);
        // 查詢緩存
        String val = (String) cache.get("key");
        System.out.println(val);
        // 查詢不存在的緩存
        String noval = (String) cache.get("noval");
        System.out.println(noval);
    }
}
           

以上程式的執行結果如下

老王
null
           

繼續閱讀