當前位置:
首頁 > 科技 > 手把手教你從0到1寫一個簡單的緩存框架

手把手教你從0到1寫一個簡單的緩存框架

接收程序員的技術早餐

目前市面上已經有很多開源的緩存框架,比如 Redis、Memcached、Ehcache 等,那為什麼還要自己動手寫緩存?本文將帶領大家從 0 到 1 寫一個簡單的緩存框架,目的是讓大家對緩存的類型、緩存的標準、緩存的實現以及原理方面有一個系統的了解,做到知其然,知其所以然。註:本文代碼部分較長,建議在 PC 端打開閱讀。

緩存定義的規範

JSR是 JavaSpecification Requests的縮寫,意思是 Java 規範提案,它已成為 Java 界的一個重要標準。在 2012年 10月 26 日 JSR 規範委員會發布了 JSR 107(JCache API)的首個早期規範,該規範定義了一種對 Java 對象臨時在內存中進行緩存的方法,包括對象的創建、訪問、失效、一致性等。

新規範的主要內容及特性

新規範的主要內容如下:

為應用程序提供緩存 Java 對象的功能。

定義了一套通用的緩存概念和工具。

最小化開發人員使用緩存的學習成本。

最大化應用程序在使用不同緩存實現之間的可移植性。

支持進程內和分散式的緩存實現。

支持 by-value 和 by-reference 的緩存對象。

定義遵照 JSR-175 的緩存註解;定義一套 Java 編程語言的元數據。

從規範的設計角度來看,在 javax.cache 包中有一個 CacheManager 介面負責保存和控制一系列的緩存,主要特性包括:

能從緩存中讀取數據

能將數據寫入到緩存中

緩存具有原子性操作

具有緩存事件監聽器

具有緩存註解

保存緩存的 KEY 和 VALUE 類型應該為泛型

新規範的 API 介紹

在 JSR107 規範中將 Cache API(javax.cache)作為實現的類庫,通過如下的 Maven 進行引入。

1. 核心概念

Cache API 定義了 4 個核心概念:

CachingProvider:定義了創建、配置、獲取、管理和控制多個 CacheManager。一個應用可以在運行期訪問多個 CachingProvider。

CacheManager:定義了創建、配置、獲取、管理和控制多個唯一命名的 Cache,這些 Cache 存在於 CacheManager 的上下文中。一個 CacheManager 僅被一個 CachingProvider 所擁有。

Cache:是一個類似 Map 的數據結構並臨時存儲以 Key 為索引的值。一個 Cache 僅被一個 CacheManager 所擁有。

Entry:是一個存儲在 Cache 中的 key-value 對。

每一個存儲在 Cache 中的條目有一個定義的有效期,即 Expiry Duration。一旦超過這個時間,條目為過期的狀態。一旦過期,條目將不可訪問、更新和刪除。緩存有效期可以通過 ExpiryPolicy 設置。

2. Store-By-Value和 Store-By-Reference

Store-By-Value 和 Store-By-Reference 是兩種不同的緩存實現。

Store-By-Value:指在 key/value 存入緩存時,將其值拷貝一份存入緩存。避免在其他程序修改 key 或 value 的值時,污染緩存內存儲的內容。

Store-By-Reference:指在 key/value 存入緩存時,直接將其引用存入緩存。

java常見的堆內緩存,一般使用 Store-By-Reference 方式,提升緩存性能。常見的堆外緩存和進程外緩存,一般由於使用引用在技術上比較複雜,通常使用 Store-By-Value 方式。

3. 緩存過期策略

如果緩存中的數據已經過期,那它將不能從緩存返回。如果緩存沒有配置過期政策,默認是永久有效的策略(Eternal)。

過期策略可以在配置時提供一個 ExpiryPolicy 實現的設置,見下面的定義

其中:

getExpiryForCreatedEntry():當數據創建後的到期持續時間

getExpiryForAccessedEntry(): 當數據訪問後的到期持續時間

getExpiryForModifiedEntry():當數據修改後的到期持續時間

當這些方法被調用時,ExpiryPolicy 將返回下列值之一:

持續時間等於緩存配置的過期時間

Duration.ZERO 表明數據目前已經是過期的

緩存框架的實現

基於上文緩存定義的規範,我們可以自己動手寫一個簡單的緩存框架,我們先對緩存框架做一個初步的規劃,實現一個具有如表 1 所描述的特性的簡單緩存。

表 1 緩存框架特性

下面,我們將遵循我們的規劃,由簡入繁逐步迭代我們的緩存組件,我們給組件取名叫做 CsCache(Cache Study)。

前期準備

參考開源緩存組件 EhCache 和 Guava,提取它們的公共方法,可以得到最核心的,也是我們最關心的一些方法,見表 2。

表 2 簡單緩存的常用方法

我們的緩存框架選取了最基本的 get(獲取緩存)、put(放入緩存)、remove(根據 key值刪除緩存)、clear(清空緩輩子)方法,這些方法是實際工作中當中最常用的功能。

緩存的架構介紹

通過上一小節的前期準備,我們確定了緩存框架的幾個基本的使用方法,那麼從這一小節,我們就由淺入深的介紹 CsCache 緩存框架。

通過 JSR107 規範,我們將框架定義為客戶端層、緩存提供層、緩存管理層、緩存存儲層。其中緩存存儲層又分為基本存儲層、LRU存儲層和 Weak 存儲層,如圖 1 所示。

圖 1 緩存分層圖

其中:

客戶端層:使用者直接通過該層與數據進行交互。

緩存提供層:主要對緩存管理層的生命周期進行維護,負責緩存管理層的創建,保存、獲取以及銷毀。

緩存管理層:主要對緩存客戶端的生命周期進行維護,負責緩存客戶端的創建,保存、獲取以及銷毀

緩存存儲層:負責數據以什麼樣的形式進行存儲。

基本存儲層:是以普通的 ConcurrentHashMap 為存儲核心,數據不淘汰。

LRU 存儲層:是以最近最少用為原則進行的數據存儲和緩存淘汰機制。

Weak 存儲層:是以弱引用為原則的數據存儲和緩存淘汰機制。

設計思路以及知識點詳解

本節開始深入介紹緩存框架的類圖以及相關知識點。圖 2 所示列出了緩存框架的工程結構。

圖 2 框架工程結構圖

整個工程結構的包結構分為 JSR107 包和 store 包,JSR107 是與規範相關的一些類的封裝,store包是與數據存儲相關類的封裝。

1. 設計類圖

通過分析上文緩存架構介紹和圖 2 工程結構圖,我們能夠對框架的整體情況有一個概覽,本小節將以類圖的方式展現框架的設計理念,如圖 3 所示。

圖 3 類圖

根據規範,CacheProvider、CacheManager、Cache是抽象出來的最基礎的緩存介面。其中 Cache 是提供最終緩存實現的基礎介面,其實現類是 CsCache107,初始化時即持有一個 BasicDataStore 對象。完整的類列表見表 3 所示。

表 3 框架核心類列表

2. 緩存框架的 SPI機制

在工程結構中的 META-INF/services/ 下面有一個 javax.cache.spi.CachingProvider 配置文件,裡面有一個 org.cachestudy.writeitbyself.jsr107.CsCaching107Provider 實現類,這個配置文件實際上是利用的 JAVA SPI 機制進行組件的發現與載入。

(1)什麼是 SPI

SPI 的全名為 Service Provider Interface,是 JDK 內置的一種服務提供發現機制,在 Java.util.ServiceLoader 的文檔里有比較詳細的介紹。

JAVA SPI 機制的思想簡單來說是:在面向的對象的設計里,我們一般推薦模塊之間基於介面編程,模塊之間不對實現類進行硬編碼。一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。為了實現在模塊裝配的時候能不在程序里動態指明,這就需要一種服務發現機制。JAVA SPI 就是提供了這樣的一個機制,為某個介面尋找服務實現的機制。有點類似 IOC 的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。

(2)SPI 的使用約定

當服務的提供者,提供了服務介面的一種實現之後,在 jar 包的 META-INF/services/ 目錄里同時創建一個以服務介面命名的文件。該文件里就是實現該服務介面的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該 jar 包 META-INF/services/ 里的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再代碼里制定。而在 jdk 裡面提供服查找工具類:java.util.ServiceLoader,如圖 4 所示。

圖 4 SPI 約定結構圖

3. 解讀緩存數據層

緩存數據層實際承擔的責任主要是緩存數據的存儲和緩存的淘汰機制,在圖 2 中可以看到數據的存儲和淘汰是基於 DataStore 這個介面來實現的,而這一實現也正是圖 1 提到的數據存儲層。目前框架一共實現了三個實現類分別是:LRUDataStore、WeakDataStore 和 BaseDataStore。

我們先來分析一下 LRUDataStore 的設計原理:

(1)基於引用的淘汰演算法

基於引用的淘汰演算法,是一種簡單有效的演算法,由 JVM 的 GC 進行回收。Java 的引用主要分為強引用、軟引用、弱引用、虛引用。

強引用(StrongReference):強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

軟引用(SoftReference):如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

弱引用(WeakReference):弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先順序很低的線程,因此不一定會很快發現那些只具有弱引用的對象。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

引用(PhantomReference):「虛引用」顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

我們的引用淘汰演算法是基於弱引用來實現的,在圖 5 中展示了 store 包的類列表

圖 5 弱引用淘汰演算法

其中 WeakValueDataStore 和 WeakValueHoler 是弱引用實現所需要的實現類。WeakValueDataStore 實現了 DataStore介面,提供基於弱引用的數據存儲,WeakValueHolder 實現 ValueHolder 介面,提供基於弱引用的實際值存儲邏輯。

WeakValueDataStore 類的代碼及實現原理如下:

WeakValueHolder 的代碼及實現原理如下:

測試用例驗證方法如下:

(2)基於 LRU的淘汰演算法

LRU(Least recently used,最近最少使用)演算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是「如果數據最近被訪問過,那麼將來被訪問的幾率也更高」。

CsCache的 LRU 簡單實現邏輯如下:我們通過維護 entry 的列表,在 get、put時維護 entry 列表實現,使最少訪問的鍵值對維持在 entry 列表的最尾部。在數據量超過緩存容量需要做 LRU 淘汰時,我們通過刪除鏈表尾部的數據,來實現簡單的 LRU 數據淘汰機制,如圖 6 所示。

圖 6 LRU 淘汰演算法

其中 LRUDataStore和 LRUEntry是弱引用實現所需要的實現類。LRUDataStore 實現了 DataStore 介面。LRUEntry 對象則是 LRU 的數據存儲類

LRUDataStore 類的關鍵代碼及實現原理如下:

這段關鍵代碼的核心意思是,在 LRUDataStore 類中維護了一個 LRUEntity 的數據鏈表,當執行 put 操作的時,則將數據封裝成 LRUEntity 數據節點,加入到鏈表的頭部以表示數據是最新的,如果數據超出鏈表的設定的大小範圍,則從鏈表的尾部刪除最不活躍的數據節點。當執行 get 操作的時,首先將 LRUEntity 數據節點移到到鏈表的頭部,以表示該數據被最新請求訪問,然後將數據返回。

4. 解讀緩存管理層(CacheManager)

在上面圖 1 中我們介紹了框架的分層結構,其中介面類 CacheManager 所對應的正是緩存管理層,在 CsCache 框架中 CacheManager 的實現類是 CsCache107Manager,它主要負責管理多個 Cache 客戶端實例,以及負責緩存客戶端實例的創建、銷毀、獲取等。

下面具體介紹 CsCache107Manager 類的關鍵代碼及實現原理。

(1)緩存實例的創建

緩存實例創建的實現代碼如下:

上面的代碼只是針對 CsCache107Manager 類的 createCache 方法的代碼進行了解讀,完整的緩存實例的創建流程,如圖 7 所示。

圖 7 緩存實例創建

(2)緩存實例的獲取

緩存實例獲取的實現代碼如下:

完整的緩存實例獲取流程圖,如圖 8 所示。

圖 8 緩存實例的獲取

緩存實例的創建和獲取實際上主要是基於一個緩存池來實現的,在代碼中使用的是一個 ConcurrentHashMap 類,可以根據多個不同的緩存名稱創建多個緩存實例,從而可以並發的讀取。

5. 解讀數據客戶端層

緩存客戶端層主要是針對實際使用者的,在工程結構中主要涉及二個類,分別是:CsCache和 CsCache107,而 CsCache107使用的代理模式對 CsCache 進行的包裝,如圖 9 所示。用戶在使用的時候,通過緩存管理層的 CacheManager 對象就可以獲得 CsCache107 客戶端對象,從而可以實現對緩存的直接操作。

圖 9 數據客戶端層

CsCache 關鍵代碼和實現原理如下:

整個過程其實較為簡單對象的構造方法中有一個 DataStore 對象,這個對象正是緩數據存儲與淘汰策略對象,這個機制已經在解讀緩存數據層小節中進行了詳解,get 方法則是從 DataStore 中獲取緩存數據,put 方法則是往 DataStore 對象中存入數據。

CscCache107 對象實際上是對 CsCache 對象根據 JSR107 規範,使用了代理模式進行包裝,下面將展示幾個示例方法,原理與上面 CsCache 是一樣的,本節就不在說明。CsCache107 關鍵代碼和實現原理如下:

通過上面代碼可以看到 put、get、remove方法都是調用的 CsCache 對象的相關方法進行的操作,其目的主要是在有特殊需求的時候可以對這幾個方法進行功能的擴展和增強。

緩存框架的使用示例

緩存框架的實現以及原理到這裡就基本介紹完了,下面我們將以一個使用示例結束本文的講解。

為方便讀者能夠完整的學習 CsCache 框架,本章實例的完整代碼放入在https://github.com/jsr107/jsr107spec,讀者可以自行下載學習。

欲了解更多有關分散式緩存方面的內容,請閱讀《深入分散式緩存:從原理到實踐》一書。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

中年程序員都在想什麼?
半路轉型做人工智慧,誰說不可行?

TAG:InfoQ |