Kafka基础及java客户端使用

Kafka简介

由Scala和Java编写,Kafka是一种高吞吐量的分布式发布订阅消息系统.

环境介绍

操作系统:centos6.5
kafka:1.0.1
zookeeper:3.4.6

术语介绍

  • Broker : Kafka集群包含一个或多个服务器,这种服务器被称为broker
  • Topic : 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition : Partition是物理上的概念,每个Topic包含一个或多个Partition.
  • Producer : 负责发布消息到Kafka broker
  • Consumer : 消息消费者,向Kafka broker读取消息的客户端。
  • Consumer Group : 每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

消费模式

这里写图片描述

为了照顾对MQ不是很了解的同学,先讲一下MQ的原理.一般MQ都是在服务端存储一个队列.生产者把消息丢到MQ server,消费者从MQ server消费.这样一来解决了生产者和消费者的高耦合问题,同时也解决了生产速度和消费速度差异导致的消费者跟不上生产者的生产速度而导致的消费者压力过大问题.

在kafka中的topic就是一系列队列的总称,称为一个主题.当然ActiveMQ和RabbitMQ中都有这个概念.一类消息都会丢到一个topic中去.

讲完topic我们讲一下partition(分区),这个东西是kafka独有的东西,也是kafka实现横向扩展和高并发的一个重要设计.我们试想一下,如果每个topic只有一个队列,随着业务增加topic里消息越来越多.多到一台server装不下了怎么办.为了解决这个问题,我们引入了partition这个概念.一个partition(分区)代表了一个物理上存在的队列.topic只是一组partition(分区)的总称,也就是说topic仅是逻辑上的概念.这样一来当topic上的消息越来越多.我们就可以将新增的partition(分区)放在其他server上.也就是说topic里边的partition(分区)可以分属于不同的机器.实际生产中,也基本都是这样玩的.

这里说一个特殊情况,有时我们创建了一个topic没有指定partition(分区)数量或者指定了partition(分区)数量为1,这时实际也是有一个默认的partition(分区)的,名字我忘记了.

从Producer(生产者)角度,一个消息丢到topic中任务就完成了.至于具体丢到了topic中的哪个partition(分区),Producer(生产者)不需要关注.这里kafka自动帮助我们做了负载均衡.当然如果我们指定某个partition(分区)也是可以的.这个大家官方文档和百度.

接下里我们讲Consumer Group(消费组),Consumer Group(消费组)顾名思义就是一组Consumer(消费者)的总称.那有了组的概念以后能起到什么作用.如果只有一组内且组内只有一个Consumer,那这个就是传统的点对点模式,如果有多组,每组内都有一个Consumer,那这个就是发布-订阅(pub-sub)模式.每组都会收到同样的消息.

最后讲最难理解也是大家讨论最多的地方,partition(分区)和Consumer(消费者)的关系.首先,一个Consumer(消费者)的一个线程在某个时刻只能接收一个partition(分区)的数据,一个partition(分区)某个时刻也只会把消息发给一个Consumer(消费者).我们设计出来几种场景:

场景一: topic-1 下有partition-1和partition-2
group-1 下有consumer-1和consumer-2和consumer-3
所有consumer只有一个线程,且都消费topic-1的消息.
消费情况 : consumer-1只消费partition-1的数据
consumer-2只消费partition-2的数据
consumer-3不会消费到任何数据
原因 : 只能接受一个partition(分区)的数据

场景二: topic-1 下有partition-1和partition-2
group-1 下有consumer-1
consumer只有一个线程,且消费topic-1的消息.
消费情况 : consumer-1先消费partition-1的数据
consumer-1消费完partition-1数据后开始消费partition-2的数据
原因 : 这里是kafka检测到当前consumer-1消费完partition-1处于空闲状态,自动帮我做了负载.所以大家看到这里在看一下上边那句话的”某个时刻”
特例: consumer在消费消息时必须指定topic,可以不指定partition,场景二的情况就是发生在不指定partition的情况下,如果consumer-1指定了partition-1,那么consumer-1消费完partition-1后哪怕处于空闲状态了也是不会消费partition-2的消息的.

进而我们总结出了一条经验,同组内的消费者(单线程消费)数量不应多于topic下的partition(分区)数量,不然就会出有消费者空闲的状态,此时并发线程数=partition(分区)数量.反之消费者数量少于topic下的partition(分区)数量也是不理想的,原因是此时并发线程数=消费者数量,并不能完全发挥kafka并发效率.

最后我们看下上边的图,Consumer Group A的两个机器分别开启两个线程消费P0 P1 P2 P3的消息Consumer Group B的四台机器单线程消费P0 P1 P2 P3的消息就可以了.此时效率最高.

Kafka安装

参考上一篇文章:Kafka安装及运行

JAVA实战

pom.xml

		
            org.apache.kafka
            kafka_2.12
            2.2.0
        
        
            org.apache.kafka
            kafka-clients
            2.2.0
        

kafka客户端配置文件(MQDict):

package com.example.demo.MQUtil;

import java.time.Duration;

/**
 * @author created 旨酒思柔
 * @date 2019/3/27
 */
public class MQDict {
    public static final String MQ_ADDRESS_COLLECTION = "127.0.0.1:9092";			//kafka地址
    public static final String CONSUMER_TOPIC = "topicDemo";						//消费者连接的topic
    public static final String PRODUCER_TOPIC = "topicDemo";						//生产者连接的topic
    public static final String CONSUMER_GROUP_ID = "1";								//groupId,可以分开配置
    public static final String CONSUMER_ENABLE_AUTO_COMMIT = "true";				//是否自动提交(消费者)
    public static final String CONSUMER_AUTO_COMMIT_INTERVAL_MS = "1000";			
    public static final String CONSUMER_SESSION_TIMEOUT_MS = "30000";				//连接超时时间
    public static final int CONSUMER_MAX_POLL_RECORDS = 10;							//每次拉取数
    public static final Duration CONSUMER_POLL_TIME_OUT = Duration.ofMillis(3000);	//拉去数据超时时间

}

Producer:

package com.example.demo.MQUtil;

import kafka.utils.json.JsonObject;
import net.sf.json.JSONObject;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.log4j.Logger;

import java.util.Arrays;
import java.util.Properties;

/**
 * @author created by 旨酒思柔
 * @date 2019/3/27
 */
public class Producer {
    static Logger log = Logger.getLogger(Producer.class);

    private static KafkaProducer producer = null;

    /*
    初始化生产者
     */
    static {
        Properties configs = initConfig();
        producer = new KafkaProducer(configs);
    }

    /*
    初始化配置
     */
    private static Properties initConfig(){
        Properties props = new Properties();
        props.put("bootstrap.servers", MQDict.MQ_ADDRESS_COLLECTION);
        props.put("acks", "all");
        props.put("retries", 0);
        props.put("batch.size", 16384);
        props.put("key.serializer", StringSerializer.class.getName());
        props.put("value.serializer", StringSerializer.class.getName());
        return props;
    }


    public static void main(String[] args) throws InterruptedException {
        //消息实体
        ProducerRecord record = null;
        for (int i = 0; i < 100; i++) {
            record = new ProducerRecord(MQDict.PRODUCER_TOPIC, "value"+i);
            //发送消息
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (null != e){
                        log.info("send error" + e.getMessage());
                    }else {
                        System.out.println(String.format("offset:%s,partition:%s",recordMetadata.offset(),recordMetadata.partition()));
                    }
                }
            });
        }
        producer.close();
    }
}

Consumer:

package com.example.demo.MQUtil;

import net.sf.json.JSONObject;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;

/**
 * @author created by 旨酒思柔
 * @date 2019/3/27
 */
public class Consumer {
    static Logger log = Logger.getLogger(Consumer.class);

    private static KafkaConsumer consumer;

	/**
	*  初始化消费者
	*/
    static {
        Properties configs = initConfig();
        consumer = new KafkaConsumer(configs);
        consumer.subscribe(Arrays.asList(MQDict.CONSUMER_TOPIC));
    }
	/**
	*  初始化配置
	*/
    private static Properties initConfig(){
        Properties props = new Properties();
        props.put("bootstrap.servers", MQDict.MQ_ADDRESS_COLLECTION);
        props.put("group.id", MQDict.CONSUMER_GROUP_ID);
        props.put("enable.auto.commit", MQDict.CONSUMER_ENABLE_AUTO_COMMIT);
        props.put("auto.commit.interval.ms", MQDict.CONSUMER_AUTO_COMMIT_INTERVAL_MS);
        props.put("session.timeout.ms", MQDict.CONSUMER_SESSION_TIMEOUT_MS);
        props.put("max.poll.records", MQDict.CONSUMER_MAX_POLL_RECORDS);
        props.put("auto.offset.reset", "earliest");
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        return props;
    }
    
    public static void main(String[] args) {
        Logger.getLogger("org").setLevel(Level.ERROR);

        while (true) {

            ConsumerRecords records = consumer.poll(MQDict.CONSUMER_POLL_TIME_OUT);
            records.forEach((ConsumerRecord record)->{
                    log.info("revice: key ==="+record.key()+" value ===="+record.value()+" topic ==="+record.topic());
            });
        }
    }

}

参考资料:Kafka : Kafka入门教程和JAVA客户端使用

你可能感兴趣的:(kafka,java,消息队列,kafka,java,消息队列)