Spring Boot集成Hazelcast實現集羣與分佈式內存緩存

Hazelcast是Hazelcast公司開源的一款分佈式內存數據庫產品,提供彈性可擴展、高性能的分佈式內存計算。並經過提供諸如Map,Queue,ExecutorService,Lock和JCache等Java的許多開發人員友好的分佈式實現。html

瞭解Hazelcast

Hazelcast特性前端

  • 簡單易用 Hazelcast是用Java編寫的,沒有其餘依賴關係。只需簡單的把jar包引入項目的classpath便可建立集羣。
  • 無主從模式 與許多NoSQL解決方案不一樣,Hazelcast節點是點對點的。沒有主從關係; 全部成員都存儲相同數量的數據,並進行相等的處理,避免了單點故障。
  • 彈性可擴展 Hazelcast旨在擴展成千上萬的成員。新成員啓動,將自動發現羣集,併線性增長存儲和處理能力。成員之間經過TCP保持鏈接和通信。
  • 讀寫快速高效 Hazelcast全部數據都存儲在內存中,提供基於內存快速高效的讀寫能力。

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

如需聚焦異步或高性能大批量任務的緩存服務,嵌入式方式是相對有優點的,最明顯嵌入式方式訪問數據延遲性低。 git

獨立建立Hazelcast集羣,統一管理,全部的應用程序若是須要訪問緩存,可經過Hazelcast客戶端(有java .NET C++的實現)或Memcache客戶端或簡單的REST客戶端訪問。後續demo示例以嵌入式爲例。github

Hazelcast數據分區 在Hazelcast分佈式環境中,默認狀況下,Hazelcast有271個分區。 當啓動第一個成員的時候,成員1在集羣中的分區以下圖: web

當在集羣中新添加一個節點2時,分區圖以下: redis

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

隨着成員的增多,Hazelcast將一些主要和副本分區逐個移動到新成員,使全部成員相等和冗餘。只有最小量的分區將被移動到擴展Hazelcast。如下是具備四個成員的Hazelcast集羣中的分區圖示以下: sql

Hazelcast在羣集成員之間平均分配分區。Hazelcast建立分區的備份,並將其分配給成員之間進行冗餘。docker

上述插圖中的分區是爲了方便描述。一般,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,首次訪問會讓你配置個用戶名密碼,進入後

在左側菜單欄,能看到現有支持的分佈式數據格式,好比Maps下面名爲instruments的是咱們前面demo本身建立的,名爲spring:session:sessions是咱們用了Hazelcast作集羣會話同步的時候Spring爲咱們建立的。

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

紅圈裏面的便是上面提到的節點數據分區數,經過左側菜單欄的數據結構進去,能看到當前對應的數據結構的詳細信息和實時吞吐量:

更多內容請參考下方參考資料。 示例代碼能夠經過https://github.com/zggg/hazelcast-in-spring-boot下載。

參考資料

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

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

個人微信:weiweiweiblack (備註:開源中國)

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

相關文章
相關標籤/搜索