Spring Boot集成Hazelcast實現集群與分布式內存緩存
Hazelcast是Hazelcast公司開源的一款分布式內存數據庫產品,提供彈性可擴展、高性能的分布式內存計算。並通過提供諸如Map,Queue,ExecutorService,Lock和JCache等Java的許多開發人員友好的分布式實現。
了解Hazelcast
Hazelcast特性
- 簡單易用
Hazelcast是用Java編寫的,沒有其他依賴關系。只需簡單的把jar包引入項目的classpath即可創建集群。 - 無主從模式
與許多NoSQL解決方案不同,Hazelcast節點是點對點的。沒有主從關系; 所有成員都存儲相同數量的數據,並進行相等的處理,避免了單點故障。 - 彈性可擴展
Hazelcast旨在擴展成千上萬的成員。新成員啟動,將自動發現群集,並線性增加存儲和處理能力。成員之間通過TCP保持連接和通訊。 - 讀寫快速高效
Hazelcast所有數據都存儲在內存中,提供基於內存快速高效的讀寫能力。
Hazelcast部署拓撲
在Hazelcast官方提供兩種方式部署集群(圖片均來自官方文檔):
如需聚焦異步或高性能大批量任務的緩存服務,嵌入式方式是相對有優勢的,最明顯嵌入式方式訪問數據延遲性低。
獨立創建Hazelcast集群,統一管理,所有的應用程序如果需要訪問緩存,可通過Hazelcast客戶端(有java .NET C++的實現)或Memcache客戶端或簡單的REST客戶端訪問。後續demo示例以嵌入式為例。
Hazelcast數據分區
在Hazelcast分布式環境中,默認情況下,Hazelcast有271個分區。
當啟動第一個成員的時候,成員1在集群中的分區如下圖:
當在集群中新添加一個節點2時,分區圖如下:
在圖示中,黑色分區是主分區,藍色分區是副本分區(備份)。第一個成員具有135個主分區(黑色),並且每個分區都備份在第二個成員(藍色)中。同時,第一個成員還具有第二個成員的主分區的副本分區。
隨著成員的增多,Hazelcast將一些主要和副本分區逐個移動到新成員,使所有成員相等和冗余。只有最小量的分區將被移動到擴展Hazelcast。以下是具有四個成員的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,首次訪問會讓你配置個用戶名密碼,進入後 :
在左側菜單欄,能看到現有支持的分布式數據格式,比如Maps下面名為instruments的是我們前面demo自己創建的,名為spring:session:sessions是我們用了Hazelcast做集群會話同步的時候Spring為我們創建的。
中間區域能看到所有節點成員的系統相關實時使用率,隨便點擊一個節點進去,能看到當前節點的系統實時使用率:
紅圈裏面的即是上面提到的節點數據分區數,通過左側菜單欄的數據結構進去,能看到當前對應的數據結構的詳細信息和實時吞吐量:
更多內容請參考下方參考資料。
示例代碼可以通過https://github.com/zggg/hazelcast-in-spring-boot下載。
參考資料
- 為什麽選Hazelcast:https://hazelcast.com/why-hazelcast/imdg/
- Hazelcast官方文檔:http://docs.hazelcast.org/docs/3.8.6/manual/html-single/index.html
- Redis對比:https://hazelcast.com/use-cases/nosql/redis-replacement/
- Redis 3.2.8 vs Hazelcast 3.8 集群基準測試對比:https://hazelcast.com/resources/benchmark-redis-vs-hazelcast/
——————————————————分割線——————————————————
我是黑少,直男一枚,微服務硬核玩家,喜歡分享、愛交友人、崇尚“實踐出真知”的理念,以折騰鼓搗代碼為樂
我的微信:heishao (備註:博客園 )
微信公號:黑少微服務,專註微服務技術分享,非技術不八卦!
Spring Boot集成Hazelcast實現集群與分布式內存緩存