Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

heishaovvv發表於2019-02-28

Hazelcast是Hazelcast公司開源的一款分散式記憶體資料庫產品,提供彈性可擴充套件、高效能的分散式記憶體計算。並通過提供諸如Map,Queue,ExecutorService,Lock和JCache等Java的許多開發人員友好的分散式實現。

瞭解Hazelcast

Hazelcast特性

  • 簡單易用
    Hazelcast是用Java編寫的,沒有其他依賴關係。只需簡單的把jar包引入專案的classpath即可建立叢集。
  • 無主從模式
    與許多NoSQL解決方案不同,Hazelcast節點是點對點的。沒有主從關係; 所有成員都儲存相同數量的資料,並進行相等的處理,避免了單點故障。
  • 彈性可擴充套件
    Hazelcast旨在擴充套件成千上萬的成員。新成員啟動,將自動發現群集,併線性增加儲存和處理能力。成員之間通過TCP保持連線和通訊。
  • 讀寫快速高效
    Hazelcast所有資料都儲存在記憶體中,提供基於記憶體快速高效的讀寫能力。

Hazelcast部署拓撲
在Hazelcast官方提供兩種方式部署叢集(圖片均來自官方文件):

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

如需聚焦非同步或高效能大批量任務的快取服務,嵌入式方式是相對有優勢的,最明顯嵌入式方式訪問資料延遲性低。

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

獨立建立Hazelcast叢集,統一管理,所有的應用程式如果需要訪問快取,可通過Hazelcast客戶端(有java .NET C++的實現)或Memcache客戶端或簡單的REST客戶端訪問。後續demo示例以嵌入式為例。

Hazelcast資料分割槽
在Hazelcast分散式環境中,預設情況下,Hazelcast有271個分割槽。
當啟動第一個成員的時候,成員1在叢集中的分割槽如下圖:

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

當在叢集中新新增一個節點2時,分割槽圖如下:

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

在圖示中,黑色分割槽是主分割槽,藍色分割槽是副本分割槽(備份)。第一個成員具有135個主分割槽(黑色),並且每個分割槽都備份在第二個成員(藍色)中。同時,第一個成員還具有第二個成員的主分割槽的副本分割槽。

隨著成員的增多,Hazelcast將一些主要和副本分割槽逐個移動到新成員,使所有成員相等和冗餘。只有最小量的分割槽將被移動到擴充套件Hazelcast。以下是具有四個成員的Hazelcast叢集中的分割槽圖示如下:

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

Hazelcast在群整合員之間平均分配分割槽。Hazelcast建立分割槽的備份,並將其分配給成員之間進行冗餘。

上述插圖中的分割槽是為了方便描述。通常,Hazelcast分割槽不會按照順序分配(如這些圖所示),而是隨機分佈。Hazelcast在成員間平均分配了分割槽和備份。

Hazelcast優勢

  • Hazelcast提供開源版本。
  • Hazelcast無需安裝,只是個極小jar包。
  • Hazelcast提供開箱即用的分散式資料結構,如Map,Queue,MultiMap,Topic,Lock和Executor。
  • Hazelcast叢集非傳統主從關係,避免了單點故障;叢集中所有成員共同分擔叢集功能。
  • Hazelcast叢集提供彈性擴充套件,新成員在記憶體不足或負載過高時能動態加入叢集。
  • Hazelcast叢集中成員分擔資料快取的同時互相冗餘備份其他成員資料,防止某成員離線後資料丟失。
  • Hazelcast提供SPI介面支援使用者自定義分散式資料結構。

Hazelcast適用場景

  • 頻繁讀寫資料
  • 需要高可用分散式快取
  • 記憶體行NoSql儲存
  • 分散式環境中彈性擴充套件

下面我們來使用Spring Boot整合Hazelcast實現分散式叢集服務看看

Spring Boot整合Hazelcast實現分散式叢集服務

首先新建一個Spring Boot的gradle專案,引入Hazelcast相關jar包:

dependencies {
   compile `com.hazelcast:hazelcast`
   compile `org.springframework.boot:spring-boot-starter-web`
}
複製程式碼

當Hazelcast包在classpath上,Spring Boot將通過下面兩種方式之一為我們建立一個HazelcastInstance例項:

方式一,通過配置屬性指定的Hazelcast.xml檔案建立:
spring.hazelcast.config = classpath:hazelcast.xml
該方式需要編寫一個hazelcast.xml檔案,通過xml檔案描述Hazelcast叢集

方式二,通過提供一個com.hazelcast.config.Config javabean到Spring容器中(下面所有demo是基於java config方式)

@Bean
   public Config hazelCastConfig() {
       //如果有叢集管理中心,可以配置
       ManagementCenterConfig centerConfig = new ManagementCenterConfig();
       centerConfig.setUrl("http://127.0.0.1:8200/mancenter");
       centerConfig.setEnabled(true);
       return new Config()
               .setInstanceName("hazelcast-instance")
               .setManagementCenterConfig(centerConfig)
               .addMapConfig(
                       new MapConfig()
                               .setName("instruments")
                               .setMaxSizeConfig(new MaxSizeConfig(200, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
                               .setEvictionPolicy(EvictionPolicy.LRU)
                               .setTimeToLiveSeconds(20000));
   }
複製程式碼

上面程式碼通過提供Config的bean時候,主要做了如下幾個事:

  • 建立一個預設名為hazelcast-instance的HazelcastInstance例項;
  • 使用預設的組播發現模式,組播傳播地址預設為:224.2.2.3,如果想修改資訊或修改為TCP模式可通過setNetworkConfig()介面設定相關資訊;
  • 建立一個名為dev,訪問密碼為dev-pass的group保障節點加入,如果想修改組,可通過setGroupConfig()介面設定相關資訊;
  • 建立了一個名為instruments的分散式map資料結構,並設定了該map的最大容量200/逐出策略LRU/有效期20000ms等資訊,當叢集啟動後,我們可以在任一成員節點上通過HazelcastInstance讀寫該map。

完整程式碼:

@SpringBootApplication
public class StartUp {

   private Logger LOGGER = LoggerFactory.getLogger(StartUp.class);

   public static void main(String[] args) {
       SpringApplication.run(StartUp.class, args);
   }

   @Bean
   public Config hazelCastConfig() {
       //如果有叢集管理中心,可以配置
       ManagementCenterConfig centerConfig = new ManagementCenterConfig();
       centerConfig.setUrl("http://127.0.0.1:8200/mancenter");
       centerConfig.setEnabled(true);
       return new Config()
               .setInstanceName("hazelcast-instance")
               .setManagementCenterConfig(centerConfig)
               .addMapConfig(
                       new MapConfig()
                               .setName("instruments")
                               .setMaxSizeConfig(new MaxSizeConfig(200, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
                               .setEvictionPolicy(EvictionPolicy.LRU)
                               .setTimeToLiveSeconds(20000));
   }
}
複製程式碼

下面我們通過修改server.port分別啟動埠為8080和8081的成員服務
當啟動完8080成員的時候,可以在8080控制檯看到如下日誌:

Members [1] {
   Member [172.17.42.1]:5701 - 0d39dd66-d4fb-4af4-8ddb-e9f4c7bbe5a1 this
}
複製程式碼

因我們使用的是組播傳播模式,5701為節點在組播網路中分配的埠
當啟動完8081成員的時候,可以在8081控制檯看到如下日誌:

Members [2] {
   Member [172.17.42.1]:5701 - 0d39dd66-d4fb-4af4-8ddb-e9f4c7bbe5a1
   Member [172.17.42.1]:5702 - a46ceeb4-e079-43a5-9c9d-c74265211bf7 this
}
複製程式碼

回到8080控制檯,發現多了一行日誌:

Members [2] {
   Member [172.17.42.1]:5701 - 0d39dd66-d4fb-4af4-8ddb-e9f4c7bbe5a1 this
   Member [172.17.42.1]:5702 - a46ceeb4-e079-43a5-9c9d-c74265211bf7
}
複製程式碼

發現8081成員也加入進來了。兩個控制檯都能看到成員列表。叢集就已經搭建成功。

為了驗證結果,上面我們在叢集中已經建立了一個名為instruments的分散式map資料結構,現在我們通過寫個介面證明:

@GetMapping("/greet")
   public Object greet() {
       Object value = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").get("hello");
       if (Objects.isNull(value)) {
           Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").put("hello", "world!");

       }        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }
複製程式碼

首先通過訪問8080服務的/greet,第一次訪問instruments中是沒有key為hello的鍵值對,會往裡面塞入{“helo”:”world!”},然後訪問8081服務的/greet,這個時候應該是能取得改鍵值對的。

完整程式碼:

@RestController
@SpringBootApplication
public class StartUp {

   private Logger LOGGER = LoggerFactory.getLogger(StartUp.class);

   public static void main(String[] args) {
       SpringApplication.run(StartUp.class, args);
   }

   @Bean
   public Config hazelCastConfig() {
       //如果有叢集管理中心,可以配置
       ManagementCenterConfig centerConfig = new ManagementCenterConfig();
       centerConfig.setUrl("http://127.0.0.1:8200/mancenter");
       centerConfig.setEnabled(true);
       return new Config()
               .setInstanceName("hazelcast-instance")
               .setManagementCenterConfig(centerConfig)
               .addMapConfig(
                       new MapConfig()
                               .setName("instruments")
                               .setMaxSizeConfig(new MaxSizeConfig(200, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
                               .setEvictionPolicy(EvictionPolicy.LRU)
                               .setTimeToLiveSeconds(20000));
   }


   @GetMapping("/greet")
   public Object greet() {
       Object value = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").get("hello");
       if (Objects.isNull(value)) {
           Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").put("hello", "world!");

       }        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }
}
複製程式碼

重啟8080和8081服務
通過瀏覽器請求http://localhost:8080/greet
檢視8080控制檯日誌:
2017-10-23 13:52:27.865 INFO 13848 --- [nio-8080-exec-1] com.hazelcast.StartUp: 從分散式快取獲取到 key=hello,value=nul

通過瀏覽器請求http://localhost:8081/greet
檢視8081控制檯日誌:
2017-10-23 13:52:40.116 INFO 13860 --- [nio-8081-exec-2] com.hazelcast.StartUp: 從分散式快取獲取到 key=hello,value=world

Spring Boot為Hazelcast提供了明確的快取支援。如果啟用快取, HazelcastInstance則會自動包含在CacheManager實現中。所以完全可以支援Spring Cache。

以往我們用Spring Cache都是基於Redis做儲存後端,現在我們使用Hazelcast來嘗試一下 首先在啟動類上開啟快取
@EnableCaching

建立個service類,demo為了方便,寫在一起
完整程式碼:

@EnableCaching
@RestController
@SpringBootApplication
public class StartUp {

   private Logger LOGGER = LoggerFactory.getLogger(StartUp.class);

   public static void main(String[] args) {
       SpringApplication.run(StartUp.class, args);
   }

   @Bean
   public Config hazelCastConfig() {
       //如果有叢集管理中心,可以配置
       ManagementCenterConfig centerConfig = new ManagementCenterConfig();
       centerConfig.setUrl("http://127.0.0.1:8200/mancenter");
       centerConfig.setEnabled(true);
       return new Config()
               .setInstanceName("hazelcast-instance")
               .setManagementCenterConfig(centerConfig)
               .addMapConfig(
                       new MapConfig()
                               .setName("instruments")
                               .setMaxSizeConfig(new MaxSizeConfig(200, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
                               .setEvictionPolicy(EvictionPolicy.LRU)
                               .setTimeToLiveSeconds(20000));
   }


   @GetMapping("/greet")
   public Object greet() {
       Object value = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").get("hello");
       if (Objects.isNull(value)) {
           Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").put("hello", "world!");

       }        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }

   @Autowired
   private DemoService demoService;

   @GetMapping("/cache")
   public Object cache() {
       String value = demoService.greet("hello");        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }

}

@Service
@CacheConfig(cacheNames = "instruments")
class DemoService {

   private Logger LOGGER = LoggerFactory.getLogger(DemoService.class);

   @Cacheable(key = "#key")
   public String greet(String key) {        LOGGER.info("快取內沒有取到key={}", key);
       return "world!";
   }

}
複製程式碼

連續訪問兩次8080服務的/cache介面 第一次控制檯輸出日誌:

2017-10-23 14:10:02.201  INFO 13069 --- [nio-8081-exec-1] com.hazelcast.DemoService: 快取內沒有取到key=hello
2017-10-23 14:10:02.202  INFO 13069 --- [nio-8081-exec-1] com.hazelcast.StartUp: 從分散式快取獲取到 key=hello,value=world!
複製程式碼

第二次控制檯輸出日誌:
2017-10-23 14:11:51.966 INFO 13069 --- [nio-8081-exec-3] com.hazelcast.StartUp: 從分散式快取獲取到 key=hello,value=world!

第二次比第一次相比少了執行service方法體內容,證明第二次是通過了快取獲取。

  • 在Hazelcast官網上,有使用Hazelcast叢集和Redis叢集做快取的對比
  • 單隻效能上來說,寫入速度Hazelcast比Redis快44%,讀取速度Hazelcast比Redis快56%
  • 詳情移步底下參考資料中連結
  • 下面,我們再來一個嘗試,既然有分散式快取了,我們可以把我們的8080和8081服務做成一個web叢集,web服務叢集主要標誌是前端負載均衡和session共享,我們來實現8080和8081的session共享。

Spring Session已經支援使用Hazelcast作為會話快取後端,首先引入Spring Session jar包

dependencies {
   compile `com.hazelcast:hazelcast`
   compile `org.springframework.boot:spring-boot-starter-web`
   compile `org.springframework.session:spring-session`
}
複製程式碼

要啟用Hazelcast作為叢集會話快取後端,有兩種方式
第一種Spring Boot配置檔案裡面配置spring.session.*屬性:
spring.session.store-type=hazelcast

第二種使用java註解開啟:
@EnableHazelcastHttpSession

這裡選擇第二種方式,要證明叢集會話共享,我們定一個簡單介面列印一下sessionId,通過同一瀏覽器訪問8080和8081服務的該介面,看看不同服務請求的時候sessionId是否一致,完整程式碼如下:

@EnableCaching
@RestController
@EnableHazelcastHttpSession
@SpringBootApplication
public class StartUp {

   private Logger LOGGER = LoggerFactory.getLogger(StartUp.class);

   public static void main(String[] args) {
       SpringApplication.run(StartUp.class, args);
   }

   @Bean
   public Config hazelCastConfig() {
       //如果有叢集管理中心,可以配置
       ManagementCenterConfig centerConfig = new ManagementCenterConfig();
       centerConfig.setUrl("http://127.0.0.1:8200/mancenter");
       centerConfig.setEnabled(true);
       return new Config()
               .setInstanceName("hazelcast-instance")
               .setManagementCenterConfig(centerConfig)
               .addMapConfig(
                       new MapConfig()
                               .setName("instruments")
                               .setMaxSizeConfig(new MaxSizeConfig(200, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
                               .setEvictionPolicy(EvictionPolicy.LRU)
                               .setTimeToLiveSeconds(20000));
   }


   @GetMapping("/greet")
   public Object greet() {
       Object value = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").get("hello");
       if (Objects.isNull(value)) {
           Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("instruments").put("hello", "world!");

       }        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }

   @Autowired
   private DemoService demoService;

   @GetMapping("/cache")
   public Object cache() {
       String value = demoService.greet("hello");        LOGGER.info("從分散式快取獲取到 key=hello,value={}", value);
       return value;
   }

   @GetMapping("/session")
   public Object session(HttpSession session) {
       String sessionId = session.getId();        LOGGER.info("當前請求的sessionId={}", sessionId);
       return sessionId;
   }
}

@Service
@CacheConfig(cacheNames = "instruments")
class DemoService {

   private Logger LOGGER = LoggerFactory.getLogger(DemoService.class);

   @Cacheable(key = "#key")
   public String greet(String key) {        LOGGER.info("快取內沒有取到key={}", key);
       return "world!";
   }

}
複製程式碼

訪問8080服務/session介面,控制檯日誌如下:
2017-10-23 14:28:41.991 INFO 14140 --- [nio-8080-exec-2] com.hazelcast.StartUp: 當前請求的sessionId=e75ffc53-90bc-41cd-8de9-e9ddb9c2a5ee

訪問8081服務/session介面,控制檯日誌如下:
2017-10-23 14:28:45.615 INFO 14152 --- [nio-8081-exec-1] com.hazelcast.StartUp: 當前請求的sessionId=e75ffc53-90bc-41cd-8de9-e9ddb9c2a5ee
叢集會話共享生效。

叢集管理介面

在上面的demo中,在建立Config的時候,設定了一個ManagementCenterConfig配置,該配置是指向一個Hazelcast叢集管理平臺,比如demo中表示在本地啟動了一個管理平臺服務。該功能也是相對其他NoSql服務的一個優勢。

要部署ManagementCenter管理平臺有多種方式
比如通過https://download.hazelcast.com/management-center/management-center-3.8.3.zip地址下載,解壓後啟動;
sh ./startManCenter.sh 8200 /mancenter

如果有docker環境,直接可以docker部署:
docker run -ti -p 8200:8080 hazelcast/management-center:latest

部署成功後,訪問http://ip:8200/mancenter,首次訪問會讓你配置個使用者名稱密碼,進入後 :

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

在左側選單欄,能看到現有支援的分散式資料格式,比如Maps下面名為instruments的是我們前面demo自己建立的,名為spring:session:sessions是我們用了Hazelcast做叢集會話同步的時候Spring為我們建立的。

中間區域能看到所有節點成員的系統相關實時使用率,隨便點選一個節點進去,能看到當前節點的系統實時使用率:

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

紅圈裡面的即是上面提到的節點資料分割槽數,通過左側選單欄的資料結構進去,能看到當前對應的資料結構的詳細資訊和實時吞吐量:

Spring Boot整合Hazelcast實現叢集與分散式記憶體快取

更多內容請參考下方參考資料。
示例程式碼可以通過https://github.com/zggg/hazelcast-in-spring-boot下載。

參考資料

——————————————————分割線——————————————————

我是黑少,直男一枚,微服務硬核玩家,喜歡分享、愛交友人、崇尚“實踐出真知”的理念,以折騰鼓搗程式碼為樂

我的微信:weiweiweiblack (備註:掘金 )

微信公號:黑少微服務,專注微服務技術分享,非技術不八卦!

相關文章