producer 的功能就是向某个 topic 的某个分区发送一条消息,所以首先要确定的就是向哪个分区发送,kafka 支持自动分配分区,也支持开发者自己指定分区。
分区是通过 Partitioner(分区器) 实现的,默认的分区器会看消息是否有 key:
Producer首先使用一个线程(用户主线程,也即用户启动Producer的线程)将待发送的消息封装进一个ProducerRecord类实例,然后将其序列化之后发送给partitioner,再结合本地缓存的元数据信息由partitioner来确定目标分区后一同发送到位于producer程序中的一块内存缓冲区中。而KafkaProducer中的另一个专门的sender I/O线程则负责实时地从该缓冲区中提取出准备就绪的消息封装进一个批次(batch),统一发送给对应的broker。
1)构造一个java.util.Properties对象,然后至少指定bootstrap.servers 、key.serializer、value.serializer这三个属性。对于bootstrap.servers参数,若kafka集群中机器数很多,可只需指定部分broker即可,producer会通过该参数找到并发现集群中所有的broker。而kafka接收到的消息都是字节数组,所以对于key和value,必须先进行序列化,使用全限定包名“org.apache.kafka.common.serialization.StringDeserializer“,
2)构造KafkaProducer对象
3)构造待发送的消息对象ProducerRecord,指定消息要被发送到的topic、分区及对应的key和value。注意,分区和key信息可以不用指定,有kafka自行确定分区
4)调用KafkaProducer的send方法发送消息。
5)关闭KafkaProducer。producer程序结束时一定要关闭producer。提供有无参数的close方法和有超时参数close方法。在实际场景中,一定要慎用待超时参数的close方法。关闭原因是producer开了额外线程和socket连接等,必须要将其关闭,避免占用较多资源
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.errors.RetriableException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/*
*/
public class ProducerTest {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");//指定发往哪台broker服务器
props.put("key.serializer", "org.apache.kafka.common.serialization.StringDeserializer");//kafka发送给broker端的数据必须是字节数组
//所以kafafka首先对消息进行序列化,然后才能发送给broker
props.put("value.server", "org.apache.kafka.common.serialization.StringDeserializer");//value和ke一样
props.put("acks", "-1");//用户控制生产消息的持久化,对于producer而言,一旦提交那么只有保存了该消息的副本存活
props.put("retries", 3);
props.put("batch.size", 323840);
props.put("linger.ms", 10);
props.put("buffer.memory", 33554432);
props.put("max.block.ms", 3000);
Producer producer = new KafkaProducer(props);//构建生产者实例】
for (int i = 0; i < 100; i++) {
//消费者实例,//异步发送,返回一个futurn对象供用户稍后处理发送结果,这就是回调机制,recordMetadata和e至少一个为null
ProducerRecord record = new ProducerRecord("my-topic", Integer.toString(i), Integer.toString(i));
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
// if (recordMetadata == null) {
// System.out.println("发送失败!");
// } else {
if (e == null) {
System.out.println("发送成功!");
} else {
if (e instanceof RetriableException) {
//处理可重试瞬时异常
} else {
//处理不可重试异常
}
}
// }
}
});
//同步发送
// ProducerRecord syncrecord=new ProducerRecord("test",Integer.toString(i));
// try {
// producer.send(syncrecord).get(30000, TimeUnit.MILLISECONDS);
//
// }
// catch (InterruptedException e)
// {
//
// }
//第三种发送后不理会发送结果
//producer.send(new ProducerRecord("my-topic",Integer.toString(i),Integer.toString(i)))
producer.close();
}
}
}
以上为异步发送
同步发送代码如下:一直等待结果返回,但是kafka也提供了同步方法的超时时间,这也是在Future对象里面的
producer.send(record).get();//使用Future.get会一直等待直至Kafka broker将发送结果返回给producer程序.
【说明】无论同步发送和异步发送都有可能失败,当前kafka的错误类型包含两类:可重试异常和不可重试异常。所有可重试异常都继承自org.apache.kafka.common.errors.RetriableException抽象类。
acks:指定在给producer发送响应前,leader broker必须要确保已成功写入该消息的副本数。有3个取值:0、1和all。
acks | producer吞吐量 | 消息持久性 | 使用场景 |
0 | 最高 | 最差 | 1、完全不关心消息是否发送成功; 2、允许消息丢失(比如统计服务器日志等) |
1 | 适中 | 适中 | 一般场景即可 |
all或-1 | 最差 | 最高 | 不能容忍消息丢失 |
buffer.memory:指定producer端用于缓存消息的缓冲区大小,单位是字节,默认33554432,即32M,我们几乎可以认为该参数指定的内存大小就是producer程序使用的内存大小。
原理:如同前面所说producer启动时会首先创建一块内存缓冲区,另一个线程会从缓冲区读取消息执行真正的发送,这块缓冲区就由buffer.memory 指定,
compression.type:指定是否压缩消息,默认是none。若要压缩直接指定压缩类型,目前kafka支持3中压缩算法:GZIP、Snappy和LZ4,根据实际使用经验producer结合LZ4的性能
retries:默认为0,两个需要注意:1,重试可能造成消息重复。目前已经解决。2,重试可能造成消息乱序,可以将contention参数设置为1,表示某一时刻只发送一次请求。并且两个重试之间可以设置间隔时间。
batch.size:producer会发往统一分区的多条消息会封装成batch中,当batch满了之后,producer会发送batch中的消息,,不过不都是等batch满了才发送消息,很有可能是还有很多空闲时间时,就发送了。默认值16384也就是16k。
linger.ms:上面说到有时候并不会等到batch满了在发送,这是一个权衡,所以此时引入linger.ms,控制消息的发送延时行为,默认为0,也就是立即发送,如果设置了这个参数会拉低producer吞吐量,因为每次发送的消息数越多越好,这样发送请求的开销瘫倒更多消息上,提高吞吐量。
max.request.size:该参数控制producer发送消息的最大值,默认1048576
request.timeout.ms:异步请求超时时间,默认30s,超时这个时间没有返回结果,则跑出TimeoutException
5,kafka自定义分区策略
1)创建一个类实现Partitioner接口,主要分区逻辑在这个类中。
2)在kafkaproducer中properties对象中加入Partitioner.class
具体代码逻辑:
package com.kafka;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Created by ycco9 on 2019/6/30.
*/
public class AuditPartitioner implements Partitioner {
private Random random;
@Override
public void configure(Map configs){
random=new Random();
}
/*
如果包含key值,如果我们的消息包含一个用于审计功能,
则这类消息的key值会固定的分配一个"audit",我们希望将这类消息放在一个分区,
暂且放在最后一个分区
首先对key进行判断,如果为空或者为普通消息,则放入最后一个分区以外的其他分区,如果为属于audit消息
则放在最后一个分区。
*/
@Override
public int partition(String topic,Object keyObj,byte[] keyBytes,Object value,byte[] valueBytes,Cluster cluster){
String key=keyObj.toString();
List partitionerInfoList=cluster.availablePartitionsForTopic(topic);
int partitionCount =partitionerInfoList.size();
int auditPartition=partitionCount-1;
return key==null||key.isEmpty()||!key.contains("audit")?random.nextInt(partitionCount-1):auditPartition;
}
@Override
public void close(){
//高方法用于清理工作
}
}
Partitioner接口:
package com.kafka;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.Configurable;
/**
* Created by ycco9 on 2019/6/30.
*/
public interface Partitioner extends Configurable, Cloneable {
/*
key:消息键值或null
keyBytes:消息键值序列化字节数组或null
value:消息体或null
valueBytes:消息题序列化字节数组或null
cluster:集群元数据
*/
public int partition(String topic,Object key,byte[] keyBytes,Object value,byte[] valueBytes,Cluster cluster);
public void close();
}
最后添加这段代码进入我们最开始创建的那个producer类
props.put("Partitioner.class","com.kafka.AuditPartitioner");//使用自定义分区策略
//然后加入
String partTopic="test-topic";
//使用分区
ProducerRecord nonKeyRecord=new ProducerRecord(partTopic,"non-key record");
ProducerRecord auditRecord=new ProducerRecord(partTopic,"audit","audit record");
ProducerRecord nonAuditRecord=new ProducerRecord(partTopic,"other","non-audit record");
producer.send(nonKeyRecord).get();
producer.send(nonAuditRecord).get();
producer.send(auditRecord).get();
producer.send(nonKeyRecord).get();
producer.send(nonAuditRecord).get();
以上代码逻辑解释见注释
5,自定义序列化
1)kafka提供了一些序列化器
由shang可见,以上序列化器不能满足具体场景,所以需要自定义序列化器
2)
6,单线程和多线程的kafka实例
1)多线程的单个kafka实例:也就是多个用户共享一个kafkaproducer变量,kafkaproducer是线程安全,所以这种方式是线程安全的。
2)多线程多个kafka实例:在每个producer中都构造一个kafkaproducer实例,该实例在该线程封闭(线程封闭是实现线程安全的重要手段)
对于分区不多的来说采用第一种方式除非,超分区集群,可以采用第二种方式提高可靠性。
7,kafka-consumer开发
1)消费者使用一个消费者组名,topic每一条消息都只会发送到每个订阅他的消费组的一个实例
应用:kafka支持两种消费引擎模型:基于队列和发布订阅都是通过kafka组实现的
所有consumer都属于同一个group--基于队列模式,每条消息只会被一个consumer实例处理
所有consumer都属于不同的group--基于发布/订阅,极端情况是每个consumer实例都会被设置成完全不同的group,这样kafka消息会被广播到所有的实例上。
作用:下挂可用
2)消费组的重平衡
3)offset:当前最新消费消息的位置,本次的位移就是下次消费的位置,比如kafka已经消费了n条数据,此时位移为n,但是kafka是从0开始消费,则此时消费了n+1条数据,如此时kafka崩了,则她会选择n+1条开始消费,则此时会出现重复消费了n+1条,所以有三种方案: