Springboot 系列 (16) - Springboot+Kafka 实现发布/订阅消息
消息是一个字节数组,可以在这些字节数组中存储任何对象,支持的数据格式包括 String、JSON 等。
消息队列 (Message Queue): 是分布式系统中重要的组成部分,主要解决应用解耦、异步消息、流量削锋等问题,实现高性能、高可用、可伸缩和最终一致性架构。目前使用较多的消息队列有 ActiveMQ、RabbitMQ、RocketMQ、Kafka 等。
Message Queue 的通讯模式:
(1) 点对点通讯:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。
(2) 多点广播:将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。
(3) 发布/订阅 (Publish/Subscribe) 模式:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。
(4) 群集 (Cluster):为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(群集) 的解决方案。群集类似于一个域 (Domain),群集内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用群集 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,群集中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。
Apache Kafka 是一个分布式发布/订阅消息系统和一个强大的消息队列 (MQ),可以处理大量的消息(数据),并使您能够将消息从一个端点传递到另一个端点。 Kafka 适合离线和在线消息消费。 Kafka 消息保留在磁盘上,并在集群集内复制以防止数据丢失。 Kafka 构建在 ZooKeeper 同步服务之上。 它与 Apache Storm 和 Spark 很容易集成,用于实时流式数据分析。
Kafka 通过给每一个消息绑定一个键值的方式来保证生产者可以把所有的消息发送到指定位置。属于某一个消费者群组的消费者订阅了一个主题,通过该订阅消费者可以跨节点地接收所有与该主题相关的消息,每一个消息只会发送给群组中的一个消费者,所有拥有相同键值的消息都会被确保发给这一个消费者。
Kafka 术语:
(1) Broker (消息代理):Kafka 集群包含一个或多个服务器,这种服务器被称为 broker。
(2) Topic (主题):每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同 Topic 的消息分开存储,逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上,但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)。
(3) Partition(分区):Partition 是物理上的概念,每个 Topic 包含一个或多个 Partition。
(4) Producer(生产者):负责发布消息到 Kafka broker。
(5) Consumer(消费者):消息消费者是从 Kafka broker 读取消息的客户端。
(6) Consumer Group(消费者组):每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。
Kafka:https://kafka.apache.org/
Kafka GitHub:https://github.com/apache/kafka
ZooKeeper 是 Apache 的一个顶级项目,为分布式应用提供高效、高可用的分布式协调服务,提供了诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知和分布式锁等分布式基础服务。由于 ZooKeeper 便捷的使用方式、卓越的性能(基于内存)和良好的稳定性,被广泛地应用于诸如 Hadoop、HBase、Kafka 和 Dubbo 等大型分布式系统中。
ZooKeeper 术语:
(1) Leader(领导者):负责进行投票的发起和决议,更新系统状态。
(2) Follower(跟随者):用于接收客户端请求并给客户端返回结果,在选主过程中进行投票。
(3) Observer(观察者):可以接受客户端连接,将写请求转发给 leader,但是observer 不参加投票的过程,只是为了扩展系统,提高读取的速度。
ZooKeeper:https://zookeeper.apache.org
Zookeeper GitHub: https://github.com/apache/zookeeper
1. Kafka 安装配置
1) Windows 下安装
下载 https://archive.apache.org/dist/kafka/3.0.1/kafka_2.13-3.0.1.tgz,保存到目录 C:\Applications\Java\,kafka_2.13-3.0.1.tgz 文件名里的 2.13 是对应 Scala 版本 2.13,3.0.1 是 Kafka 的版本。
启动一个 cmd 窗口,进入目录 C:\Applications\Java\,运行如下命令。
C:\Applications\Java\> tar -zvxf kafka_2.13-3.0.1.tgz
C:\Applications\Java\> move kafka_2.13-3.0.1 kafka-3.0.1
C:\Applications\Java\> cd kafka-3.0.1
C:\Applications\Java\kafka-3.0.1> mkdir data
C:\Applications\Java\kafka-3.0.1> mkdir logs
修改 C:\Applications\Java\kafka-3.0.1\config\server.properties 文件里的 log.dirs:
log.dirs=C:/Applications/Java/kafka-3.0.1/logs
Kafka 运行包里自带 ZooKeeper,修改 C:\Applications\Java\kafka-3.0.1\config\zookeeper.properties 文件里的 dataDir:
dataDir=C:/Applications/Java/kafka-3.0.1/data
在一个主机上运行一个 Broker:
(1) 启动 Kafka & Zookeeper
在启动 Kafka 之前,要先启动 Zookeeper Server 。
C:\Applications\Java\kafka-3.0.1\bin\windows>zookeeper-server-start ..\..\config\zookeeper.properties
C:\Applications\Java\kafka-3.0.1\bin\windows>kafka-server-start ..\..\config\server.properties
# 查看状态
> jps
2501 Jps
1991 QuorumPeerMain
2024 Kafka
QuorumPeerMain 是 ZooKeeper 守护进程,2024 是 Kafka 守护进程。
Zookeeper 默认端口是 2181, Kafka broker 默认端口是 9092。
(2) 停止 Kafka & Zookeeper
C:\Applications\Java\kafka-3.0.1\bin\windows>kafka-server-stop
C:\Applications\Java\kafka-3.0.1\bin\windows>zookeeper-server-stop
Kafka 的运行方式:
(1) 在一个主机上运行一个 Broker;
(2) 在一个主机上运行多个 Brokers;
(3) 在多个主机上运行多个 Brokers。
注:如果不使用 Kafka 自带的 Zookeeper,可以独立安装配置 Zookeeper。
2) Ubuntu 下安装
$ wget https://archive.apache.org/dist/kafka/3.0.1/kafka_2.13-3.0.1.tgz
$ mv ./kafka_2.13-3.0.1.tgz ~/apps/ # 这里使用 /home/xxx/apps 目录, xxx 是用户根目录
$ tar -zvxf kafka_2.13-3.0.1.tgz
$ mv kafka_2.13-3.0.1 kafka-3.0.1
$ cd kafka-3.0.1
$ mkdir data
$ mkdir logs
修改 ~/apps/kafka-3.0.1/config/server.properties 文件里的 log.dirs:
log.dirs=/home/xxx/apps/kafka-3.0.1/logs
Kafka 运行包里自带 ZooKeeper,修改 ~/apps/kafka-3.0.1/config/zookeeper.properties 文件里的 dataDir:
dataDir=/home/xxx/apps/kafka-3.0.1/data
在一台主机上运行一个 Broker:
(1) 启动 Kafka & Zookeeper
在启动 Kafka 之前,要确保 Zookeeper Server 已经在运行。
$ ./bin/zookeeper-server-start.sh config/zookeeper.properties &
$ ./bin/kafka-server-start.sh config/server.properties &
# 查看状态
$ jps
2501 Jps
1991 QuorumPeerMain
2024 Kafka
QuorumPeerMain 是 ZooKeeper 守护进程,2024 是 Kafka 守护进程。
Zookeeper 默认端口是 2181, Kafka broker 默认端口是 9092。
(2) 停止 Kafka & Zookeeper
$ ./bin/kafka-server-stop.sh
$ ./bin/zookeeper-server-stop.sh
2. 独立 ZooKeeper 安装配置 (非必需)
1) Windows 下安装
下载 https://downloads.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz,保存到目录 C:\Applications\Java\。
启动一个 cmd 窗口,进入目录 C:\Applications\Java\,运行如下命令。
C:\Applications\Java\> tar -zvxf apache-zookeeper-3.6.3-bin.tar.gz
C:\Applications\Java\> move apache-zookeeper-3.6.3-bin zookeeper-3.6.3
C:\Applications\Java\> cd zookeeper-3.6.3
C:\Applications\Java\zookeeper-3.6.3> mkdir data
复制 C:\Applications\Java\zookeeper-3.6.3\conf\zoo_sample.cfg 生成 C:\Applications\Java\zookeeper-3.6.3\conf\zoo.cfg,修改 zoo.cfg 文件里的 dataDir :
dataDir=C:\Applications\Java\zookeeper-3.6.3\data
运行 ZooKeeper Server
C:\Applications\Java\zookeeper-3.6.3\bin> zkServer
运行 ZooKeeper Cli
C:\Applications\Java\zookeeper-3.6.3\bin> zkCli
2) Ubuntu 下安装
$ wget https://downloads.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz
$ mv ./apache-zookeeper-3.6.3-bin.tar.gz ~/apps/ # 这里使用 /home/xxx/apps 目录, xxx 是用户根目录
$ tar -zvxf apache-zookeeper-3.6.3-bin.tar.gz
$ mv apache-zookeeper-3.6.3-bin zookeeper-3.6.3
$ cd zookeeper-3.6.3
$ mkdir data
$ cp conf/zoo_sample.cfg conf/zoo.cfg
$ vim conf/zoo.cfg
dataDir = /home/xxx/apps/zookeeper-3.6.3/data
启动 ZooKeeper Server
$ ./bin/zkServer.sh start
运行 ZooKeeper Cli
$ ./bin/zkCli.sh
停止 ZooKeeper server
$ ./bin/zkServer.sh stop
3. 开发环境
Windows版本:Windows 10 Home (20H2)
IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
Apache Maven (https://maven.apache.org/):3.8.1
注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。
4. 创建 Spring Boot 基础项目
项目实例名称:SpringbootExample16
Spring Boot 版本:2.6.6
创建步骤:
(1) 创建 Maven 项目实例 SpringbootExample16;
(2) Spring Boot Web 配置;
(3) 导入 Thymeleaf 依赖包;
(4) 配置 jQuery;
具体操作请参考 “Springboot 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。
SpringbootExample16 和 SpringbootExample02 相比,SpringbootExample16 不配置 Bootstrap、模版文件(templates/*.html)和国际化。
5. 配置 Kafka
1) 修改 pom.xml,导入 Kafka 依赖包
1 <project ... > 2 ... 3 <dependencies> 4 ... 5 6 <dependency> 7 <groupId>org.springframework.kafka</groupId> 8 <artifactId>spring-kafka</artifactId> 9 </dependency> 10 11 ... 12 </dependencies> 13 14 ... 15 </project>
在IDE中项目列表 -> SpringbootExample16 -> 点击鼠标右键 -> Maven -> Reload Project
2) 修改 src/main/resources/application.properties 文件,添加如下配置
########### Kafka 集群 ###########
spring.kafka.bootstrap-servers=localhost:9092
########### 初始化生产者配置 ###########
# 生产端缓冲区大小 (30 MB)
spring.kafka.producer.buffer-memory=31457280
# Kafka 提供的序列化和反序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 重试次数
#spring.kafka.producer.retries=0
# 应答级别: 多少个分区副本备份完成时向生产者发送 ack 确认(可选0、1、all/-1)
#spring.kafka.producer.acks=1
# 批量大小
#spring.kafka.producer.batch-size=16384
# 提交延时(当生产端积累的消息达到 batch-size 或接收到消息 linger.ms 后,
# 生产者就会将消息提交给 kafka linger.ms 为 0 表示每接收到一条消息就提交给 kafka,
# 这时候 batch-size 其实就没用了)
#spring.kafka.producer.properties.linger.ms=0
# 自定义分区器
#spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner
########### 初始化消费者配置 ###########
# 默认的消费组 ID
spring.kafka.consumer.properties.group.id=defaultConsumerGroup
# 是否自动提交 offset
spring.kafka.consumer.enable-auto-commit=true
# 提交 offset 延时(接收到消息后多久提交 offset)
spring.kafka.consumer.auto.commit.interval.ms=1000
# Kafka 提供的序列化和反序列化类
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# 当 kafka 中没有初始 offset 或 offset 超出范围时将自动重置 offset
# earliest: 重置为分区中最小的 offset;
# latest: 重置为分区中最新的 offset(消费分区中新产生的数据);
# none: 只要有一个分区不存在已提交的 offset,就抛出异常;
#spring.kafka.consumer.auto-offset-reset=latest
# 消费会话超时时间(超过这个时间 consumer 没有发送心跳,就会触发 rebalance 操作)
#spring.kafka.consumer.properties.session.timeout.ms=120000
# 消费请求超时时间
#spring.kafka.consumer.properties.request.timeout.ms=180000
# 消费端监听的 topic 不存在时,项目启动会报错(关掉)
#spring.kafka.listener.missing-topics-fatal=false
# 设置批量消费
#spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
#spring.kafka.consumer.max-poll-records=50
注:这里对 Kafka 详细的参数配置不做介绍,但本文保留了(注释掉了)没用到的参数。
6. 测试实例 (Web 模式)
1) 创建 src/main/java/com/example/kafka/KafkaConsumerListener.java 文件
1 package com.example.kafka; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import org.springframework.stereotype.Component; 7 import org.apache.kafka.clients.consumer.ConsumerRecord; 8 import org.springframework.kafka.annotation.KafkaListener; 9 10 @Component 11 public class KafkaConsumerListener { 12 13 public List<String> messageList = new ArrayList<>(); 14 15 @KafkaListener(topics = {"topic1"}) 16 public void onMessage(ConsumerRecord<?, ?> record) { 17 System.out.println("Kafka Consumer: " + record.topic() + " - " + record.partition() + " - " + record.value()); 18 19 if (record != null) 20 messageList.add((String)record.value()); 21 } 22 23 }
注:消费者使用 @KafkaListener 监听 topic1 主题,把收到的消息保存到 messageList 。
2) 创建 src/main/resources/templates/client.html 文件
1 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 2 <head> 3 <meta charset="UTF-8"> 4 <title th:text="${var}">Title</title> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script> 6 </head> 7 <body> 8 9 <h3>Kafka Client</h3> 10 <hr> 11 12 <p>Producer</p> 13 <p> 14 <input type="text" name="message" id="message" style="width: 320px; height: 32px;" value="" /> 15 <button type="button" id="btn_send">Send</button> 16 </p> 17 <div id="producer_result" style="width: 50%; padding: 15px;"></div> 18 <p> </p> 19 <hr> 20 <p>Consumer</p> 21 <p><button type="button" id="btn_receive">Receive</button></p> 22 <div id="consumer_result" style="width: 50%; padding: 15px;"></div> 23 24 <p> </p> 25 <script type="text/javascript"> 26 27 $(document).ready(function(){ 28 29 $('#btn_send').click(function() { 30 31 var msg = $('#message').val(); 32 if (msg == '') { 33 alert('Please enter message'); 34 $('#message').focus(); 35 return; 36 } 37 38 $.ajax({ 39 40 type: 'POST', 41 url: '/message/post', 42 data: { 43 message: msg, 44 }, 45 success: function(response) { 46 console.log(response); 47 if (response.ret == "OK") { 48 $('#producer_result').append("Sent '" + msg + "'<br>"); 49 } else { 50 $('#producer_result').append("Failed to send '" + msg + "'<br>"); 51 } 52 }, 53 error: function(err) { 54 console.log(err); 55 $('#producer_result').append("Error: AJAX issue<br>"); 56 } 57 }); 58 59 }); 60 61 $('#btn_receive').click(function() { 62 63 $.ajax({ 64 type: 'GET', 65 url: '/message/get', 66 success: function(response) { 67 console.log(response); 68 if (response.ret == "OK") { 69 $('#consumer_result').html(response.message + "<br>"); 70 } else { 71 $('#consumer_result').html("No message<br>"); 72 } 73 }, 74 error: function(err) { 75 console.log(err); 76 $('#consumer_result').html("Error: AJAX issue<br>"); 77 }, 78 }); 79 }); 80 }); 81 82 </script> 83 84 </body> 85 </html>
3) 修改 src/main/java/com/example/controller/IndexController.java 文件
1 package com.example.controller; 2 3 import java.util.Map; 4 import java.util.HashMap; 5 6 import org.springframework.stereotype.Controller; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RequestParam; 10 import org.springframework.web.bind.annotation.ResponseBody; 11 12 import org.springframework.kafka.core.KafkaTemplate; 13 import com.example.kafka.KafkaConsumerListener; 14 15 @Controller 16 public class IndexController { 17 @Autowired 18 private KafkaTemplate<String, Object> kafkaTemplate; 19 @Autowired 20 private KafkaConsumerListener kafkaConsumerListener; 21 22 @ResponseBody 23 @RequestMapping("/test") 24 public String test() { 25 return "Test Page"; 26 } 27 28 @RequestMapping("/client") 29 public String client() { 30 return "client"; 31 } 32 33 @ResponseBody 34 @RequestMapping("/message/post") 35 public Map<String, String> messagePost(@RequestParam String message) { 36 37 Map<String, String> map = new HashMap(); 38 if ("".equals(message)) { 39 map.put("ret", "ERROR"); 40 map.put("description", "Message is empty"); 41 } else { 42 43 // 生产者发送消息到 topic1 主题 44 kafkaTemplate.send("topic1", message); 45 map.put("ret", "OK"); 46 map.put("description", "Message has been sent"); 47 } 48 49 return map; 50 } 51 52 @ResponseBody 53 @RequestMapping("/message/get") 54 public Map<String, String> messageGet() { 55 56 Map<String, String> map = new HashMap(); 57 58 if (kafkaConsumerListener.messageList.size() == 0) { 59 map.put("ret", "ERROR"); 60 map.put("description", "Message is empty"); 61 } else { 62 String str = ""; 63 for (String item : kafkaConsumerListener.messageList) { 64 if ("".equals(str)) { 65 str = item; 66 } else { 67 str += "<br>" + item; 68 } 69 } 70 71 map.put("ret", "OK"); 72 map.put("message", str); 73 } 74 75 return map; 76 } 77 78 }
注:Spring Bean 的作用域被默认为 singleton,这里自动装配的 kafkaConsumerListener 就是那个唯一的 Bean,所以可以在 IndexController 里读取到消费者保存的数据。
运行并访问 http://localhost:9090/client,确保 Kafka 和 ZooKeeper 服务已正常运行,在页面上 Producer 部分发送消息,在 Consumer 部分接收消息,消息有延时。