結合Hazelcast和Spring的分散式快取 - reflectoring

banq發表於2020-06-18

在某些應用程式中,我們需要保護資料庫或避免進行成本高昂的計算。我們可以為此目的使用快取。本文展示瞭如何在分散式可伸縮應用程式中將Hazelcast用作Spring的快取。

本文隨附GitHub上的工作程式碼示例。

假設我們有一個Spring Boot應用程式,我們想在該應用程式中使用快取。但我們也希望能夠擴充套件此應用程式。這意味著,例如,當我們啟動應用程式的三個例項時,它們必須共享快取以保持資料一致。

Hazelcast是一個分散式的記憶體物件儲存,並提供許多功能,包括TTL,直寫和可伸縮性。我們可以通過啟動網路中的多個Hazelcast節點來構建Hazelcast叢集。每個節點稱為成員。

我們可以使用Hazelcast實現兩種拓撲:

  • 嵌入式快取拓撲,以及
  • 客戶端-伺服器拓撲。

讓我們看一下如何使用Spring實現每個拓撲。

嵌入式快取

這種拓撲意味著應用程式的每個例項都有一個整合的成員:

結合Hazelcast和Spring的分散式快取 - reflectoring

在這種情況下,應用程式和快取資料在同一節點上執行。當在快取中寫入新的快取條目時,Hazelcast會負責將其分發給其他成員。從快取中讀取資料時,可以在執行應用程式的同一節點上找到資料。

讓我們看一下如何使用嵌入式Hazelcast快取拓撲和Spring應用程式構建叢集。Hazelcast支援許多用於快取的分散式資料結構。我們將使用Map,因為它提供了眾所周知的get 和put操作。

首先,我們必須新增Hazelcast依賴項。Hazelcast只是一個Java庫,因此可以很容易地完成(Gradle表示法):

compile group: 'com.hazelcast', name: 'hazelcast', version: '4.0.1'

現在讓我們為應用程式建立一個快取客戶端。

@Component
public class CacheClient {

    public static final String CARS = "cars";
    private final HazelcastInstance hazelcastInstance
                            = Hazelcast.newHazelcastInstance();

    public Car put(String number, Car car){
        IMap<String, Car> map = hazelcastInstance.getMap(CARS);
        return map.putIfAbsent(number, car);
    }

    public Car get(String key){
        IMap<String, Car> map = hazelcastInstance.getMap(CARS);
        return map.get(key);
    }
   
   // other methods omitted

}

現在,應用程式具有分散式快取。該程式碼最重要的部分是建立叢集成員。它通過呼叫Hazelcast.newHazelcastInstance()方法來建立。

當我們要擴充套件應用程式時,每個新例項都將建立一個新成員,並且該成員將自動加入叢集。

Hazelcast提供了多種發現成員的機制。如果我們未配置任何發現機制,則使用預設機制,即Hazelcast嘗試使用多播在同一網路中查詢其他成員。

這種方法有兩個優點:

  • 設定叢集非常容易,並且
  • 資料訪問非常快。

我們不需要設定單獨的快取叢集。這意味著我們可以通過新增幾行程式碼來非常快速地建立叢集。

如果我們想從叢集中讀取資料,則資料訪問是低延遲的,因為我們不需要通過網路向快取叢集傳送請求。

但這也帶來了弊端。假設我們有一個系統,該系統需要一百個應用程式例項。在此叢集拓撲中,這意味著即使我們不需要叢集成員,我們也將擁有一百個叢集成員。如此大量的快取成員將消耗大量記憶體。

而且,複製和同步將非常昂貴。每當在快取中新增或更新條目時,該條目就會與群集的其他成員同步,這會導致大量網路通訊。

另外,我們必須注意Hazelcast是一個Java庫。這意味著該成員只能嵌入在Java應用程式中。

當我們必須使用快取中的資料執行高效能運算時,應使用嵌入式快取拓撲。

我們可以通過將一個Config物件傳遞給factory方法來定製配置快取。讓我們看幾個配置引數:

@Component
public class CacheClient {

    public static final String CARS = "cars";
    private final HazelcastInstance hazelcastInstance 
         = Hazelcast.newHazelcastInstance(createConfig());

    public Config createConfig() {
        Config config = new Config();
        config.addMapConfig(mapConfig());
        return config;
    }

    private MapConfig mapConfig() {
        MapConfig mapConfig = new MapConfig(CARS);
        mapConfig.setTimeToLiveSeconds(360);
        mapConfig.setMaxIdleSeconds(20);
        return mapConfig;
    }
    
    // other methods omitted
}

我們可以分別配置Map叢集中的每個或其他資料結構。

通過setTimeToLiveSeconds(360)我們定義條目在快取中保留的時間。360秒後,該條目將被逐出。如果條目被更新,逐出時間將再次重置為0。

該方法setMaxIdleSeconds(20)定義條目在快取中停留多長時間而不會被訪問。每次讀取操作都會“訪問”一個條目。如果20秒鐘內未訪問任何條目,則該條目將被驅逐。

客戶端-伺服器拓撲

這種拓撲結構意味著我們建立了一個單獨的快取叢集,而我們的應用程式就是該叢集的客戶端。

結合Hazelcast和Spring的分散式快取 - reflectoring

成員形成一個單獨的群集,客戶端從外部訪問該群集。

為了構建叢集,我們可以建立一個設定Hazelcast成員的Java應用程式,但是在此示例中,我們將使用準備好的Hazelcast 伺服器

或者,我們可以啟動Docker容器 作為叢集成員。每個伺服器或每個Docker容器都將使用預設配置啟動叢集的新成員。

現在,我們需要建立一個客戶端來訪問快取叢集。Hazelcast使用TCP套接字通訊。這就是為什麼不僅可以使用Java建立客戶端的原因。Hazelcast提供了用其他語言編寫的客戶列表。為簡單起見,讓我們看看如何使用Spring建立客戶端。

首先,我們將依賴項新增到Hazelcast客戶端:

compile group: 'com.hazelcast', name: 'hazelcast', version: '4.0.1'

接下來,我們在Spring應用程式中建立一個Hazelcast客戶端,類似於對嵌入式快取拓撲所做的操作:

@Component
public class CacheClient {

    private static final String CARS = "cars";

    private HazelcastInstance client = HazelcastClient.newHazelcastClient();

    public Car put(String key, Car car){
        IMap<String, Car> map = client.getMap(CARS);
        return map.putIfAbsent(key, car);
    }

    public Car get(String key){
        IMap<String, Car> map = client.getMap(CARS);
        return map.get(key);
    }
    
    // other methods omitted

}

要建立Hazelcast客戶端,我們需要呼叫方法 HazelcastClient.newHazelcastClient()。Hazelcast將自動找到快取群集。之後,我們可以通過Map 再次使用快取。如果我們向map放入資料或從地圖獲取資料,則Hazelcast客戶端會連線群集以訪問資料。

現在,我們可以獨立部署和擴充套件應用程式和快取叢集。例如,我們可以有50個應用程式例項和5個快取叢集成員。這是這種拓撲的最大優勢。

如果我們在叢集中遇到一些問題,則由於客戶端和快取是分開的而不是混合的,因此更容易識別和解決此問題。

但是,這種方法也有缺點。

首先,無論何時從叢集寫入或讀取資料,我們都需要網路通訊。與嵌入式快取相比,它可能需要更長的時間。這種差異對於讀取操作尤其重要。

其次,我們必須注意叢集成員與客戶端之間的版本相容性。

當應用程式的部署大於群集快取時,我們應該使用客戶端-伺服器拓撲。

由於我們的應用程式現在僅包含快取的客戶端,而不包含快取本身,因此我們需要在測試中啟動快取例項。我們可以使用Hazelcast Docker映像Testcontainers輕鬆地做到這一點(請參閱GitHub上的示例)。

Near近快取

當使用客戶端-伺服器拓撲時,我們正在產生網路流量以從快取中請求資料。它在兩種情況下發生:

  • 客戶端從快取成員讀取資料時,以及
  • 當快取記憶體成員開始與其他快取記憶體成員進行通訊以同步快取記憶體中的資料時。

我們可以通過使用近快取來避免這種缺點。

Near-cache是​​在Hazelcast成員或客戶端上建立的本地快取。讓我們看一下在hazelcast客戶端上建立近快取時的工作方式:

結合Hazelcast和Spring的分散式快取 - reflectoring

每個客戶端都建立其近快取。當應用程式從快取請求資料時,它首先在近快取中查詢資料。如果找不到資料,我們稱其為快取記憶體未命中。在這種情況下,資料是從遠端快取群集中請求的,並新增到了近快取中。當應用程式想要再次讀取此資料時,可以在近快取中找到它。我們稱此為快取命中。

因此,near近快取是二級快取-或“快取的快取”。

我們可以在Spring應用程式中輕鬆配置近快取:

@Component
public class CacheClient {

    private static final String CARS = "cars";

    private HazelcastInstance client 
       = HazelcastClient.newHazelcastClient(createClientConfig());

    private ClientConfig createClientConfig() {
        ClientConfig clientConfig = new ClientConfig();
        clientConfig.addNearCacheConfig(createNearCacheConfig());
        return clientConfig;
    }

    private NearCacheConfig createNearCacheConfig() {
        NearCacheConfig nearCacheConfig = new NearCacheConfig();
        nearCacheConfig.setName(CARS);
        nearCacheConfig.setTimeToLiveSeconds(360);
        nearCacheConfig.setMaxIdleSeconds(60);
        return nearCacheConfig;
    }
    
    // other methods omitted

}

方法createNearCacheConfig()建立Near快取的配置。通過呼叫將配置新增到Hazelcast客戶端配置clientConfig.addNearCacheConfig()。請注意,這僅是此客戶端上的Near快取的配置。每個客戶端都必須自己配置Near-cache。

使用近快取,我們可以減少網路流量。但重要的是要了解我們必須接受可能的資料不一致。由於近快取具有自己的配置,因此它將根據此配置逐出資料。如果資料在快取叢集中被更新或收回,我們仍然可以在近快取中儲存陳舊的資料。稍後將根據驅逐配置將這些資料逐出,然後我們將獲得快取未命中。只有從近快取中逐出資料後,才會再次從快取叢集中讀取資料。

當我們非常頻繁地從快取記憶體中讀取資料時,並且當快取記憶體群集中的資料很少變化時,我們應該使用Near快取記憶體。

 

相關文章