Spring Cloud Stream/Spring Boot 集成Kafka

实战: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 其他文章介绍

二. 实战集成过程

2.1 概述:

  • 经过章节一的讲解,我们大概知道了各个消息队列的特点。每个MQ都有其特点,我们可以根据实际需求选择合适的MQ应用于项目中。
  • 此章节将讲述Kafka与SpringCloud整合

2.2 实战步骤:

  1. 配置SpringCloud项目,我们假设已有SpringCloud项目;

  2. 引入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>
      
  3. 定义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代码。

  4. 启动类添加 @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}),多个以逗号隔开

  5. 配置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);
      	    }
      	}
      

3.1 测试发送消息:

  • 发送消息:
    在这里插入图片描述
  • 接收到消息:
    在这里插入图片描述

四. 中间遇到的一些坑,及排错思路详解

4.1 问题现象:

  • 博主一开始引入的依赖为:
    在这里插入图片描述

  • 启动出现错误:
    在这里插入图片描述

    博主参照的网上教程引入的依赖,每个人的环境不同,可能我这的项目环境与此依赖不匹配。

4.2 解决思路

  1. 查看依赖版本:
    • 在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", "要发送的消息内容")
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暗余

码字来之不易,您的鼓励我的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值