实战:Spring Cloud Stream 集成Kafka
一. 消息队列
1.1 消息队列是什么?
- “消息队列”是在消息的传输过程中保存消息的容器。
- 专业的解释说明:
- “消息”是在两台计算机间传送的数据单位。
- 消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。
- 消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。
- 消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;
- 如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
- 从生活上来讲,消息传输就类似于我们讲话,一个人说话,一个人听,传播过程是消息队列,说话的人是消息生产者,听人说话的人是消费者;
1.2 消息队列的特点
- RabbitMQ:
- RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。
- 同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。
- Redis:
- Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。
- 虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。
- 对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。
- 实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
- Redis的特点:
- 异常快速
- 支持丰富的数据类型
- 所有操作都是原子的
- ZeroMQ:
- ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。
- ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。
- 但是ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。其中,Twitter的Storm中默认使用ZeroMQ作为数据流的传输。
- ActiveMQ:
- ActiveMQ是Apache下的一个子项目。 ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,持久化,类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。
- 同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。
- 在询证调研过程中,ActiveMQ会因为频繁发送大数据消息而偶尔出现崩溃的情况。
artemis是ActiveMQ的下一代MQ,SpringBoot自身已集成ActiveMQ。
- Kafka/Jafka:Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:
- 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。(文件追加的方式写入数据,过期的数据定期删除)
- 高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒数百万的消息。
- 支持通过Kafka服务器和消费机集群来分区消息。
- 支持Hadoop并行数据加载。
1.3 使用消息队列的优缺点:
- 优点: 异步、解耦、削峰等
- 缺点: 系统复杂性提高,可用性受到挑战;
1.4 其他文章介绍
- 关于SpringCloud 的系统性学习,可参考博主其他文章:
二. 实战集成过程
2.1 概述:
- 经过章节一的讲解,我们大概知道了各个消息队列的特点。每个MQ都有其特点,我们可以根据实际需求选择合适的MQ应用于项目中。
- 此章节将讲述Kafka与SpringCloud整合
2.2 实战步骤:
-
配置SpringCloud项目,我们假设已有SpringCloud项目;
-
引入Kafka配置:
- Gradle:
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-stream-kafka', version: '1.3.2.RELEASE'
- Maven:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> <version>1.3.2.RELEASE</version> </dependency>
- Gradle:
-
定义DemoSource:
- Kotlin:
import org.springframework.cloud.stream.annotation.Input import org.springframework.cloud.stream.annotation.Output import org.springframework.messaging.MessageChannel import org.springframework.messaging.SubscribableChannel interface DemoSource { @Output(DEMO_OUTPUT) fun output(): MessageChannel? @Input(DEMO_INPUT) fun input(): SubscribableChannel? companion object { const val DEMO_INPUT = "demo-input" const val DEMO_OUTPUT = "demo-output" } }
- java:
interface ProductProcessor { String DEMO_INPUT = "demo-input"; String DEMO_OUTPUT = "outputProductAdd"; @Input(DEMO_INPUT) SubscribableChannel inputOut(); @Output(DEMO_OUTPUT) MessageChannel output(); }
Kotlin和Java 任选其一,根据自身项目所选语言来写。笔者是Kotlin项目所以用的Kotlin代码,为了部分同学方便,又写了份java代码。
- Kotlin:
-
启动类添加 @EnableBinding注解,并绑定Source:
import com.sino.migration.kafka.DemoSource import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.cloud.openfeign.EnableFeignClients import org.springframework.cloud.stream.annotation.EnableBinding import org.springframework.scheduling.annotation.EnableScheduling /** * @program: DemoApplication * @description: 启动类 * @author: 暗余 * @create: 2020-08-19 */ @EnableFeignClients @SpringBootApplication @EnableScheduling @EnableBinding(value = [DemoSource::class]) class DemoApplication{ companion object{ @JvmStatic fun main(args: Array<String>) { runApplication<DemoApplication>(*args) } } }
如果定义了多个Source,就引入多个,在@EnableBinding中进行绑定。Java版本的启动类则直接在上方添加此注解即可:@EnableBinding({DemoSource.class}),多个以逗号隔开
-
配置Application.yml,添加Zookeeper、Kafka配置,以及Topic:
spring: cloud: stream: kafka: binder: brokers: 192.168.9.20:9092 # kafka服务地址和端口 zk-nodes: 192.168.9.20:2181 # ZK的集群配置地址和端口 bindings: demo-input: destination: demo-topic demo-output: destination: demo-topic
这里destination里写的是Topic,消息生产者和消息的消费者必须topic一样才能接收到彼此的消息内容;brokers是kafka地址,zk_nodes是节点地址;
此处项目是以生产者和消费者都在同一个项目中,若是不同微服务,配置方法一致。
三. 测试并获取结果
- 编写消息生产者和消息消费者:
- Kotlin版本代码:
import com.sino.migration.kafka.DemoSource import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import org.springframework.beans.factory.annotation.Autowired import org.springframework.cloud.stream.annotation.StreamListener import org.springframework.messaging.support.MessageBuilder import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController @Api(tags = ["测试Kafka消息发送"]) @RestController class DemoController{ @Autowired private lateinit var demoSource: DemoSource @PostMapping("pushDemo") @ApiOperation("发送设备历史消息") fun pushDeviceHistory(msg: String): String{ this.demoSource.output()!!.send( MessageBuilder.withPayload(msg).build()) return "消息已成功发送" } @StreamListener(DemoSource.DEMO_OUTPUT) fun receive(messageBody: String){ System.err.println("接收到了消息,内容为:$messageBody") } }
- Java版本代码:
import com.sino.migration.kafka.DemoSource; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.messaging.support.MessageBuilder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = {"测试Kafka消息发送"}) @RestController public class DemoController { private final DemoSource demoSource; public DemoController(DemoSource demoSource) { this.demoSource = demoSource; } @PostMapping("pushDemo") @ApiOperation("发送设备历史消息") public String pushDeviceHistory(String msg) { this.demoSource.output().send( MessageBuilder.withPayload(msg).build()) return "消息已成功发送"; } @StreamListener(DemoSource.DEMO_OUTPUT) public void receive(String messageBody) { System.err.println("接收到了消息,内容为:"+messageBody); } }
- Kotlin版本代码:
3.1 测试发送消息:
- 发送消息:
- 接收到消息:
四. 中间遇到的一些坑,及排错思路详解
4.1 问题现象:
-
博主一开始引入的依赖为:
-
启动出现错误:
博主参照的网上教程引入的依赖,每个人的环境不同,可能我这的项目环境与此依赖不匹配。
4.2 解决思路
- 查看依赖版本:
-
在Dependencies中查看依赖:
-
发现依赖重复,于是去掉Stream依赖,重新启动
-
发现依旧报错,提示依旧为调用不存在的方法。
-
现在不可能是重复依赖造成的问题,又是调用不存在的方法。那说明可能版本不对。博主采用的是最新的版本,如果启动失败,说明之前的这个方法已经在新版本中去掉了,于是博主降低依赖版本;将版本替换为: 3.0.0
-
刷新后重新启动,发现启动正常,问题解决!
问题总结: 一般出错,可以通过控制台打印的信息进行分析,能够为你解决提供思路。
-
五. 补充: SpringBoot中集成Kafka
5.1 引入依赖
- Maven版本:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.0</version>
</dependency>
- Gradle版本:
compile group: 'org.springframework.kafka', name: 'spring-kafka', version: '2.3.4.RELEASE'
compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.2.0'
5.2 引入配置
spring:
kafka:
bootstrap-servers: 118.178.141.31:9092
producer:
retries: 0
batch-size: 16384
buffer-memory: 33554432
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
listener:
missing-topics-fatal: false
log-container-config: false
concurrency: 5
# 手动提交
ack-mode: manual_immediate
在application.yml中进行配置
5.3 KafkaConsts类
public class KafkaConsts {
/**
* 默认分区大小
*/
public static final Integer DEFAULT_PARTITION_NUM = 3;
/**
* Topic 名称
*/
public static final String TOPIC_LOCATION = "topicName";
}
5.4 配置类 KafkaConfig
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import org.springframework.kafka.listener.ContainerProperties;
@Configuration
@EnableConfigurationProperties({KafkaProperties.class})
@EnableKafka
public class KafkaConfig {
private final KafkaProperties kafkaProperties;
public KafkaConfig(KafkaProperties kafkaProperties) {
this.kafkaProperties = kafkaProperties;
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
factory.setBatchListener(true);
factory.getContainerProperties().setPollTimeout(3000);
return factory;
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
}
@Bean("ackContainerFactory")
public ConcurrentKafkaListenerContainerFactory<String, String> ackContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
return factory;
}
}
5.5 发送信息类 KafkaMessageSender
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
@Service
public class KafkaMessageSender {
private KafkaTemplate<String, String> kafkaTemplate;
private static final Logger log= LoggerFactory.getLogger(KafkaMessageSender.class);
@Autowired
public void setKafkaTemplate(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void sendMessage(String topic, String data) {
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, data);
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onFailure(Throwable ex) {
log.error("kafka sendMessage error, ex = {}, topic = {}, data = {}", ex, topic, data);
}
@Override
public void onSuccess(SendResult<String, String> result) {
log.info("kafka sendMessage success topic = {}, data = {}",topic, data);
}
});
}
}
5.6 给指定的Topic推送消息demo图示:
- 引入KafkaMessageSender:
@Autowired
private KafkaMessageSender kafkaMessageSender;
- 发送消息:
kafkaMessageSender.sendMessage("topic", "要发送的消息内容")