Kafka-高性能的分布式消息队列


参考

https://zhuanlan.zhihu.com/p/79579389
https://www.jianshu.com/p/a036405f989c
https://blog.csdn.net/zhaoyachao123/article/details/89527233
作者:扎心了,老铁
https://www.cnblogs.com/qingyunzong/category/1212387.html

前言

本文是本人在之前的实际开发中使用kafka中的总结以及对应的代码,相关参考的优秀的博客文档已经在开头参考中贴出,可以直接移步学习

一 简介

Kafka是一个分布式,支持分区(partition),多副本(replica),基于zookeeper协调的高性能的分布式消息系统。 这句话基本上是涵盖了kafka全部的特性特征

二 架构

附一张kakfa的架构图(该架构图片来源于:51CTO博客
在这里插入图片描述
在一套完备的kafka架构中,是存在着多个Producer,多个Broker,多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个ConsumerGroup。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息

1 zookeeper在kafka集群中的作用

kafka需要依赖于zookeeper进行集群节点的管理,以及在consumer group 发生变化时进行rebalance。
kafka对应zookeeper的数据存储结构如下:
在这里插入图片描述
上面的kafka01并不是zookeeper的节点。可以通过zookeeper的get / 进行查看
在这里插入图片描述

1.1 Broker注册

一个Broker是一个kafka的部署节点,Broker是分布式部署并且相互依赖,每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点
如/brokers/ids/[0…N]。
该节点存储着当前注册broker的基本信息:
在这里插入图片描述
kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker Id进行注册,其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。

{
    "listener_security_protocol_map":{
        "PLAINTEXT":"PLAINTEXT"
    },
    "endpoints":[
        "PLAINTEXT://192.168.129.128:9192"
    ],
    "jmx_port":-1, //jmx端口号
    "host":"192.168.129.128", //主机ip地址
    "timestamp":"1571828014387", //kafka broker初始启动时的时间戳,
    "port":9192, //kafka broker的服务端端口号,由server.properties中参数port确定
    "version":4 //版本号
}
1.2 Topic注册

在kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上并将其分配在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:
在这里插入图片描述
/borkers/topics
Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,如/brokers/topics/login和/brokers/topics/search等。Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。
在这里插入图片描述
使用**get /brokers/topics/[topic]**查看数据
在这里插入图片描述

{
    "version":1, //版本编号,目前固定为1
    "partitions":{
    	//partitionId编号
        "0":[ 
            0 //同步副本组brokerId列表
        ],
        "1":[
            0
        ],
        "2":[
            0
        ],
        "3":[
            0
        ]
    }
}

使用 get /broker/topics/[topic]/partition/0/state 查看partition状态
在这里插入图片描述

{
    "controller_epoch":7, //表示kafka集群中的中央控制器选举次数
    "leader":0, //表示该partition选举lead的brokerId,
    "version":1, //版本编号,固定1
    "leader_epoch":6, //该partition lead选举次数
    "isr":[
        0  //同步副本组brokerId列表
    ]
}

使用 get /controller_epoll
在这里插入图片描述
此值为一个数字,kafka集群中第一个broker第一次启动时为1,以后只要集群中center controller中央控制器所在broker变更或挂掉,就会重新选举新的center controller,每次center controller变更controller_epoch值就会 + 1;

使用 get /controller 查看center controller中央控制器所在kafka broker的信息
在这里插入图片描述

{
    "version":1, //版本编号默认为1,
    "brokerid":0, //kafka集群中broker唯一编号,
    "timestamp":"1571828013306" //kafka broker中央控制器变更时的时间戳
}
1.3 生产者负载均衡

由于同一个Topic消息会被分区并将其分布在多个Broker上,因此,生产者需要将消息合理地发送到这些分布式的Broker上,那么如何实现生产者的负载均衡,Kafka支持传统的四层负载均衡,也支持Zookeeper方式实现负载均衡。

  • (1)四层负载均衡,根据生产者的IP地址和端口来为其确定一个相关联的Broker。通常,一个生产者只会对应单个Broker,然后该生产者产生的消息都发往该Broker。这种方式逻辑简单,每个生产者不需要同其他系统建立额外的TCP连接,只需要和Broker维护单个TCP连接即可。但是,其无法做到真正的负载均衡,因为实际系统中的每个生产者产生的消息量及每个Broker的消息存储量都是不一样的,如果有些生产者产生的消息远多于其他生产者的话,那么会导致不同的Broker接收到的消息总数差异巨大,同时,生产者也无法实时感知到Broker的新增和删除。
  • (2) 使用Zookeeper进行负载均衡,由于每个Broker启动时,都会完成Broker注册过程,生产者会通过该节点的变化来动态地感知到Broker服务器列表的变更,这样就可以实现动态的负载均衡机制。
1.4 消费者负载均衡

与生产者类似,Kafka中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的Broker服务器上接收消息,每个消费者分组包含若干消费者,每条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定的Topic下面的消息,互不干扰。

1.5 分区与消费者的关系

消费组 (Consumer Group):
consumer group 下有多个Consumer(消费者)
对于每个消费组(consumer group),kafka都会为其分配一个唯一的全局的Group ID,Group 内部的所有消费者共享该 ID订阅的topic下的每个分区只能分配给某个 group 下的一个consumer(当然该分区还可以被分配给其他group)
同时,Kafka为每个消费者分配一个Consumer ID,通常采用"Hostname:UUID"形式表示。
在kafka中,规定每个消费分区(partition)只能被同组的一个消费者进行消费, ,因此,需要在 Zookeeper 上记录 消息分区 与 Consumer 之间的关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上,例如:
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
其中,[broker_id-partition_id]就是一个 消息分区 的标识,节点内容就是该 消息分区 上 消费者的Consumer ID。

1.6 消息消费进度Offset 记录

在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度Offset记录到Zookeeper上,以便在该消费者进行重启或者其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。Offset在Zookeeper中由一个专门节点进行记录,其节点路径为
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]
节点内容就是Offset的值。

1.7 消费者注册

消费者服务器在初始化启动时加入消费者分组的步骤如下

注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。

消费者分组 中的 消费者 的变化注册监听。每个 消费者 都需要关注所属 消费者分组 中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。

对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。

进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个 消费者 消费而进行 消费者 与 消息 分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。

2 kafka如何保证数据的可靠性和一致性

Kafka的ack机制,指的是producer的消息发送确认机制,这直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像硬币的两面,两者不可兼得,只能平衡。
request.required.acks有三个只0,1,-1(all),

  • 0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据,简单的说,producer发送一次就不再发送了,不管是否发送成功
  • -1(all):简单地说producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功
  • 1:简单来说就是,producer只要收到一个分区副本成功写入的通知就认为推送消息成功了。这里有一个地方需要注意,这个副本必须是leader副本。只有leader副本成功写入了,producer才会认为消息发送成功。数据丢失,如果其他的follow副本在拉取之前的follow副本的时候不行
3 kafka的数据丢失问题

常见的数据丢失的解决方案:
1,消费端的数据丢失:

  • (1)关闭掉消费段自动提交offset,设置处理完消息之后手动提交,这个还是会出现消费端的数据丢失
  • (2)自己还没有处理完,系统程序就挂掉了,保证自己程序的幂等性

2,kafka本身弄丢了数据:
丢失场景:这块比较常见的一个场景,就是kafka某个broker宕机,然后重新选举partiton的leader时。要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,他不就少了一些数据?这就丢了一些数据。

  • 1,设置topic的replication.factor参数,大于1,要求每个partition的副本大于1
  • 2,在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower。
  • 3,设置ack为-1或者是all
  • 4,设置无限重试的次数,

3,生产端的数据丢失问题,
设置ack为all

4 kafka的ISR机制

问题如下:kafka为了保证数据的一致性使用了ISR机制,isr 的全称是:In-Sync Replicas isr 是一个副本的列表,里面存储的都是能跟leader 数据一致的副本,确定一个副本在isr列表中,有2个判断条件

  • 条件1:根据副本和leader 的交互时间差,如果大于某个时间差 就认定这个副本不行了,就把此副本从isr 中剔除,此时间差根据配置参数rerplica.lag.time.max.ms=10000 决定 单位ms
  • 条件2:根据leader 和副本的信息条数差值决定是否从isr 中剔除此副本,此信息条数差值根据配置参数rerplica.lag.max.messages=4000 决定 单位ms
5 kafka的数据存储结构

kafka的指定topic是分为多个partition,每一个partition是分为多个Segment。以创建topic:zhoucg_topic为例:

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 4 --topic zhoucg_topic

其中,对应的partition的个数为4。
kafka的数据存储是位于kafka目录下的log目录中
在这里插入图片描述
每一个partition为一个目录。partition命令的规则是topic的名称加上一个序号,序号从0开始,每一个partition目录下的文件被平均切割成大小相等的数据文件(默认是500m,可以自行调整)
其中,每一个数据文件都被称为一个段(segment file)但每个段消息数量不一定相等,这种特性能够使得老的segment可以被快速清除。默认保留7天的数据。
这里我们测试模拟向topic插入大量数据
在这里插入图片描述
Segment File组成:由3大部分组成。分别为index file和data file,此2个文件一一相应,成对出现,后缀”.index”和“.log”分别表示为segment索引文件、数据文件,以.timeindex为后缀的表示对应kafka的具体时间日志
在这里插入图片描述
索引文件.index和数据文件.log的对应关系:
在这里插入图片描述
上图的左半部分是索引文件,里面存储的是一对一对的key-value,其中key是消息在数据文件(对应的log文件)中的编号,比如“1,3,6,8……”,分别表示在log文件中的第1条消息,第3条消息,第6条消息、第8条消息……,那么为什么在index文件中这些编号不是连续的呢?
这是因为index文件并没有为数据文件中的每条消息建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。
但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
其中以索引文件中元数据3,497为例,其中3代表在右边log数据文件中从上到下第3个消息(在全局partiton表示第368772个消息),
其中497表示该消息的物理偏移地址(位置)为497。

5 kafka的Producer消息发送如何指定topic对应的Partition

Produer发送消息时,会根据Partition机制选择将其存储到哪一个Partition,如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。
在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Partition,Paritition机制可以通过指定Producer的paritition. class这一参数来指定,该class必须实现kafka.producer.Partitioner接口。
例如:
1,首先设置生产者对应的partitioner.class参数

//此处对应的是kafka定义分区的类
config.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.common.kafka.Partitions");

2,然后创建一个Partitions类,该类需要继承自org.apache.kafka.clients.producer.Partitioner

package com.common.kafka;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.Node;

import java.util.List;
import java.util.Map;

/**
 * kafka生产者消息设置自定义分区
 * @author: zhoucg
 * @date: 2019-10-22
 */
public class Partitions implements Partitioner{

    /**
     *
     * @param topic 指定topic数据
     * @param key 当前key
     * @param keyBytes 当前key的字节数组
     * @param value 当前value
     * @param valueBytes 当前value的字节数组
     * @param cluster 当前集群信息
     * @return 返回的值对应当前消息的partition
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        /**
         * 可以根据对应的集群环境和对应的value数据进行负载指定message对应的partition数据
         */
        List<Node> nodes = cluster.nodes();
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

其中Cluster类的相关数据:
在这里插入图片描述
在partition()方法中,我们可以自行设计相应的算法规则,将对应的message对应到指定的partition中

6 kafka的Replica分布算法

为了更好的做负载均衡,kafka尽量将所有的partition均匀分配到整个集群中,一个典型的部署方式是一个Topic的Partition数量大于broker的数量,同时为了提高kafka的容错能力,也需要将同一个partition的Replica尽量分散到不同的机器,实际上,如果所有的Replica都在同一个Broker上,那一旦该Broker宕机,该Partition的所有Replica都无法工作。
Kafka分配Replica的算法如下:

  • 1,将所有的Broker(假设共n个Broker)和待分配的Partition排序,Broker的排序是存储在zookeeper中的/broker/ids/[broker
    id] 下,partition的排序是存储到/broker/topics/{topicName}/partition/[0,1,N]
  • 2,将第i个Partition分配到第(i mod n)个Broker上,
  • 3,将第i个Partition的第 j 个Replica分配到(i+j mode n)个Broker上
7 kafka的Produer写数据流程

参照:Kafka学习之路 (三)Kafka的高可用#四、producer发布消息

8 kafka的Broker存储消息

参考:5 kafka的数据存储结构

9 kafka和其他消息中间件的区别(面试)

kafka和其他消息中间件的优势

  • 1,kafka的单机吞吐量比较高,单机吞吐量可以达到10万级
  • 2,kafka的数据处理的时效性是毫秒级的
  • 3,kafka的消息可靠性可以通过参数的优化做到0丢失,rocketMQ也是可以,rabbitMQ和activeMQ都会丢失数据
  • 4,kafka的可用性分布式架构,可用性很高
  • 5,kafka主要适用于大数据实时计算和日志收集

kafka和其他消息中间价的对比
在这里插入图片描述

三 kafka常见命令

1,查看kafka topic列表,使用 --list

bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --list

2,查看kafka特定topic的详情,使用–topic与–describe参数

bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --topic lx_test_topic --describe
Topic:lx_test_topic     PartitionCount:1        ReplicationFactor:1     Configs:
Topic: lx_test_topic    Partition: 0    Leader: 0       Replicas: 0     Isr: 0
列出了lx_test_topic的parition数量、replica因子以及每个partition的leader、replica信息

3,查看指定topic中的实时推送过来的数据

./bin/kafka-console-consumer.sh --bootstrap-server localhost:9192 --topic  RT.DPC.STATION.DI

4,创建指定topic

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 4 --topic test
replication-factor:表示副本个数
partitions:表示分区个数

5,创建生产者

bin/kafka-console-producer.sh --broker-list localhost:9192 --topic test --producer.config config/producer.properties
producer.properties:表示对应生产者的相关参数配置

6,创建消费者

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --new-consumer --from-beginning --consumer.config config/consumer.properties
from-beginning:表示从对应的分区的offset0开始消费数据
consumer.properties:消费者对应的相关参数配置

7,topic分区扩容

./kafka-topics.sh --zookeeper localhost:2181 -alter --partitions 4 --topic zhoucg_topic

8,topic副本修改

1,根据topic的分区的分区情况修改partitions-topic.json信息
{
        "partitions":
                [
                {
                        "topic": "test1",
                        "partition": 0,
                        "replicas": [1,2]
                },
                {
                        "topic": "test1",
                        "partition": 1,
                        "replicas": [0,3]
                },
                {
                        "topic": "test1",
                        "partition": 2,
                        "replicas": [4,5]
                }
                ],
        "version":1
}
2,执行副本迁移
../bin/kafka-reassign-partitions.sh --zookeeper 127.0.0.1:2181 --reassignment-json-file partitions-topic.json --execute 
3,查看迁移情况
../bin/kafka-reassign-partitions.sh --zookeeper 127.0.0.1:2181 --reassignment-json-file partitions-topic.json --verify
Status of partition reassignment:
Reassignment of partition [mx_prd_nginx_access,0] is still in progress
Reassignment of partition [mx_prd_nginx_access,1] completed successfully
Reassignment of partition [mx_prd_nginx_access,2] is still in progress

四 java操作kafka

简单的生产者消费者类

import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Created by zhoucg on 2018-09-25.
 */
public class KafkaUtil {

    private static KafkaProducer<String,byte[]> producer = null;

    private static ConsumerConnector consumer = null;

    static {
        Map<String,Object> config = new HashMap<>();

        //kafka服务器地址
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.129.128:9192");
        //kafka消息序列化类 即将传入对象序列化为字节数组
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.ByteArraySerializer");
        //kafka消息key序列化类 若传入key的值,则根据该key的值进行hash散列计算出在哪个partition上
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        //当多条消息发送到同一个partition时,该值控制生产者批量发送消息的大小,批量发送可以减少生产者到服务端的请求数,有助于提高客户端和服务端的性能。
        config.put(ProducerConfig.BATCH_SIZE_CONFIG,1024*1024*5);
        //往kafka服务器提交消息间隔时间,0则立即提交不等待
        config.put(ProducerConfig.LINGER_MS_CONFIG,0);
        //kakfa的ack模式 0,1,-1(all)
        //config.put(ProducerConfig.ACKS_CONFIG,"all");
        //生产者发送失败后,重试的次数
        config.put(ProducerConfig.RETRIES_CONFIG,1);

        //消费者配置文件
        Properties properties = new Properties();
        properties.put("zookeeper.connect","192.168.129.128:2181");
        properties.put("group.id","123");
        properties.put("auto.commit.interval.ms","1000");
        ConsumerConfig consumerConfig = new ConsumerConfig(properties);
        producer = new KafkaProducer<>(config);
        consumer = Consumer.createJavaConsumerConnector(consumerConfig);


    }


    /**
     *启动一个消费程序
     * @param topic 要消费的topic名称
     * @param handler 自己的处理逻辑的实现
     * @param threadCount 消费线程数,该值应小于等于partition个数,多了也没用
     */
    public static <T extends Serializable>void startConsumer(String topic, final MqMessageHandler<T> handler, int threadCount) throws Exception{
        if(threadCount<1)
            throw new Exception("处理消息线程数最少为1");
        //设置处理消息线程数,线程数应小于等于partition数量,若线程数大于partition数量,则多余的线程则闲置,不会进行工作
        //key:topic名称 value:线程数
        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
        topicCountMap.put(topic, new Integer(threadCount));

        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =  consumer.createMessageStreams(topicCountMap);


        //Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCountMap);
        //声明一个线程池,用于消费各个partition
        ExecutorService executor= Executors.newFixedThreadPool(threadCount);
        //获取对应topic的消息队列
        List<KafkaStream<byte[], byte[]>> streams = consumerMap.get(topic);
        //为每一个partition分配一个线程去消费
        for (final KafkaStream stream : streams) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    ConsumerIterator<byte[], byte[]> it = stream.iterator();
                    //有信息则消费,无信息将会阻塞
                    while (it.hasNext()){
                        T message=null;
                        try {
                            //将字节码反序列化成相应的对象
                            byte[] bytes=it.next().message();
                            message = (T) SerializationUtils.deserialize(bytes);
                        } catch (Exception e) {
                            e.printStackTrace();
                            return;
                        }
                        //调用自己的业务逻辑
                        try {
                            handler.handle(message);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
        }
    }

    /**
     *发送消息,发送的对象必须是可序列化的
     */
    public static Future<RecordMetadata> send(String topic, Serializable value) throws Exception{
        try {
            //将对象序列化称字节码
            byte[] bytes=SerializationUtils.serialize(value);
            Future<RecordMetadata> future=producer.send(new ProducerRecord<String,byte[]>(topic,bytes));
            RecordMetadata recordMetadata  = future.get();


            return future;
        }catch(Exception e){
            throw e;
        }
    }

    //内部抽象类 用于实现自己的处理逻辑
    public static abstract class MqMessageHandler<T extends Serializable>{
        public abstract void handle(T message);
    }


    public static void main(String[] args) throws Exception {
        //发送一个信息
        send("zhoucg_topic",new User("id","userName", "password"));
        //为test启动一个消费者,启动后每次有消息则打印对象信息
        /*KafkaUtil.startConsumer("test", new MqMessageHandler<User>() {
            @Override
            public void handle(User user) {
                //实现自己的处理逻辑,这里只打印出消息
                System.out.println(user.toString());
            }
        },2);*/
    }

    static class User implements Serializable{

        private static final long serialVersionUID = 8576358642877640767L;

        private People people;

        private String name;

        private int age;


        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public People getPeople() {
            return people;
        }

        public void setPeople(People people) {
            this.people = people;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id='" + id + '\'' +
                    ", userName='" + userName + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }

        private String id;
        private String userName;
        private String password;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public User(){}

        public User(String id,String userName,String password){
            this.id = id;
            this.userName = userName;
            this.password = password;
        }
    }

    static class People {
        private String name;

        private String age ;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAge() {
            return age;
        }

        public void setAge(String age) {
            this.age = age;
        }
    }

}

kafka生产者创建:

基本配置信息:
//kafka服务器地址
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.129.128:9192");
//kafka消息序列化类 即将传入对象序列化为字节数组
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.ByteArraySerializer");
//kafka消息key序列化类 若传入key的值,则根据该key的值进行hash散列计算出在哪个partition上
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//当多条消息发送到同一个partition时,该值控制生产者批量发送消息的大小,批量发送可以减少生产者到服务端的请求数,有助于提高客户端和服务端的性能。
config.put(ProducerConfig.BATCH_SIZE_CONFIG,1024*1024*5);
//往kafka服务器提交消息间隔时间,0则立即提交不等待
config.put(ProducerConfig.LINGER_MS_CONFIG,0);
//kakfa的ack模式 0,1,-1(all)
//config.put(ProducerConfig.ACKS_CONFIG,"all");
//生产者发送失败后,重试的次数
config.put(ProducerConfig.RETRIES_CONFIG,1);

//创建生产者:
producer = new KafkaProducer<>(config);

kafka消费者创建:

//消费者配置文件
Properties properties = new Properties();
properties.put("zookeeper.connect","192.168.129.128:2181");
properties.put("group.id","123");
properties.put("auto.commit.interval.ms","1000");
ConsumerConfig consumerConfig = new ConsumerConfig(properties);
consumer = Consumer.createJavaConsumerConnector(consumerConfig);

kafka生产者发送消息:

	/**
     *发送消息,发送的对象必须是可序列化的
     */
    public static Future<RecordMetadata> send(String topic, Serializable value) throws Exception{
        try {
            //将对象序列化称字节码
            byte[] bytes=SerializationUtils.serialize(value);
            Future<RecordMetadata> future=producer.send(new ProducerRecord<String,byte[]>(topic,bytes));
            RecordMetadata recordMetadata  = future.get();


            return future;
        }catch(Exception e){
            throw e;
        }
    }

其中,Future返回的RecordMetadata对象能够获取对应message的offset,以及写入的topic的partition索引
kafka消费者消费消息:

/**
     *启动一个消费程序
     * @param topic 要消费的topic名称
     * @param handler 自己的处理逻辑的实现
     * @param threadCount 消费线程数,该值应小于等于partition个数,多了也没用
     */
    public static <T extends Serializable>void startConsumer(String topic, final MqMessageHandler<T> handler, int threadCount) throws Exception{
        if(threadCount<1)
            throw new Exception("处理消息线程数最少为1");
        //设置处理消息线程数,线程数应小于等于partition数量,若线程数大于partition数量,则多余的线程则闲置,不会进行工作
        //key:topic名称 value:线程数
        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
        topicCountMap.put(topic, new Integer(threadCount));

        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =  consumer.createMessageStreams(topicCountMap);


        //Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCountMap);
        //声明一个线程池,用于消费各个partition
        ExecutorService executor= Executors.newFixedThreadPool(threadCount);
        //获取对应topic的消息队列
        List<KafkaStream<byte[], byte[]>> streams = consumerMap.get(topic);
        //为每一个partition分配一个线程去消费
        for (final KafkaStream stream : streams) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    ConsumerIterator<byte[], byte[]> it = stream.iterator();
                    //有信息则消费,无信息将会阻塞
                    while (it.hasNext()){
                        T message=null;
                        try {
                            //将字节码反序列化成相应的对象
                            byte[] bytes=it.next().message();
                            message = (T) SerializationUtils.deserialize(bytes);
                        } catch (Exception e) {
                            e.printStackTrace();
                            return;
                        }
                        //调用自己的业务逻辑
                        try {
                            handler.handle(message);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
        }
    }
发布了55 篇原创文章 · 获赞 14 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zcswl7961/article/details/102665087