Hazelcast简介
Hazelcast 是一个分布式计算和缓存平台。Hazelcast 采用 Java 语言实现,拥有 Java、C++、.NET、REST、Python、Go 和 Node.js 客户端。Hazelcast 还支持 Memcached 和 REST 协议。它最常使用的场景:在服务器A上缓存的数据,可以在服务器B上直接使用,感觉就像用本地缓存一样。
其主要功能有:
- 提供了 Map、Queue、MultiMap、Set、List、Semaphore、Atomic 等接口的分布式实现
- 提供了基于Topic 实现的消息队列或订阅\发布模式;
- 提供了分布式id生成器(IdGenerator)
- 提供了分布式事件驱动(Distributed Events)
- 提供了分布式计算(Distributed Computing)
- 提供了分布式查询(Distributed Query)
官网文档地址:
docs.hazelcast.com/hazelcast/l…
课外知识:
Hazelcast缓存框架背后的公司也是叫Hazelcast。
Hazelcast公司的名称来源于两个单词:Hazel和cast。Hazel是一种植物(榛树),而cast则表示将数据从一个地方传输到另一个地方。因此,Hazelcast的名称意味着将数据从一个地方传输到另一个地方,就像植物的花粉一样。
重要说明:
Hazelcast 不再支持 JDK 8 作为 Hazelcast 5.3.0 及更高版本的运行时。需要JDK 11+。
JDK8所能支持的最高版本为Hazelcast5.2.X版本。所以本文以最新版本Hzelcast5.2.4版本为例进行讲述
虽然,我在JDK8中使用最新版本Hazelcast5.3.1没有遇到问题,但是避免不必要的麻烦,如果用JDK8,那么最高就不要超过5.2版本
Hazelcast版本区别
Hazelcast 分为开源版和商用版,开源版本遵循 Apache License 2.0 开源协议可以免费使用,商用版本需要获取特定的License。 两者之间最大的区别在于:商用版本提供了数据的高密度存储。
我们知道在JVM中,有自己特定的GC机制,无论数据是在堆中还是栈中,只要发现无效引用的数据块,就有可能被回收。而Hazelcast的分布式数据都存放在JVM的内存中,频繁的读写数据会导致大量的GC开销。使用商业版的Hazelcast会拥有高密度存储的特性,大大降低JVM的内存开销,从而降低GC开销。
商用版本初提供高密度存储外,还提供了更多的数据结构、更好的性能、更好的可扩展性、更好的安全性等。此外,商用版本还提供了更好的支持和服务,例如,技术支持、培训、咨询等。
针对springboot项目,开源版和商用版不同的引入方式:
-
开源版
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>5.2.4</version> </dependency>
-
商用版
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-enterprise</artifactId> <version>5.2.4</version> </dependency>
hazelcast集群方式
1.嵌入式方式
这种嵌入式方式集群在springboot项目中非常的简单,由于hazelcast本身就是java编写的,我们在pom文件中引入hazelcast,几乎不需要什么代码,就能把带有集群特性的hazelcast跑起来。当一台机器上改写缓存记录时,hazelcast负责将改动分发到集群中的其他机器中。而加入集群的发现机制,如果我们在不配置的情况下,会采用多播方式查找同一网络中的其他成员。
该方式的优点:
-
设置集群容易
上面用描述过这种方式非常的简单,只需要几行代码或者几句配置就能创建集群。
-
数据访问非常快
访问数据的时候,由于访问的是本地缓存,没有网络开销,所以访问非常快
该方式的缺点:
-
复制和同步代价很大
缓存中添加或更新记录时,该记录都会与集群的其他成员同步,这会导致大量的网络通信。
-
消耗大量的内存
当集群中的节点过多时,由于集群中的每个成员机,都会有其他成员的部分或者全部备份。那么会消耗大量的内存。
-
仅能用于java
springboot项目中引入一个jar包,就能实现集群,这种方式仅仅用于java,其他语言无法引入jar包。
hazelcast关键配置:
Hazelcast.newHazelcastInstance();
2.客户端-服务器方式
这里member1到member4是Hazelcast集群的成员,他们构成了缓存集群。Hazelcast是通过外部访问的形式,和该集群进行通讯。相当于现在springboot程序,是一个客户端来访问Hazelcast集群。Hazelcast 使用 TCP socket通信。这时,就不仅仅可以使用java来访问这个缓存集群了。
该方式的优点:
-
缓存具有更好的扩展性
这里的缓存集群是独立的,避免了上面说的缓存浪费的情况,而且便于扩展集群数量。
-
更容易找到问题
由于缓存和程序是独立的,如果发生异常时,便于程序定位问题。
-
可以针对不同语言的客户端
客户端可以不仅仅是java语言,还能支持REST、Python、Go 和 Node.js 客户端。
该方式的缺点:
-
通讯时间更长
由于客户端和缓存集群有网络通讯的开销,所以可能会比嵌入式花费更长的时间。
-
版本兼容性问题
必须注意缓存集群和客户端之间的Hazelcast版本兼容性问题。
hazelcast关键配置:
HazelcastClient.newHazelcastClient();
3.二级缓存方式
其实就是把上面两种方式进行结合。在取缓存值的时候,会首先从本地缓存读取,如果本地缓存没有找到数据,再从远端的缓存集群中请求数据并且将其添加到本地缓存中。当应用程序想要再次读取该数据时,可以在本地缓存中找到它。这样可以减少网络流量。但是凡事有利有弊,用二级缓存是我们必须接受可能的数据不一致。这是由于本地缓存有自己的缓存配置,它会根据这个配置失效数据。如果缓存集群中的数据被更新或删除,而我们在本地缓存中仍然可能有过时的数据。要跟进实际业务具体情况具体分析。不过一般情况是用不到二级缓存的。
hazelcast关键配置:
HazelcastClient.newHazelcastClient(createClientConfig()); private ClientConfig createClientConfig() { ClientConfig clientConfig = new ClientConfig(); clientConfig.addNearCacheConfig(createNearCacheConfig()); return clientConfig; } private NearCacheConfig createNearCacheConfig() { NearCacheConfig nearCacheConfig = new NearCacheConfig(); nearCacheConfig.setName("mymap"); nearCacheConfig.setTimeToLiveSeconds(360); nearCacheConfig.setMaxIdleSeconds(60); return nearCacheConfig; }
Hazelcast存储数据的实现过程
1.Hazelcast分区
由于Hazelcast 服务之间是端对端的,没有主从之分,集群中所有的节点都存储等量的数据以及进行等量的计算。
Hazelcast 默认情况下把数据存储在 271 个区上,这个值可以通过系统属性 hazelcast.partition.count来配置。
2.Hazelcast分区存储原理
对于一个给定的键,在经过序列化、哈希并对分区总数取模之后能得到此键对应的分区号,所有的分区等量的分布与集群中所有的节点中,每个分区对应的备份也同样分布在集群中。
也就是说 Hazelcast 会使用哈希算法对数据进行分区,比如对于一个给定的map中的键,或者topic和list中的对象名称,分区存储的过程如下:
- 先序列化此键或对象名称,得到一个byte数组;
- 然后对上面得到的byte数组进行哈希运算;
- 再进行取模后的值即为分区号;
- 最后每个节点维护一个分区表,存储着分区号与节点之间的对应关系,这样每个节点都知道如何获取数据。
3.Hazelcast集群实现原理
Hazelcast通过分片来存储和管理所有进入集群的数据,采用分片的方案目标是保证数据可以快速被读写、通过冗余保证数据不会因节点退出而丢失、节点可线性扩展存储能力。下面将从理论上说明Hazelcast是如何进行分片管理的。Hazelcast的每个数据分片(shards)被称为一个分区(Partitions)。分区是一些内存段,根据系统内存容量的不同,每个这样的内存段都包含了几百到几千项数据条目,默认情况下,Hazelcast会把数据划分为271个分区,并且每个分区都有一个备份副本。当启动一个集群成员时,这271个分区将会一起被启动。
-
下图展示了集群只有一个节点时的分区情况。
从一个节点的分区情况可以看出,当只启动一个节点时,所有的271个分区都存放在一个节点中
-
启动第二个节点,会出现下面这样的集群分区方式
其中黑色的字体表示分区,蓝色的字体表示备份。节点1存储了标号为1到135的分区,这些分区会同时备份到节点2中。而节点2则存储了136到271的分区,并备份到了节点1中。
-
再添加2个新的节点到集群中
Hazelcast会一个一个的移动分区和备份到新的节点中,使得集群数据分布平衡。实际中分区并不是有序的分布,而是随机分布,上面的示例只是为了方便理解,重要的是理解 Hazelcast 的平均分布分区以及备份。
这个备份数量是可以设置的:不管是xml,yaml或者java代码,配置都是差不多的,参考java代码如下
config.getMapConfig("my-map").setBackupCount(2);
Hazelcast 默认备份的数量是1个,如果备份数量超过1时,每个节点会存放自己的数据以及其它节点上的备份。
嵌入式方式
1.目标
假设有登录用户的信息需要缓存,为了演示过期方便,设置30秒的有效期。看30秒后Hazelcast是不是自动删除了。这里采用map缓存所有用户信息。map的key是userId,而value就是User对象。
2.编码方式
2.1加入依赖
<!-- Hazelcast -->
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.2.4</version>
<exclusions>
<exclusion>
<artifactId>hazelcast</artifactId>
<groupId>com.hazelcast</groupId>
</exclusion>
</exclusions>
</dependency>
说明:
- 其实在引入hazelcast-spring时,它本身就依赖了hazelcast。这里为什么都引入一遍呢?理由是springboot本身维护了hazelcast的版本。就拿现在做例子用的springboot2.7.10版本来说,它依赖的是hazelcast的5.1.15版本。而我们想使用比较新的hazelcast,所以这里从新指定了两个依赖的版本。
- hazelcast 和 hazelcast-spring 的区别在于,hazelcast-spring 是为了结合 Spring 使用,例如在 xml 配置中使用 <hz:hazelcast id="instance"> 这样的命名空间。而 hazelcast 的主要依赖只有一个,即 hazelcast-5.3.1.jar,引入这一个 jar 理论上就能使用 Hazelcast 了。
- 如果想使用springboot的cache和Hazelcast结合的话,那么引入hazelcast-spring还是不错的。
2.2配置类
@EnableCaching
@Configuration
public class HazelcastConfig {
@Bean
public Config config() {
Config config = new Config();
config.setInstanceName("hazelcast-instance");
config.setClusterName("dev");
// 设置驱逐策略
EvictionConfig evictionConfig = new EvictionConfig();
evictionConfig.setEvictionPolicy(EvictionPolicy.LFU);
evictionConfig.setMaxSizePolicy(MaxSizePolicy.PER_NODE);
evictionConfig.setSize(542);
// 设置map配置
MapConfig mapUserConfig = new MapConfig();
mapUserConfig.setName(Constants.CACHE_NAME_SESSION_USERS)
.setBackupCount(2)
.setTimeToLiveSeconds(30)
.setMaxIdleSeconds(30)
.setEvictionConfig(evictionConfig);
config.addMapConfig(mapUserConfig);
return config;
}
}
驱逐策略:就是当映射的大小超过限制时,缓存的数据会根据策略进行清除。程序代码中设置的是每个节点如果数量达到542个【默认是10000个】,那么按照LFU【最不经常使用】的策略删除缓存中的数据。
Map配置:这里设置备份的数量是2份【setBackupCount(2)
】,设置了生命周期30秒【setTimeToLiveSeconds(30)
】,设置了最大空闲时间30秒【setMaxIdleSeconds(30)
】同时还设置了驱逐策略。过期策略和驱逐策略是可以同时设置的,满足其中的任何一个策略,数据都会被清除。
说明:
1.驱逐策略后面会再单独说明,这里先做个了解即可
2.TimeToLiveSeconds和MaxIdleSeconds的区别这里说一下:
- TimeToLiveSeconds[TTL]:该元素,如果没有写入操作,那么到了这个时间就会被删除
- MaxIdleSeconds[最大空闲时间]:该元素,在设置的时间段内,如果没有get()、put()、EntryProcessor.process()、containsKey()这些访问该元素的操作,那么久回被删除。
2.3controller测试类
@RestController
public class UserCacheTestController {
@Resource
private HazelcastInstance hazelcastInstance;
@PostMapping(value = "/writeuser")
public String writeDataToHazelcast(String userId, String userName) {
UserInfo userInfo = new UserInfo();
userInfo.setUserId(userId);
userInfo.setUserName(userName);
Map<String, Object> hazelcastMap = hazelcastInstance.getMap(Constants.CACHE_NAME_SESSION_USERS);
hazelcastMap.put(userId, userInfo);
return "Map数据写入完成" + JSONUtil.toJsonStr(hazelcastMap.get(userId));
}
@GetMapping(value = "/readoneuser")
//@Cacheable(cacheNames = "session:users", key = "#userId", condition = "#userId != null")
public UserInfo readDataFromHazelcast(String userId) {
Map<String, Object> hazelcastMap = hazelcastInstance.getMap(Constants.CACHE_NAME_SESSION_USERS);
return (UserInfo) hazelcastMap.get(userId);
}
@GetMapping(value = "/readalluser")
public Map<String, Object> readAllDataFromHazelcast() {
return hazelcastInstance.getMap(Constants.CACHE_NAME_SESSION_USERS);
}
}
这个测试controller取我们定义好的map,然后向里面写入值,进行Hazelcast基本的写入和读取操作。
我们可以用springboot的cache,来缓存数据@Cacheable(cacheNames = "session:users", key = "#userId", condition = "#userId != null")
这时需要注意两点:
- pom文件中引入了hazelcast-spring的依赖
- HazelcastConfig类或者springboot启动类上加入注解:@EnableCaching
2.4测试
-
为了能够模拟集群,我们可以设置idea的端口来启动多个springboot程序.
-
当启动两个程序的时候,能在控台中看到如下信息:
-
测试一下TTL
可以发现写入的值在30秒后被自动清除了。
2.5.程序代码
gitee.com/mayuanfei/S…下的springboot12
3.配置文件方式
配置文件方式和java配置类设置的属性名称几乎可以一一对应。在用配置文件实现上述功能前,先了解下Hazelcast配置的优先级。
3.1配置优先级
不管是嵌入方式集群还是客户端-服务器方式集群,都会按照如下顺序由高到低查找配置:
优先级 | 嵌入式 | 客户端 |
---|---|---|
1 | 编码方式配置(上例HazelcastConfig类) | 编码方式配置(上例中HazelcastConfig类) |
2 | 系统属性配置的hazelcast.config 指定的文件 | 系统属性配置的hazelcast.client.config |
3 | 工作目录中的hazelcast.xml | 工作目录中的hazelcast-client.xml |
4 | 类路径上的hazelcast.xml | 类路径上的hazelcast-client.xml |
5 | 工作目录中的hazelcast.yaml | 工作目录中的hazelcast-client.yaml |
6 | 类路径上的hazelcast.yaml | 类路径上的hazelcast-client.yaml |
解释说明:
-
优先级2的解释
-Dhazelcast.config=`*`<hazelcast.xml或者hazelcast.yaml的路径>
该路径可以是常规路径,也可以是带有前缀的类路径比如:
classpath:
。 -
优先级3的解释
工作目录,拿springboot项目打出来的jar包为例,就是和jar包放在一块的那个目录。
-
优先级4的解释
类路径针对springboot项目来说,
src.main.java
和src.main.resources
路径以及第三方jar包
的根路径
3.2配置文件
-
Hazelcast默认配置文件
在hazelcast默认配置文件中,对Map的默认设置总结如下:
- 1个同步备份
- 内存对象为二进制格式
- 所有其他功能均被禁用
-
我们定义的xml配置文件
<?xml version="1.0" encoding="UTF-8"?> <hazelcast xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-5.3.xsd"> <instance-name>hazelcast-instance</instance-name> <cluster-name>dev</cluster-name> <!-- 设置session中user的map配置 --> <map name="session:users"> <backup-count>2</backup-count> <max-idle-seconds>30</max-idle-seconds> <time-to-live-seconds>30</time-to-live-seconds> <eviction eviction-policy="LFU" max-size-policy="PER_NODE" size="542"/> </map> </hazelcast>
-
也可以用yaml配置文件
hazelcast: instance-name: hazelcast-instance cluster-name: dev map: session:users: backup-count: 2 max-idle-seconds: 30 time-to-live-seconds: 30 eviction: eviction-policy: LFU max-size-policy: PER_NODE size: 542
3.3程序代码
除了上面核心配置的内容外,其他内容与编码方式的代码一致。
gitee.com/mayuanfei/S…下的springboot13
客户端-服务器方式
1.目标
和上面嵌入式方式的目标一致,也是缓存用户信息,只不过这里集群的方式修改为客户端-服务器方式。这种方式很像使用redis或者说是数据库。我们程序中所有缓存的数据都放在了Hazelcast的一个服务器缓存集群中。我们的程序就是个客户端,通过远程获取服务器中的缓存的数据。
2.Hazelcast集群发现机制
Hazelcast集群是由一推的Hazelcast实例构成的网络。集群成员靠发现机制自动加入集群中,集群一旦形成,他们之间的通信始终通过 TCP/IP 进行,就与发现机制无关了。
2.1 发现机制
就是集群成员之间彼此知道对方存在的一种方式。Hazelcast支持的发现机制很多,主要包括以下几种:
-
自动检测
默认发现机制是自动检测,当它被启用时,会便利所有可用的发现机制。比如下面列出的TCP、多播等。并且会检测当前的运行时环境,比如你在AWS实例【亚马云的云服务器】上运行,则会自动使用 hazelcast-aws 插件,再比如Kubernetes环境中,也是可以自动检测环境并且发现集群成员的。这个默认发现机制官方是不推荐在生产环境中使用的。估计是效率问题吧。
-
TCP
-
Multicast【多播】
-
Eureka
-
Zookeeper
-
Kubernetes
-
Tanzu VMware
这里可以看到Hazelcast支持的发现方式还是蛮多的。但是居然连Eureka都支持,却没有对nacos的支持,不免有点感叹国产软件在世界范围内的应用还是不算广泛啊。言归正传,由于其他方式都需要搭建相应的环境,一般采用TCP和多播这两种应用的场景比较多,而多播采用UDP协议,会向集群内所有侦听的成员广播消息。由于局域网内可能限制广播的发送,所以使用前要谨慎确认,避免由于局域网的限制导致功能异常。下面介绍前三种常用方式。
2.2通过自动检测发现集群成员
如果用户没有指定或提供任何配置文件,Hazelcast默认会使用jar包中自带的配置文件——"hazelcast-default.xml"来配置Hazelcast的运行环境。打开这个默认的xml文件可以看到它配置的自动检测配置:
<hazelcast>
<port auto-increment="true" port-count="100">5701</port>
...
<network>
<join>
<auto-detection enabled="true" />
<multicast enabled="false">
...
</join>
</network>
...
</hazelcast>
2.3通过TCP发现集群成员
使用TCP发现机制,需要配置一个完整的TPC/IP集群(发现和通信都使用TCP/IP协议)。使用TCP/P配Hazelcas集群成员发现时,需要列出全部或部分成员的主机名或IP地址。无需列出所有集群成员,但是当新成员加入时,至少有一个列出的成员必须在集群中处于活跃状态。配置如下:
<hazelcast>
...
<network>
<join>
<tcp-ip enabled="true">
<member-list>
<member>machine1</member>
<member>machine2</member>
<member>machine3:5799</member>
<member>192.168.1.0-7</member>
<member>192.168.1.21</member>
</member-list>
</tcp-ip>
</join>
</network>
...
</hazelcast>
member元素接受的值类型可以是ip或者主机名,还能指定端口。甚至还可以指定 IP 范围,例如192.168.1.0-7。如果我们都是以ip来地址来定义的话,还能简化为:
<members>192.168.1.0-7,192.168.1.21</members>
在没有指定端口号时,默认是从5701开始到5801结束。就是默认配置里的这段配置:
<port auto-increment="true" port-count="100">5701</port>
2.4通过Multicast[多播]发现集群成员
通过多播自动发现机制,Hazelcast允许集群成员使用多播通信找到彼此。集群成员不需要知道其他成员的具体地址,因为它们只是多播给所有其他成员进行监听。是否可以或允许多播取决于运行时的环境。配置如下:
<hazelcast>
...
<network>
<join>
<multicast enabled="true">
<multicast-group>224.2.2.3</multicast-group>
<multicast-port>54327</multicast-port>
<multicast-time-to-live>32</multicast-time-to-live>
<multicast-timeout-seconds>2</multicast-timeout-seconds>
<trusted-interfaces>
<interface>192.168.1.102</interface>
</trusted-interfaces>
</multicast>
</join>
</network>
...
</hazelcast>
-
multicast-group
多播的组地址。默认为:224.2.2.3
-
multicast-port
指定Hazelcast成员侦听或者发送消息的socket端口。默认为:54327端口
-
multicast-time-to-live
多播数据包的生存时间。
-
multicast-timeout-seconds
该参数表明了一个成员等待合法的多播响应的时间,单位为s,如果在设定的时间内没有收到合法的响应,该成员就会选举自己成为leader并创建自己的集群。该参数只适用于集群无leader而且新的成员刚启动的场景。如果参数的值设置的太大,比如60s,这意味着在选举出leader之前需要等待60s之后才能进行下一次的尝试。值设置过大和过小都需要谨慎处理,如果值设置的太小,可能导致成员过早的放弃而开始下一轮尝试。
3.搭建一个Hazelcast缓存集群
3.1搭建方式
了解了上面的集群发现机制,搭建集群就是具体实践的过程了。这个搭建也分好几种:
-
下载压缩包单独在每台服务器上运行构成集群
下载地址:hazelcast.com/open-source…
这个压缩包里包含Hazelcast和管理中心。可以采用每台服务器,独立运行的方式构成集群。
-
docker方式
采用拉取的方式把Hazelcast和管理中心分别拉取,以容器方式启动。比较省时省力。
-
云部署
-
k8s
3.2采用docker方式部署集群
-
拉取Hazelcast镜像
docker pull hazelcast/hazelcast:5.2.4
-
创建一个hazelcast-docker.xml文件
Docker 容器中的默认配置文件是
/opt/hazelcast/config/hazelcast-docker.xml
.我们为了便于修改配置在docker启动的时候做一个数据卷。hazelcast-docker.xml内容如下:<?xml version="1.0" encoding="UTF-8"?> <hazelcast xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-5.2.xsd"> <!-- 集群的名称 --> <cluster-name>lmcache</cluster-name> <network> <port auto-increment="true" port-count="5">5701</port> <join> <!-- 关闭自动检测发现 --> <auto-detection enabled="false"/> <!-- 采用tcp/ip机制发现集群成员 --> <tcp-ip enabled="true"> <members>member1,member2,member3</members> </tcp-ip> </join> </network> </hazelcast>
这里要特别注意设置的内容。里面都是主机名称。也就是docker容器的名字。
-
docker中创建本地网络
docker network create hazelcast-network
所有指向该网络的容器,均可通过使用容器的名称进行互通。
-
运行Hazelcast容器
# 启动第1个集群成员 docker run --rm -d \ -v /Users/mayuanfei/docker-vol/hazelcast/hazelcast-docker.xml:/opt/hazelcast/config/hazelcast-docker.xml \ -e JAVA_OPTS="-Dhazelcast.local.publicAddress=172.17.236.59:5701 -Xms256M -Xmx256M" \ --network hazelcast-network \ --name member1 \ -p 5701:5701 hazelcast/hazelcast:5.2.4 # 启动第2个集群成员 docker run --rm -d \ -v /Users/mayuanfei/docker-vol/hazelcast/hazelcast-docker.xml:/opt/hazelcast/config/hazelcast-docker.xml \ -e JAVA_OPTS="-Dhazelcast.local.publicAddress=172.17.236.59:5701 -Xms256M -Xmx256M" \ --network hazelcast-network \ --name member2 \ -p 5702:5701 hazelcast/hazelcast:5.2.4 # 启动第3个集群成员 docker run --rm -d \ -v /Users/mayuanfei/docker-vol/hazelcast/hazelcast-docker.xml:/opt/hazelcast/config/hazelcast-docker.xml \ -e JAVA_OPTS="-Dhazelcast.local.publicAddress=172.17.236.59:5701 -Xms256M -Xmx256M" \ --network hazelcast-network \ --name member3 \ -p 5703:5701 hazelcast/hazelcast:5.2.4
docker命令参数解释:
命令参数 解释 --rm 告诉 Docker 在退出后将容器从本地缓存中删除 -d 采用后台方式启动容器 -v 给容器挂载存储卷,挂载到容器的某个目录。这里用自己定义的配置文件替换默认 -e 指定环境变量,容器中可以使用该环境变量。这里设置JVM占用的最小、最大内存 --network 指定本地网络。处于该网络的容器,均可以通过容器名进行访问 --name 指定容器名。 -p 端口映射。“宿主机端口:容器内端口” -
查看容器的运行情况
docker ps
-
查看容器启动日志
docker logs --tail 200 member1
4.客户端方式使用Hazelcast集群
4.1编码方式
还是和上面的例子实现的功能一样,这里仅仅看配置类的内容:
@EnableCaching
@Configuration
public class HazelcastConfig {
@Bean
public ClientConfig config() {
ClientConfig clientConfig = new ClientConfig();
//设置Hazelcast实例名称
clientConfig.setInstanceName("hazelcast-client-instance");
// 设置集群名称.这个参数很重要,一定要和搭建集群时名称一致.默认是dev
clientConfig.setClusterName("lmcache");
// 设置集群服务器的地址
clientConfig.getNetworkConfig()
.addAddress("172.17.236.59");
return clientConfig;
}
/**
* 这里默认就会调用HazelcastClient.newHazelcastClient方法.
* 为了便于理解,加入此方法.
*/
@Bean
public HazelcastInstance hazelcastInstance(ClientConfig config) {
return HazelcastClient.newHazelcastClient(config);
}
}
4.2配置文件方式
resouces目录下创建hazelcast-client.xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast-client xmlns="http://www.hazelcast.com/schema/client-config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/client-config
http://www.hazelcast.com/schema/client-config/hazelcast-client-config-5.2.xsd">
<cluster-name>lmcache</cluster-name>
<instance-name>hazelcast-client-instance</instance-name>
<network>
<cluster-members>
<!--
以下是客户端尝试连接的地址列表。 Hazelcast 集群的所有成员都接受客户端连接。
使用格式 <hostname>:<port> 如果未指定端口号,则将尝试端口范围 5701-5703。
-->
<address>172.17.236.59</address>
</cluster-members>
<!--
关闭智能路由功能.仅用上面地址列表中的集群成员
-->
<smart-routing>false</smart-routing>
</network>
<connection-strategy>
<connection-retry>
<!--
客户端连接服务器集群超时时间.
-->
<cluster-connect-timeout-millis>1000</cluster-connect-timeout-millis>
</connection-retry>
</connection-strategy>
</hazelcast-client>
5.程序代码
gitee.com/mayuanfei/S…下的springboot14
Hazelcast管理中心
1.管理中心作用
管理中心是用于管理和监控 Hazelcast 集群的工具。它具有的一些特性如下:
- 通过 UI、JMX 界面和 Prometheus 监控集群的性能。
- 查看有关会员、客户和数据结构的统计信息。
- 在集群上执行 SQL 查询。
- 执行管理任务,例如识别和诊断集群中的问题。
- 使用 REST API 端点返回管理中心中显示的信息。
官网地址:docs.hazelcast.com/management-…
2.本地启动管理中心容器
-
启动管理中心
# 找到下载的Hazelcast目录,进入management-center目录 cd management-center/bin ./hz-mc start
-
访问管理中心
点击enable蓝色button。进入集群链接界面:
增加用户的命令:
./hz-mc conf user create --username=laoma --role=admin --password=8位以上的复杂密码
3.添加集群
-
点击add按钮
-
添加集群需要填写的内容
-
添加结果
4.查看集群
点击VIEW CLUSTER按钮后:
Hazelcast序列化
在客户端-服务器这种模式下,java对象存储在集群服务器缓存中时是序列化的。可以理解成一个对象以字节的形式保存上面,存的时候是一个序列化的过程,取的时候是一个反序列化过程。Hazelcast提供了很多序列化方式,其优缺点如下:
序列化接口 | 优点 | 缺点 |
---|---|---|
Serializable | 1.标准java接口 2.无需额外实现其他方法 3.兼容数据类型很好 | 1. 费时并且CUP占用率高 2. 占用空间大 |
Externalizable | 1.标准java接口 2.比Serializable效率更高 | 1.必须实现序列化接口 |
DataSerializable | 1.比Serializable效率更高 | 1. Hazelcast提供的接口,具有局限性 |
IdentifiedDataSerializable | 1. 比Serializable效率更高 2.反序列化期间不使用反射 | 1.局限于Hazelcast 2.必须实现序列化接口 3.必须实现工厂和配置 |
Portable | 1.比Serializable效率更高 2.反序列化期间不使用反射 3.支持版本控制 4.查询期间支持部分反序列化 | 1.局限于Hazelcast 2.必须实现序列化接口 3.必须实现工厂和配置 4.类定义也与数据一起发送 |
Compact Serialization | 1.比Portable内存使用率高 2.无须实现任何接口 3.无须配置 4.查询期间支持部分反序列化 | 1.局限于Hazelcast |
HazelcastJsonValue | 1.Hazelcast实现,无须编码 | 1.局限于Hazelcast 2.需要在服务器成员上存储额外的元数据。 |
Custom Serialization | 1.无须实现任何接口 2.方便灵活 3.基于StreamSerializer、ByteArraySerializer实现 | 1.必须实现接口 2.需要配置 |
这里挑常用的几个做实验。
1.Serializable【java的序列化接口】
一个类如果实现了java的Serializable接口,那么这个类对象在存储时,Hazelcast会采用java序列化和反序列的方式进行对象的存储和转换。示例代码:
@Data
public class UserInfo1 implements Serializable {
/**
* 用户id
*/
private String userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户出生日期,java.util.Date类型
*/
private Date birthday;
}
通过java Serializable存入集群中的对象,无法通过管理中心查看具体的值。报错如下:
因为我们客户端的UserInfo1在集群服务器中是没有这个类的。所以在反序列化显示时就报错了。但是程序中是不会有问题的。
2.Compact Serialization【紧凑序列化】
它可以说是Hazelcast中最强序列化,具有以下主要特点:
- 将结构和数据分开,并按类型存储,而不是按对象存储,这将会减少内存和带宽的使用
- 无须继承任何类或者实现任何接口
- 独立于平台和语言
- 支持查询或索引期间字段的部分反序列化
- 支持模式演化,允许添加或删除字段,或更改字段类型
紧凑序列化在5.2版本中已经是稳定版。官方建议使用。并且如果你一个Bean什么接口也不实现并且也没有配置序列化的话,默认的序列化方式就是这个紧凑系列化。
2.1支持类型[仅列出java语言]
更多语言查看:docs.hazelcast.com/hazelcast/5…
类型 | Java | 描述 |
---|---|---|
BOOLEAN | boolean | 真或假由单个位表示。真为 1; 假为 0。 |
ARRAY_OF_BOOLEAN | boolean[] | 布尔数组或 null。 |
NULLABLE_BOOLEAN | Boolean | 一个可以为null的布尔值 |
ARRAY_OF_NULLABLE_BOOLEAN | Boolean[] | 一个可以为null的布尔值数组或null |
INT8 | byte | 8位二进制补码有符号整数 |
ARRAY_OF_INT8 | byte[] | 一个可以为null的int8数组 |
NULLABLE_INT8 | Byte | 一个可以为null的int8 |
ARRAY_OF_NULLABLE_INT8 | Byte[] | 一个可以为null的int8数组或null |
INT16 | short | 16位二进制补码有符号整数 |
ARRAY_OF_INT16 | short[] | 一个可以为null的int16数组 |
NULLABLE_INT16 | Short | 一个可以为null的int16 |
ARRAY_OF_NULLABLE_INT16 | Short[] | 一个可以为null的int16数组或null |
INT32 | int | 32位二进制补码有符号整数 |
ARRAY_OF_INT32 | int[] | 一个可以为null的int32数组 |
NULLABLE_INT32 | Integer | 一个可以为null的int32 |
ARRAY_OF_NULLABLE_INT32 | Integer[] | 一个可以为null的int32数组或null |
INT64 | long | 64位二进制补码有符号整数 |
ARRAY_OF_INT64 | long[] | 一个可以为null的int64数组 |
NULLABLE_INT64 | Long | 一个可以为null的int64 |
ARRAY_OF_NULLABLE_INT64 | Long[] | 一个可以为null的int64数组或null |
FLOAT32 | float | 32位IEEE 754浮点数 |
ARRAY_OF_FLOAT32 | float[] | 一个可以为null的float32数组 |
NULLABLE_FLOAT32 | Float | 一个可以为null的float32 |
ARRAY_OF_NULLABLE_FLOAT32 | Float[] | 一个可以为null的float32数组或null |
FLOAT64 | double | 64位IEEE 754浮点数 |
ARRAY_OF_FLOAT64 | double[] | 一个可以为null的float64数组 |
NULLABLE_FLOAT64 | Double | 一个可以为null的float64 |
ARRAY_OF_NULLABLE_FLOAT64 | Double[] | 一个可以为null的float64数组或null |
STRING | String | 一个可以为null的UTF-8编码字符串 |
ARRAY_OF_STRING | String[] | 一个可以为null的字符串数组 |
DECIMAL | BigDecimal | 一个可以为null的任意精度和比例浮点数 |
ARRAY_OF_DECIMAL | BigDecimal[] | 一个可以为null的DECIMAL数组 |
TIME | LocalTime | 一个可以为null的由小时、分钟、秒和纳秒组成的时间 |
ARRAY_OF_TIME | LocalTime[] | 一个可以为null的TIME数组 |
DATE | LocalDate | 一个可以为null的由年、月和日组成的日期 |
ARRAY_OF_DATE | LocalDate[] | 一个可以为null的DATE数组 |
TIMESTAMP | LocalDateTime | 由年、月、日、小时、分钟、秒和纳秒或null组成的时间戳 |
ARRAY_OF_TIMESTAMP | LocalDateTime[] | 一个可以为null的TIMESTAMP数组 |
TIMESTAMP_WITH_TIMEZONE | OffsetDateTime | 由年、月、日、小时、分钟、秒和纳秒或null组成的带时区的时间戳 |
ARRAY_OF_TIMESTAMP_WITH_TIMEZONE | OffsetDateTime[] | 一个可以为null的OffsetDateTime数组 |
COMPACT | 任何用户类型 | 用户定义的嵌套紧凑可序列化对象或null |
ARRAY_OF_COMPACT | 任何用户类型数组 | 用户定义的紧凑可序列化对象或null的数组 |
注意:
从上表中可以看到,Hazelcast的紧凑序列化器,不支持java.util.Date类型。如果使用会报错。可替换为LocalDate、LocalTime、LocalDateTime等支持的格式。
2.2使用很便利
POJO类不用继承特别的类或者实现接口了。如下代码所示:
@Data
public class UserInfo2 {
/**
* 用户id
*/
private String userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户出生日期
*/
private LocalDate birthday;
//private Date birthday;
}
这里如果是java.util.Date。则报错:
com.hazelcast.nio.serialization.HazelcastSerializationException: The 'class java.util.Date' cannot be serialized with zero configuration Compact serialization because this type is not supported yet....
2.3管理中心可以查看
2.4增加字段也很方便
比如在UserInfo2的类中增加一个地址字段:
/**
* 新增一个地址
*/
private String address;
那么无须进行什么配置或者其他什么改动。直接就可以进行序列化和反序列化了。只不过之前的反序列地址字段时一个null值而已。
2.5补充说明
这里Hazelcast为什么不支持Date,而仅仅支持LocalDate、LocalTime、localDateTime呢?也就是说Date和LocalDate、LocalTime、localDateTime的区别是什么。列出几个最显著的区别:
- 所在的包不同。Date在java.util包下;而另外几个在 java.time包下
- Date是JDK1.0就已经存在的一个日期时间类。而LocalDate、LocalTime、LocalDateTime是JDK8才有的
- Date中不光有时间信息还有时区的信息,而JDK8的LocalDate只表示日期,没有时区等信息;LocalTime仅表示时间;localDateTime表示日期时间和Date很像,但是他能精确到纳秒,而Date只能到毫秒。
- Date非线程安全的;而JDK新增的这几个日期时间类是线程安全的
- Date的API设计的不太直观,而且部分方法已过时。JDK新增的日期时间类易于使用,提供了更方便的方法来处理日期和时间。
总之,如果是在JDK8以上的JVM环境中推荐使用JDK8新增的这些日期时间类。如果是更旧版本的JDK程序,序列化时最好就选java的Serializable接口。
3.序列化为 JSON
如果想查询存储在Hazelcast中的JSON字符串,可以使用HazelcastJsonValue。HazelcastJsonValue是一个轻量级包装器,告诉Hazelcast集群将给定字符串视为JSON。使用此信息,集群中的成员可以创建元数据来优化对字符串中数据的查询。因此,HazelcastJsonValue当你希望能够查询存储在Hazelcast集群中的JSON值时,最好使用它。
@PostMapping(value = "/writeJsonValue")
public UserInfo2 writeDataToHazelcast(String userId, String userName, Integer age) {
UserInfo2 userInfo = new UserInfo2();
userInfo.setUserId(userId);
userInfo.setUserName(userName);
userInfo.setAge(age);
userInfo.setBirthday(LocalDate.now());
userInfo.setAddress("北京");
IMap<String, Object> map = hazelcastInstance.getMap(Constants.CACHE_NAME_SESSION_USERS);
// 这里把对象转换为json,并且告诉Hazelcast这是个HazelcastJsonValue
map.put(userId, new HazelcastJsonValue(JSONUtil.toJsonStr(userInfo)));
return userInfo;
}
通过管理中心可以查看这个json字符串:
4.自定义序列化器
要想实现自己的序列化器,可以通过StreamSerializer和ByteArraySerializer接口来实现。
4.1实现 StreamSerializer
public class UserInfo2Serializer implements StreamSerializer<UserInfo2> {
@Override
public int getTypeId() {
return 100;
}
@Override
public void write(ObjectDataOutput out, UserInfo2 object) throws IOException {
ObjectMapper mapper = SpringUtil.getBean(ObjectMapper.class);
out.write(mapper.writeValueAsBytes(object));
}
@Override
public UserInfo2 read(ObjectDataInput in) throws IOException {
InputStream inputStream = (InputStream) in;
ObjectMapper mapper = SpringUtil.getBean(ObjectMapper.class);
return mapper.readValue(inputStream, UserInfo2.class);
}
}
注意:
typeId 必须是唯一的,并且>=1,它被用来在Hazelcast 反序列化对象时确认使用哪个序列化程序用的。
4.2配置 StreamSerializer
//配置序列化器
SerializerConfig sc = new SerializerConfig()
.setImplementation(new UserInfo2Serializer())
.setTypeClass(UserInfo2.class);
clientConfig.getSerializationConfig().addSerializerConfig(sc);
或者使用xml配置
<hazelcast>
<serialization>
<serializers>
<serializer type-class="com.mayuanfei.springboot15.pojo.UserInfo2"
class-name="com.mayuanfei.springboot15.serializer.UserInfo2Serializer" />
</serializers>
</serialization>
...
</hazelcast>
5.全局序列化器
从实现的角度上来说,全局序列化器和自定义序列化器基本一致。只是配置的地方和作用域的范围不一样。全局序列化器被设计为一个备用序列化候选方案。就是当一个对象找不到序列化器处理时,用这个全局序列化器进行处理。默认情况下,全局序列化器是不会处理java.io.Serializable和java.io.Externalizable实例。但是可以通过配置来指定全局序列化来处理这些实例。
5.1实现全局序列化器
public class GlobalStreamSerializer implements StreamSerializer<Object> {
private ObjectMapper mapper = SpringUtil.getBean(ObjectMapper.class);
@Override
public int getTypeId() {
return 10000;
}
@Override
public void write(ObjectDataOutput out, Object object) throws IOException {
out.write(mapper.writeValueAsBytes(object));
}
@Override
public Object read(ObjectDataInput in) throws IOException {
InputStream inputStream = (InputStream) in;
return mapper.readValue(inputStream, Object.class);
}
}
5.2配置全局序列化器
// 配置全局序列化器
GlobalSerializerConfig gc = new GlobalSerializerConfig()
.setImplementation(new GlobalStreamSerializer())
.setOverrideJavaSerialization(true);
clientConfig.getSerializationConfig().setGlobalSerializerConfig(gc);
或者使用xml配置
<hazelcast>
...
<serialization>
<serializers>
<global-serializer override-java-serialization="true">GlobalStreamSerializer</global-serializer>
</serializers>
</serialization>
...
</hazelcast>
6.序列化器的优先级
当Hazelcast序列化一个对象时:
- 首先检查对象是否为null。
- 如果上述检查失败,则Hazelcast会查找用户指定的CompactSerializer。
- 如果上述检查失败,则Hazelcast会检查它是否是com.hazelcast.nio.serialization.DataSerializable或com.hazelcast.nio.serialization.IdentifiedDataSerializable的实例。
- 如果上述检查失败,则Hazelcast会检查它是否是com.hazelcast.nio.serialization.Portable的实例。
- 如果上述检查失败,则Hazelcast会检查它是否是默认类型之一的实例。
- 如果上述检查失败,则Hazelcast会查找用户指定的Custom Serializer,即ByteArraySerializer或StreamSerializer的实现。使用输入对象的Class及其父类(直到Object)搜索自定义序列化器。如果父类搜索失败,则还将检查类实现的所有接口(不包括java.io.Serializable和java.io.Externalizable)。
- 如果上述检查失败,则Hazelcast会检查它是否是java.io.Serializable或java.io.Externalizable的实例,并且未使用Java Serialization Override功能注册全局序列化器。
- 如果上述检查失败,并且已注册全局序列化器,则Hazelcast使用已注册的全局序列化器。
- 如果上述检查失败,并且启用了紧凑序列化,则Hazelcast尝试自动从对象的类中提取模式。
7.程序代码
gitee.com/mayuanfei/S…下的springboot15
再谈Map中的驱逐策略
这个驱逐策略和过期策略要区分开,过期策略是设置一个条目在内存中的生命周期,主要通过:time-to-live-seconds
和max-idle-seconds
来设置;而驱逐策略限制了Map的大小,当超过限制时,采用什么策略删除条目以减小其大小。通常是配置 size
、max-size-policy
和 eviction-policy
属性。
前面示例中配置过的驱逐策略:
<hazelcast>
...
<!-- 设置session中user的map配置 -->
<map name="session:users">
<backup-count>2</backup-count>
<max-idle-seconds>30</max-idle-seconds>
<time-to-live-seconds>30</time-to-live-seconds>
<eviction eviction-policy="LFU" max-size-policy="PER_NODE" size="542"/>
</map>
...
</hazelcast>
1.size属性
此属性定义Map的最大值。当达到最大值时,将根据驱逐策略eviction-policy
设置的方式删除Map中的条目。
- 默认值 :0(无限制)
- 接受的值: 0到Integer.MaxValue之间的整数。如果设置了>0的整数值,则驱逐策略
eviction-policy
设置的值不能为NONE(也就是不驱逐,下文会列出驱逐策略有哪些值)。
2.max-size-policy属性
如果说上面的size属性是设置最大值的话,这里就是这个最大值的统计角度或者说统计的范围、方式。包括的取值有:
-
PER_NODE
每个集群成员中Map条目的最大数量。这个是默认值。
-
PER_PARTITION
每个分区内地图条目的最大数量。存储大小取决于群集成员中的分区计数。一般不应该设置此属性。譬如避免在小型群集中使用此属性。如果群集很小,则它会托管比较大的群分区,因此也会托管比较多的Map条目。因此,对于小型群集,按分区删除条目的开销可能会影响整体性能。
-
USED_HEAP_SIZE
每个Hazelcast实例中的Map最大可用堆内存大小(单位:MB)。注意,当内存中格式设置为OBJECT时,此策略不起作用,因为在将数据放置为OBJECT时无法确定内存占用情况(默认内存中的对象都是以二进制存储的)。
-
USED_HEAP_PERCENTAGE
每个Hazelcast实例中的Map最大可用堆内存大小的百分比。如:JVM配置1000MB,而这里设置的是10,则在使用堆内存超过100MB时将开始删除Map中的条目。当内存中格式设置为OBJECT时,此策略不起作用。
-
FREE_HEAP_SIZE
JVM中最小空闲堆大小(以MB为单位)。
-
FREE_HEAP_PERCENTAGE
JVM中最小空闲堆大小百分比。例如,如果JVM配置1000 MB,并且该值为10,则在空闲堆大小低于100 MB时将清除Map中的条目。
-
USED_NATIVE_MEMORY_SIZE
企业版具有的功能。使用堆外内存。每个Hazelcast实例中的Map最大已用本机内存大小(单位:MB)。
-
USED_NATIVE_MEMORY_PERCENTAGE
企业版具有的功能。每个Hazelcast实例中的Map最大使用本机内存大小的百分比。
-
FREE_NATIVE_MEMORY_SIZE
企业版具有的功能。每个Hazelcast实例的本机最小空闲内存大小(以MB为单位)。
-
FREE_NATIVE_MEMORY_PERCENTAGE
企业版具有的功能。每个Hazelcast实例的本机最小空闲内存大小百分比
3.理解示例中设置的size="542"
Hazelcast通过分区来计算地图的大小。例如,使用max-size的PER_NODE属性,则Hazelcast会为每个分区的每个群集成员计算地图条目的最大数量。Hazelcast使用以下方程式来计算分区的最大大小:
partition-maximum-size = max-size * member-count / partition-count
翻译过来就是:
分区存的最大条目数 = 我们设置的size数 * 集群成员的数量 / 分区数(前文说了这个数是271)
分区最大条目数 = 542/271 * 1 = 2 当一个节点时每个分区中存了2条数据。
在实际测试中,发现PER_NODE和PER_PARTITION在单机上是一样的效果。设置的size最好是271的倍数
4.eviction-policy属性
定义当Map的大小增长超过阈值时要删除哪些映射条目。可以设置如下值:
-
NONE
默认策略。如果设置,则不会驱逐任何项目,并忽略size设置的大小。
-
LRU
删除最近最少使用的条目
-
LFU
删除最不经常使用的映射条目
记忆印记
- Hazelcast是一个分布式计算和缓存平台。这里我们主要还是用到它的缓存功能。
- Hazelcast被springboot框架默认集成。说明这框架还是很稳定高效的。
- Hazelcast可以采用嵌入式集成(也就是引入jar包方式),也可以采用类似redis的集群方式
- 通过配置类的方式优先级最高。通过xml配置的方式最直观。如果是嵌入式hazelcast.xml;是客户端hazelcast-client.xml【这里再次体现了springboot的约定大于配置的思想】
- Hazelcast管理中心是一个单独web项目,可以监控集群成员和客户端的各种内存使用情况,还能查看集群成员中存储的值。集监控和查看与一体。
- 针对客户端-服务器这种模式,Hazelcast的序列化如果一个Bean实现了java.io.Serializable那么会安装java序列化的方式进行存储;而一个Bean什么接口也不实现会按照紧凑序列化方式存储。这个紧凑序列化方式也是官方推荐的。只不过要注意它支持的类型。java.util.Date用JDK8新包中的类代替
- Hazelcast占用的是JVM的内存,也就是说会收到JVM垃圾回收的影响。