kafka学习总结(含java生产者、消费者、Topic操作代码)

java 同时被 3 个专栏收录
2 篇文章 0 订阅
1 篇文章 0 订阅
2 篇文章 0 订阅
kafka(http://kafka.apache.org)是一款分布式消息发布和订阅的系统,具有高性能和高吞吐率。它的优点是能够直接使用磁盘进行存储、线性读写、速度快,避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收。首先简单说明一下相关概念。
生产者(Producer):信息的发布者(publish)
消费者(Consumer):消息的接收者,也就是订阅者(subscribe)
话题(Topic):消费者发消息的对象,消费者也是从Topic中获取信息
组(Group):同一组的消费者对于同一消息只会收到一次,每一组都会对应一个id
分区(partitions):消费者接收消息的最小单位,实现了Kafka的高吞吐量

这里列出一张图来直观的看生产者与消费者的关系(图片来源:http://czj4451.iteye.com/blog/2041096)


可以看出多个broker协同合作,producer、consumer和broker三者之间通过zookeeper来协调请求和转发。
笔者使用Kafka来进行消息队列的分布式处理。包含一个生产者和多个消费者,这里最主要的地方就在于要让消费者接收到的数据都不一样并且在数量上尽量相近以实现负载均衡。因此对于Kafka分区是最关键的部分,在说具体应用之前还是从部署开始说起,笔者是在windows系统下做的代码编写与测试。
第一步是安装JDK环境这里不再多说。
第二步部署zookeeper:
去官网http://zookeeper.apache.org/releases.html#download下载压缩包,解压在某个文件夹下,然后将“zoo_sample.cfg”重命名为“zoo.cfg”,然后打开“zoo.cfg”编辑dataDir=“自己想放的目录下,一般是解压目录/tmp”,最后在系统环境变量中添加ZOOKEEPER_HOME = ”你的解压目录“, 编辑path系统变量,添加%ZOOKEEPER_HOME%\bin
最后打开命令行输入zkserver即可启动zookeeper,这里要注意的是使用kafka期间这个命令行窗口是不能关的。zookeeper的默认端口是2181.
第三步部署kafka:
从官网http://kafka.apache.org/downloads.html下载二进制(Binary )版本的kafka,同样解压找到配置文件server.properties,编辑log.dirs为自己想要存放的目录下,编辑zookeeper.connect=localhost:2181,笔者是在本地做的测试,所以使用localhost,应用时要按照实际情况进行修改,Kafka的默认端口为9092。最后在安装目录下按下Shift+右键,选择“打开命令窗口”选项,打开命令行。输入.\bin\windows\kafka-server-start.bat .\config\server.properties启动kafka。之后可以通过打开/bin/windows目录,同样按下Shift+右键,选择“打开命令窗口”选项,打开命令行进行相关操作,比如:
创建一个topic:
kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 2 --topic test
删除一个topic
kafka-topics.bat --zookeeper localhost:2181 --delete --topic test
修改topic
kafka-topics.bat –zookeeper 127.0.0.1:2181 -–alter -–partitions 4 –topic test
其中partitions指的就是分区数,而java利用java字符串数组加上TopicCommand类也可以实现对于Topic的操作,包括增删改,具体实现代码如下:

import kafka.admin.TopicCommand;

public class api {
    public static void add() {
        String[] options = new String[]{
                "--create",
                "--zookeeper",
                "127.0.0.1:2181",
                "--partitions",
                "24",
                "--topic",
                "recordData",
                "--replication-factor",
                "1"
        };
        TopicCommand.main(options);
    }

    public static void delete() {
        String[] options = new String[]{
                "--zookeeper",
                "127.0.0.1:2181",
                "--delete",
                "--topic",
                "test3"
        };
        TopicCommand.main(options);
    }

    public static void alter(String[] args) {
        String[] options = new String[]{
                "--zookeeper",
                "127.0.0.1:2181",
                "--alter",
                "--partitions",
                "12",
                "--topic",
                "test3"
        };
        TopicCommand.main(options);
    }
}
最后就是生产者与消费者代码的实现,注意在代码运行期间zookeeper和kafka的窗口都不能关闭。在编写代码之前,关于分区(partitions)与消费者的关系应当重点理解一下,这样才有利于后面真正应用时部署不会出错。正如前面所说,消费者接收信息的最小单位是partitions,也就是说同一组的消费者接收到的消息是生产者将所有信息分成几块之后发布,按块给消费者并且尽量均衡。这里借助http://www.jianshu.com/p/6233d5341dfe里的一些图来进行更详细的说明。举个例子,生产者要发送的数据为1 2 3 4 5 6,如果partitions的数量为1,消费者数量比partitions大为2,那么就会出现其中一个消费者接收到1 2 3 4 5 6,另一个消费者收不到消息的情况,如图所示。

如果partitions和消费者一样数量为2的话,那么两个消费者就会收到一样多的数据,其中一个收到1 3 5,另一个收到2 4 6这种间隔的数据,说明发布者是一条一条数据分下去的。如果partitions的数量为3,那么其中一个消费者就会比另一个消费者多接收到一个分区的消息,其中一个收到1 4 3 6,另一个收到2 5,如图所示。

最后展示一下出现不同组的情况下是如何分发数据的。

因此我们在设置partitions的时候最好设置成消费者的倍数,这里笔者推荐24的倍数,可以满足大多种情况下消费者数量的需求,同时分区也不能太多会给kafka造成负担,具体要根据实际情况而定。
最后来看一下生产者和消费者所对应的代码,笔者这里所要传说的数据类型是Map对象,因此在传输之前要先将类转换为字节数组,后面再转换回来,如果是传输字符串就可以直接传输了,这里附上对象与字节数组的相互转化代码,感谢http://blog.csdn.net/u012737182/article/details/54135533的分享。

package com.util;

/**
 * Created by chenzhsh on 2017/7/13 0013.
 * 对象字节数组的相互转换,传输数据用
 */
import java.io.*;

public class BeanUtil {
    private BeanUtil(){}
    /**
     * 对象转字节数组
     * @param obj
     * @return
     */
    public static byte[] ObjectToBytes(Object obj){
        byte[] bytes = null;
        ByteArrayOutputStream bo = null;
        ObjectOutputStream oo = null;
        try {
            bo = new ByteArrayOutputStream();
            oo = new ObjectOutputStream(bo);
            oo.writeObject(obj);
            bytes = bo.toByteArray();

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(bo!=null){
                    bo.close();
                }
                if(oo!=null){
                    oo.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bytes;
    }
    /**
     * 字节数组转对象
     * @param bytes
     * @return
     */
    public static Object BytesToObject(byte[] bytes){
        Object obj = null;
        ByteArrayInputStream bi = null;
        ObjectInputStream oi = null;
        try {
            bi =new ByteArrayInputStream(bytes);
            oi =new ObjectInputStream(bi);
            obj = oi.readObject();

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(bi!=null){
                    bi.close();
                }
                if(oi!=null){
                    oi.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return obj;
    }
}
然后是生产者发送消息的代码

import com.util.BeanUtil;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

public class Producer{
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "127.0.0.1:9092");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");
        props.put("acks", "all");
        props.put("retries", "1");

        Producer<String, byte[]> producer = new KafkaProducer<String, byte[]>(props);
        for(int i = 0 ; i < 1000;i++){
            Map<String,String> app = new HashMap<String,String>();
            app.put("23333",i+"");
            app.put("77",i+"7");
            producer.send(new ProducerRecord<String, byte[]>("test3",BeanUtil.ObjectToBytes(app)));
            System.out.println("成功发送" + i);
//            try {
//                TimeUnit.SECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
        producer.flush();
        producer.close();
    }
}
稍微对几个参数做一下说明:

bootstrap.servers:建立与kafka集群连接的host/port组,数据将会在所有servers上均衡加载
key.serializer:关键词的序列化方式,这个笔者没用到就直接设置String类型了
value.serializer:传输内容的序列化方式,这里笔者用的是字节数组,读者可以打开kafka-clients-0.11.0.0.jar看看对应目录下有哪几种序列化方式
acks:producer需要server接收到数据之后发出的确认接收的信号
retries:设置大于0的值将使客户端在数据发送失败后重新发送数据

整个代码思路还是比较清晰的,先设置参数,再根据参数构建生产者,最后将要要发送的map数据转换为字节数组发送至Topic test3.

而后是消费者的代码:

import com.google.common.collect.Lists;
import com.util.BeanUtil;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class Consumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", " 127.0.0.1:9092");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer");
        props.setProperty("group.id", "0");
        props.setProperty("enable.auto.commit", "true");
        props.setProperty("auto.offset.reset", "earliest");

        Consumer<String, byte[]> consumer = new KafkaConsumer<String, byte[]>(props);
        consumer.subscribe(Lists.newArrayList("test3"));

        while(true){
            ConsumerRecords<String, byte[]> records = consumer.poll(1);
            if(records.count()>0)
                for (ConsumerRecord<String, byte[]> record : records) {
                    Map<String, String> app = (Map<String, String>) BeanUtil.BytesToObject(record.value());
                    for (Map.Entry<String, String> entry : app.entrySet()) {
                        System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
                    }
                    System.out.println();
                }
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

同样的对参数进行一下说明:

group.id:区分消费者所在组的方式
enable.auto.commit:自动提交位移,若设置为false则需要手动提交
auto.offset.reset:earliest可读旧数据 latest只读新数据 none没有旧数据抛出异常
注意这里的key.serializer和value.serializer和生产者有所不同,后面是Deserializer

同样的根据参数构建消费者,这里要注意的是KafkaConsumer是非线程安全的,在多线程编程中要注意,一般有两种解决方法,一种是在每个线程中构建一个,另一种就是统一构建然后各线程自己去调用。之后消费者订阅了Topic test3,然后就可以去poll数据,这里poll函数中的参数单位是微秒,笔者的理解是在一定的时间内去取数据,如有错误还请指正。最后将获取的数据转换回对象即可。


之后可以根据自己的测试需要建立多个生产者和消费者即可,代码所需jar包都可以在kafka的libs目录下找到,笔者也是尝试了好久才从几十个包中找到了所有关联的包,其实生产者和消费者的代码涉及到了:
guava-20.0.jar
kafka-clients-0.11.0.0.jar 
log4j-1.2.17.jar 
slf4j-api-1.7.25.jar 
slf4j-log4j12-1.7.25.jar
这五个包。而Topic操作的代码则是涉及到了:
jopt-simple-5.0.3.jar 
kafka-log4j-appender-0.11.0.0.jar
kafka_2.11-0.11.0.0.jar
kafka_2.11-0.11.0.0-test-sources.jar
scala-library-2.11.11.jar
scala-parser-combinators_2.11-1.0.4.jar
zkclient-0.10.jar
zookeeper-3.4.10.jar
这八个包,单单一个函数就涉及到8个jar包也是没有想到,有一个还特别难找。
关于kafka的初步学习及应用暂时就到这里了,后续如果还有深入学习还会继续总结的。

  • 5
    点赞
  • 3
    评论
  • 12
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值