四種強大且隱祕的快取

2023-06-25 18:00:40

掃地僧,是金庸武俠小說《天龍八部》中的人物。

他的來歷沒有太多描述,負責打掃藏經閣,神祕而且武功深不可測,並具有大智慧,有極高技藝卻深藏不露,隱匿在少林寺默默無聞。

這篇文章,筆者想聊聊快取,只不過並不是大家耳熟能詳的 Redis ,而是那些隱藏在中介軟體或者框架中強大卻又隱祕的快取,筆者願稱他們為快取世界的掃地僧

1 HashMap/ConcurrentHashMap 設定快取

HashMap 是一種基於雜湊表的集合類,它提供了快速的插入、查詢和刪除操作。可以將鍵值對作為快取項的儲存方式,將鍵作為快取項的唯一識別符號,值作為快取項的內容。

HashMap 是很多程式設計師接觸的第一種快取 , 因為現實業務場景裡,我們可能需要給快取新增快取統計過期失效淘汰策略等功能,HashMap 的功能就顯得孱弱 ,所以 HashMap 在業務系統中使用得並不算多。

HashMap 在中介軟體中卻是香餑餑,我們訊息中介軟體 RocketMQ 為例。

上圖是 RocketMQ 的叢集模式 ,Broker 分為 Master 與 Slave,一個 Master 可以對應多個 Slave,但是一個 Slave 只能對應一個 Master。

每個 Broker 與 Name Server 叢集中的所有節點建立長連線,定時每隔 30 秒註冊 主題的路由資訊到所有 Name Server。

訊息傳送者、訊息消費者,在同一時間只會連線 Name Server 叢集中的一臺伺服器,並且會每隔 30s 會定時更新 Topic 的路由資訊。

我們可以理解 Name Server 叢集的作用就是註冊中心,註冊中心會儲存路由資訊(主題的讀寫佇列數、操作許可權等),路由資訊就是儲存在 HashMap 中 。

路由資訊通過幾個 HashMap 來儲存,當 Broker 向 Nameserver 傳送心跳包(路由資訊),Nameserver 需要對 HashMap 進行資料更新,但我們都知道 HashMap 並不是執行緒安全的,高並行場景下,容易出現 CPU 100% 問題,所以更新 HashMap 時需要加鎖,RocketMQ 使用了 JDK 的讀寫鎖 ReentrantReadWriteLock 。

下面我們看下路由資訊如何更新和讀取:

1、寫操作:更新路由資訊,操作寫鎖

2、讀操作:查詢主題資訊,操作讀鎖

同時,我們需要注意 Name Server 維護路由資訊還需要定時任務的支撐。

  • 每個 Broker 定時每隔 30 秒註冊 主題的路由資訊到所有 Name Server
  • Name Server 定時任務每隔10 秒清除已宕機的 Broker

我們做一個小小的總結,Name Server 維護路由的模式是: HashMap + 讀寫鎖 + 定時任務更新

  • HashMap 作為儲存容器
  • 讀寫鎖控制鎖的顆粒度
  • 定時任務定時更新快取

寫到這裡,我們不禁想到 ConcurrentHashMap 。

ConcurrentHashMap 可以保證執行緒安全,JDK1.7 之前使用分段鎖機制實現,JDK1.8 則使用陣列+連結串列+紅黑樹資料結構和CAS原子操作實現。

Broker 使用不同的 ConcurrentHashMap 分別用來儲存消費組、消費進度、訊息過濾資訊等。

那麼名字服務為什麼不使用 ConcurrentHashMap 作為儲存容器呢 ?

最核心的原因在於:路由資訊由多個 HashMap 組成,通過每次寫操作可能要操作多個物件 ,為了保證其一致性,所以才需要加讀寫鎖。

2 LinkedHashMap 最近最少使用快取

LinkedHashMap 是 HashMap 的子類,但是內部還有一個雙向連結串列維護鍵值對的順序,每個鍵值對既位於雜湊表中,也位於雙向連結串列中。

LinkedHashMap 支援兩種順序插入順序 、 存取順序

  • 插入順序:先新增的在前面,後新增的在後面,修改操作並不影響順序
  • 存取順序:問指的是 get/put 操作,對一個鍵執行 get/put 操作後,其對應的鍵值對會移動到連結串列末尾,所以最末尾的是最近存取的,最開始的是最久沒有被存取的,這就是存取順序。

LinkedHashMap 經典的用法是作為 LruCache (最近最少使用快取) ,而 MyBatis 的二級快取的淘汰機制就是使用的 LinkedHashMap 。

MyBatis 的二級快取是使用責任鏈+ 裝飾器的設計模式實現的,雖然 Mybatis 的二級快取功能在生產環境並不推薦使用,但它的設計模式還是值得學習。

上圖中,裝飾器包目錄下 Cache 介面有不同的實現類,比如過期淘汰紀錄檔記錄等。

LruCache 使用了裝飾器模式 ,使用 LinkedHashMap 預設儲存 1024 個快取 key ,當 key 最久未被存取,並且 keyMap 的大小超過 1024 時 ,記錄最老的 key ,當下次新增快取物件時,刪除最老的 key。

使用 LinkedHashMap 重點需要做到使用存取順序模式重寫 removeEldestEntry 方法。 因為 LinkedHashMap 並不是執行緒安全的,Mybatis 二級快取責任鏈中 SynchronizedCache 物件可以實現執行緒安全的對快取讀寫。

3 TreeMap 排序物件快取

TreeMap 是一種基於紅黑樹的有序 Map,它可以按照鍵的順序進行遍歷。

一致性雜湊(Consistent Hashing)演演算法被廣泛應用於快取系統、分散式資料庫、負載均衡器等分散式系統中,以實現高效能和高可用性。它解決了傳統雜湊演演算法在動態環境下擴充套件性和負載均衡效能的問題。

一致性雜湊的主要優點是在節點增減時,只有少量的資料需要重新對映,因為只有那些直接或間接與新增或刪除節點相鄰的資料項需要遷移。這大大減少了系統的遷移開銷和影響,使得系統更具擴充套件性和可伸縮性。

TreeMap 在一致性雜湊中可以用作節點/虛擬節點的儲存結構,用來維護節點在雜湊環上的位置和鍵的有序性。

1、我們定義一個 TreeMap 儲存節點/虛擬節點 。

2、初始化節點

建構函式包含三個部分:物理節點集合、每個物理節點對應的虛擬節點個數、雜湊函數 。

我們重點看下新增節點邏輯:

3、按照 key 查詢節點

新增完節點之後,節點分佈類似下圖:

當需要定位某個 key 屬於哪個節點時,先通過雜湊函數計算 key 的雜湊值,並在環上順時針方向找到第一個大於等於該雜湊值的節點位置。該節點即為資料的歸屬節點 。

我們新增一個新的節點 node5 , 從下圖中,我們可以看到,影響的範圍(深黃色)並不大 ,這也就是一致性雜湊演演算法的優勢。

4 ByteBuffer 網路程式設計緩衝池

ByteBuffer 是位元組緩衝區,主要用於使用者讀取和快取位元組資料,多用於網路程式設計、檔案 IO 處理等。

筆者第一次接觸 ByteBuffer 是在分庫分表中介軟體 Cobar 中 。在網路程式設計裡,經常需要分配記憶體,在高並行場景下,效能壓力比較大。

Cobar 抽象了一個 NIOProcessor 類用來處理網路請求,每個處理器初始化的時候都會建立一個緩衝池 BufferPool 。 BufferPool 用於池化 ByteBuffer ,這和我們平常使用的資料庫連線池的思路是一致的。

下圖展示了緩衝池 BufferPool 的原始碼:

緩衝池 BufferPool 的核心功能是分配快取回收快取 ,通過將快取池化,可以大大提升系統的效能。

如今 ,Netty 內建了更為強大的記憶體池化工具 ByteBuf ,我們會在後面的文章裡詳聊。

5 寫到最後

這篇文章,筆者總結了四種非常強大且隱祕的快取。

1、HashMap/ConcurrentHashMap 經常用於設定快取,對於 HashMap 來講,HashMap + 讀寫鎖 + 定時任務更新是常用的模式。而 ConcurrentHashMap 廣泛存在於各種中介軟體,執行緒安全且靈活易用。

2、LinkedHashMap 經常被用於建立最近最少使用快取 LruCache 。推薦學習 Mybatis 二級快取的設計,它使用責任鏈+ 裝飾器的設計模式,內建 LruCache 的實現就是使用 LinkedHashMap 。

3、TreeMap 是一種基於紅黑樹的有序 Map 。TreeMap 在一致性雜湊中可以用作節點/虛擬節點的儲存結構,用來維護節點在雜湊環上的位置和鍵的有序性。

4、ByteBuffer 是位元組緩衝區,主要用於使用者讀取和快取位元組資料,多用於網路程式設計、檔案 IO 處理等。分庫分表中介軟體 Cobar 在網路請求處理中,建立了緩衝池 BufferPool 用於池化 ByteBuffer ,從而大大提升系統的效能。


如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!