一、生产者
1. 创建kafka生产者
1.1 java生产者客户端的构造方法
可以看到有多个构造方法,传入map或者properties,还有要求传入serializer的。从源码中可以得知传入map或者properties都是一样的,使用key、value的形式传递配置参数。
必要的三个配置为:
// 连接地址
bootstrap.servers
// key的序列化器
key.serializer
// value的序列化器
value.serializer
其中key和value的序列化器配置可以在map中指定,也可以使用:
KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer)
这个构造函数来传入即可。
1.2 最简单的发送消息的小示例
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 存放配置信息
Map<String,Object> configMap = new HashMap<>();
// kafka服务连接地址
configMap.put("bootstrap.servers","127.0.0.1:9092");
// key和value的序列化器
StringSerializer serializer = new StringSerializer();
// 创建生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(configMap,serializer,serializer);
// 指定主题(如果该主题不存在,kafka会自动创建)
String topicName = "SimpTopicTest";
// 封装消息
ProducerRecord<String,String> record = new ProducerRecord<>(topicName,"我是消息888000");
// 使用生产者对象发送封装好的消息(异步的)
Future<RecordMetadata> future = kafkaProducer.send(record);
// 获取发送结果(偏移量、时间戳等信息)
RecordMetadata metadata = future.get();
}
2. 使用kafka生产者
2.1 send方法说明
- 同步发送消息
RecordMetadata recordMetadata = kafkaProducer.send(record).get();
send方法会返回一个future对象,调用future对象的get方法,阻塞等待返回发送结果。
- 异步发送消息
// 发送消息的时候同时传入一个回调函数
kafkaProducer.send(record, new Callback(){
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
// 这里可以得到发送结果,和异常信息
}
});
2.2 ProducerRecord封装消息的方法说明
可以看到有多种参数组合来封装消息,最简单就两个参数,分别指定topic(主题)和消息体即可。
我们来看一个参数最多的构造函数:
/**
topic:指定主题
partition:指定分区
timestamp:指定时间戳
key:消息key
value:消息值
headers:迭代器
*/
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers)
3. 生产者对象的配置
-
acks
该参数指定了必须要有多少个分区副本收到消息,生产者才认为消息写入是成功的。
acks=0:生产者不等待服务器的响应,不关心消息是否发送成功,消息可能会丢失。因为生产者不需要等待服务器的响应,所以它可以支持最大速度的发送消息。
acks=1:只要集群的主节点收到消息,生产者就会收到发送成功的响应。如果主节点挂了,生产者会收到一个错误响应,生产者会重发消息。但是,如果是没有收到消息的节点成为了主节点,消息还是会丢失。
acks=all:只有当集群内所有有副本的节点都收到消息时,生产者才会收到成功响应。这个模式是最安全的,但是延迟也是最高的。 -
buffer.memory
该参数用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。
如果发送消息的速度过快,导致缓冲区的空间不足,那么send()方法的调用要么被阻塞,要么抛出异常。
这个根据阻塞时间配置来决定。 -
comperession.type
指定发送消息时的数据压缩方式,默认为不压缩。该参数可以设置为snappy、gzip或lz4等。
snappy压缩算法:占用cpu较少,性能和压缩比还不错,适合要求性能和网络带宽的情况。
gzip压缩算法:占用cpu较多,但是压缩比较高,适合带宽有限的情况。 -
retries
该参数指定生产者可以重发消息的次数。默认情况下每次重试之间间隔100ms,可以通过retry.backoff.ms参数来设置间隔时间。 -
batch.size
当有多个消息需要被发送到同一个分区时,生产者会把他们放在同一个批次里,该参数用于指定一个批次可以使用的内存大小(按字节数计算)。当批次满了之后,消息会被一次发送出去。不过生产者并不一定要等待批次满了之后才发送消息。 -
client.id
可以时任意字符串,服务器会用它来识别消息来源。 -
max.in.flight.requests.per.connection
该参数指定了生产者在收到服务器响应之前可以发送多少个消息。值越高占用的内存越大,同时也会提升吞吐量。把它设为1可以保证消息是按照发送的顺序写入服务器的(即使发送了重试)。
顺序保证:可以把值设为1,这样在生产者发送第一批消息时,就不会有其他的消息发送给broker,虽然会影响生产者的吞吐量,但是可以对消息的顺序有保障。 -
timeout.ms
指定了broker等待同步副本返回消息的确认时间,与asks配置相匹配,如果在指定时间内没有收到同步副本的确认,那么broker就会返回错误。 -
request.timeout.ms
指定了发送数据时等待服务器返回响应的时间。 -
metadata.fetch,timeout.ms
指定了生产者在获取元数据时等待服务器返回响应的时间。 -
max.block.ms
该参数指定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间。
当生产者的发送缓冲区已满,或者没有可用元数据时,这些方法就会阻塞。在阻塞时间达到max.block.ms时,生产者会抛出超时异常。 -
max.request.size
用于控制生产者发送的请求大小。可以指定能发送单个消息的最大值,也可以指定单个请求里所有消息的总大小。broker对可接收消息的最大值也有自己的限制(message.max.bytes),所以两边的配置最好一致。 -
receive.buffer.bytes和send.buffer.bytes
这两个参数分别指定了TCP socket接收和发送数据包的缓冲区大小。如果它们被设置为-1,则使用操作系统的默认值。如果生产者或消费者与broker处于不同的数据中心,那么可以适当增大这些值,因为宽网络一般都有比较高的延迟。
4. 分区
4.1 默认的分区器
如果key为null,分区器使用轮询算法将消息均衡的分布到各个分区上。
如果key不为null,kafka会会key进行散列算法计算出分区,相同key的消息会被发送到同一个分区。
注意:如果新增了分区数量,那么无法保证之前的key的消息还会发送到以前的那个分区上了。
4.2 自定义分区器
- 创建
创建一个类实现Partitioner接口:
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class ZdyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 根据key或者value在指定分区逻辑
// 最终返回某个分区的数值
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
- 使用
在配置文件中指定自定义的分区器的全类名:
configMap.put(“partitioner.class”,“xxx.xx.xxx.ZdyPartitioner”);
// 存放配置信息
Map<String,Object> configMap = new HashMap<>();
// kafka服务连接地址
configMap.put("bootstrap.servers","127.0.0.1:9092");
// 指定自定义分区器
configMap.put("partitioner.class","xxx.xx.xxx.ZdyPartitioner");
// key和value的序列化器
StringSerializer serializer = new StringSerializer();
// 创建生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(configMap,serializer,serializer);
二、消费者
1. 创建kafka消费者
1.1 java消费者客户端的构造方法
必要的四个参数配置为:
// 连接地址
bootstrap.servers
// 消费组id
group.id
// key的反序列化器
key.deserializer
// value的反序列化器
value.deserializer
其中key和value的反序列化器配置可以在map中指定,也可以使用:
public KafkaConsumer(Map<String, Object> configs, Deserializer<K> keyDeserializer, Deserializer<V> valueDeserializer)
这个构造函数来传入即可。
1.2 消费者简单使用示例
public static void main(String[] args) {
// 存放配置信息
Map<String,Object> configMap = new HashMap<>();
// kafka服务连接地址
configMap.put("bootstrap.servers","127.0.0.1:9092");
// 需要有一个消费组id
configMap.put("group.id","salulu-group-test1");
// key和value的反序列化器
StringDeserializer deserializer = new StringDeserializer();
// 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(configMap, deserializer, deserializer);
// 封装要订阅的topic名称,可以同时订阅多个主题,放在一个list集合中
List<String> subTopicList = new ArrayList<>();
subTopicList.add("SimpTopicTest");
// 使用消费者对象订阅这些主题
kafkaConsumer.subscribe(subTopicList);
// 不断的轮询获取主题中的消息
while (true){
// poll() 获取消息列表,可以传入超时时间
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
String topic = record.topic();
int partition = record.partition();
long offset = record.offset();
String key = record.key();
String value = record.value();
String formatStr = "[接收到数据] topic:%s, partition:%s, offset:%s, key:%s, value:%s";
String msg = String.format(formatStr, topic, partition, offset, key, value);
System.out.println(msg);
}
}
// 最终应该关闭kafkaConsumer,这里没有关 kafkaConsumer.close();
}
启动生产者发送数据,运行结果:
[接收到数据] topic:SimpTopicTest, partition:0, offset:4, key:null, value:我是消息888000
[接收到数据] topic:SimpTopicTest, partition:0, offset:5, key:null, value:我是消息9999
2. 消费者配置
-
fetch.min.bytes
指令消费者从服务器获取消息的最小字节数。broker在收到消费者的消息请求时,如果现存可用的数据量小于fetch.min.bytes的大小,那么它会等到有足够可用的数据的时候才会返回数据给消费者。好处是如果消息不是很多的情况下,可以修改配置来降低消费者和broker的工作负载。 -
fetch.max.wait.ms
和fetch.min.bytes配合使用,fetch.min.bytes指定数据大小,fetch.max.wait.ms指定broker的等待时间。默认是500ms,如果超过500ms都还是没有足够多的可用数据,那么消息也是会返回的。两个条件任意满足其中一个消息都会返回给消费者。 -
max.partition.fetch.bytes
指定了服务器从每个分区里返回给消费者的最大字节数。默认值是1MB。即kafkaConsumer.poll()方法从每个分区里返回的记录最多不超过max.partition.fetch.bytes指定的大小。即如果某个主题有10个分区,消费这个主题的消费者有2个,那么消费者在获取消息的时候,最多会获取到5MB大小的消息数据。 -
session.timeout.ms
消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3秒。如果消费者在指定时间内没有发送心跳,则会被认为该消费者已经死亡,协调器就会触发再均衡,把它的分区分配给组里其他活着的消费者。heartbeat.interval.ms指定了poll方法像协调器发送心跳的频率,所以一般需要同时修改这两个属性。heartbeat.interval.ms的值一般设置为session.timeout.ms的三分之一。 -
auto.offset.reset
指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作如何处理。默认值是latest,即消费者将从最新的记录开始读取数据(在消费者启动之后的消息)。另一个值是earliest,在偏移量无效的时候消费者将从起始位置读取分区记录。 -
enable.auto.commit
指定了消费者是否自动提交偏移量,默认值是true(可以通过auto.commit.interval.ms来控制提交频率)。为了尽量避免出现重复数据和数据丢失,可以把它设为false,有自己控制何时提交偏移量。 -
partition.assignment.strategy
分区分配给消费者的分配策略:
Range:把主题的多个连续分区分配给消费者。如果一个消费者订阅了多个主题,那么是按单个主题平均分配分区给消费者。
RoundRobin:把所有主题的所有分区逐个分配给消费者。如果一个消费者订阅了多个主题,那么是按所有主题的总分区数平均分配给消费者。 -
client.id
broker用来标识从客户端发来的信息。 -
max.poll.records
用于控制单次调用call()方法能够返回的记录数量。 -
receive.buffer.bytes和send.buffer.bytes
socket在读写数据时用到的TCP缓冲区。如果设为-1,则使用操作系统的默认值。如果网络延迟较高或者带宽较低,则可以适当增大这两个值。
3. 偏移量
3.1 偏移量
用来记录消费者读取到分区里消息的位置。
_consumer_offset 这个主题中,就是记录每个分区的偏移量。
当消费者需要继续消费数据的时候,可以读取偏移量就知道之前消费到哪里了。
当消费组中有新增或减少消费者的时候,会触发再均衡,此时也会用到偏移量来继续消费。
3.2 自动提交偏移量
当enable.auto.commit的值为true时,每隔一段时间消费者会自动把消费到的最大偏移量的值提交,提交的时间间隔有auto.commit.interval.ms配置,默认值为5秒。
3.3 手动提交偏移量
当enable.auto.commit的值为false时,可以使用commitSync()方法来提交偏移量,它会提交有poll()方法返回的最新偏移量。
- 同步提交
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
// 处理消息
}
// 提交偏移量(同步)
kafkaConsumer.commitSync();
- 异步提交
// 提交偏移量(异步)
kafkaConsumer.commitAsync();
- 组合提交
由于同步提交方法会一直尝试提交知道成功为止(慢),而异步提交不会进行重试(快)。
如果又要快,又要让偏移量能提交成功,可以使用组合提交的方式。比如说在直接关闭消费者的时候:
try{
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
// 处理records的逻辑....
// 异步提交
kafkaConsumer.commitAsync();
}catch (Exception e){
e.printStackTrace();
}finally {
try{
kafkaConsumer.commitSync();
}finally {
kafkaConsumer.close();
}
}
- 提交指定的偏移量
同步:commitSync(Map<TopicPartition, OffsetAndMetadata> offsets)
异步:commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)
将分区和偏移量封装在map中提交。
// 提交指定偏移量到指定分区
// 指定分区
TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition());
// 指定偏移量
OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset()+1,null);
// 封装为map
Map<TopicPartition,OffsetAndMetadata> offsetMap = new HashMap<>();
offsetMap.put(topicPartition,offsetAndMetadata);
// 提交是传入数据
kafkaConsumer.commitSync(offsetMap);
3.4 再均衡监听器
在消费者分配新分区或移除旧分区时,会触发在均衡。
如果需要接收到这个通知并处理,可以在调用subscribe()方法的时候传入一个ConsumerRebalanceListener实例即可:
订阅方法:
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener)
监听器:
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.common.TopicPartition;
import java.util.Collection;
public class MyConsumerRebalanceListener implements ConsumerRebalanceListener {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> collection) {
// 如果发生在均衡,则在即将失去分区时,提交偏移量
// 一般来说是提交所有分区获取到消息的最后一个偏移量
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> collection) {
// 如果是新加入了一个分区来消费,则不需要做什么
}
}
订阅时传入监听器:
kafkaConsumer.subscribe(subTopicList,new MyConsumerRebalanceListener());
3.5 从指定偏移量处进行消费
使用seek方法来指定偏移量的消费数据。
4 优雅的停止消费
消费者在订阅了主题进行消费的时候,是通过轮询的方式进行的,如下:
// 使用消费者对象订阅这些主题
kafkaConsumer.subscribe(subTopicList);
// 不断的轮询获取主题中的消息
try{
while (true){
// poll() 获取消息列表,可以传入超时时间
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
// 处理消息的逻辑
}
}
}catch (WakeupException e){
// 不用处理这个异常,它只是用来停止循环的
}finally {
kafkaConsumer.close();
}
可以在其他线程中调用消费者的wakeup()方法来退出循环,调用wakeup()方法可以退出poll(),并抛出WakeupException异常,如果此时没有等待轮询,那么异常会在下一轮调用poll()方法时抛出。
例如,是关闭程序时停止:
Runtime.getRuntime().addShutdownHook(new Thread(()->{
kafkaConsumer.wakeup();
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));