V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
heishao
V2EX  ›  推广

SpringBoot 集成 Hazelcast 实现集群与分布式内存缓存

  •  
  •   heishao · 2018-11-12 16:15:19 +08:00 · 1714 次点击
    这是一个创建于 2205 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 下载。

    参考资料

    ———————————————————————分割线—————————————————————————

    我是黑少,专注微服务技术分享,喜欢分享、爱交友人、崇尚“实践出真知”的理念,以折腾鼓捣代码为乐

    我的微信:weiweiweiblack (备注:v2ex )

    微信公号:黑少微服务,“分享技术,热爱生活”,欢迎关注

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3766 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 10:39 · PVG 18:39 · LAX 02:39 · JFK 05:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.