title: kafka学习
date: 2022-01-24 17:20:22
tags: [‘MQ’,‘概念’]
categories: [框架,MQ,kafka]
kafka介绍
Kafka传统定义: Kafka是一个分布式的基于发布/订阅模式的消息队列(MessageQueue),主要应用于大数据实时处理领域。发布/订阅:消息的发布者不会将消息直接发送给特定的订阅者,而是将发布的消息分为不同的类别,订阅者只接收感兴趣的消息。
Kafka最新定义: Kafka是一个开源的分布式事件流平台(Event StreamingPlatform),被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。
kafka可以在廉价的商用服务器中也能做到每秒100K条的数据传输。支持kafka server的消息分区,及分布式消费。保证每个分区的消息顺序传输。支持离线数据和实时数据的处理。支持在线水平拓展。且kafka是一种发布订阅模式。kafka只有消息的拉取没有推送,通过轮询实现消息的推送。
基础架构
producer
生产者,生产消息然后发布到kafka的topic中,broker接收到消息之后,将消息追加到当前主题+分区的segment文件中。
一般情况下,一个消息会被发布到一个特定的主题上
- 生产者指定分区的话,则消息发送到特定分区
- 如生产者未指定分区,则按照分区策略发送到对应的分区
- 生产者如果有自定义的分区器,则按照自定义分区器的规则将消息映射到分区
- 没有指定分区器,则使用默认的。
- 未指定分区,存在key的情况下,会根据key的hash取模当前主题所有的分区,然后发送到分区中
- key不存在的情况下,会随机分配一个分区。这个随机是根据参数
metadata.max.age.ms
的时间范围内随机选择一个。对于这个时间段内,如果key为 null,则只会发送到唯一的分区。这个值默认情况下是10分钟更新一次。
消息和批次
kafka的数据单元称为消息,可以把消息看做为数据库里面的一条记录,消息由字节数组组成。为了提高效率,kafka会将消息分批写入broker。批次就是一组消息,这些消息属于同一个主题和分区
topic
每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。物理上不同Topic的消息分开存储。逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处
partition
- 主题可以被分为若干个分区,一个分区就是一个提交日志。
- 消息以追加的方式写入分区,然后以先入先出的顺序读取。
- 无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。
- Kafka 通过分区来实现数据冗余和伸缩性。
- 在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
Segment
partition物理上由多个segment组成,每个Segment存着message信息。
broker
和AMQP里协议的概念一样, 就是消息中间件所在的服务器;Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。每个集群都有一个broker充当leader角色。从集群的活跃成员中选举出来。
Replicas
Kafka使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在broker 上,每个broker 可以保存成百上千个属于不同主题和分区的副本。
副本分为leader和follower。为了保证一致性,所有生产者请求和消费者请求都会经过leader这个副本。
follower副本任务就是从leader复制消息,保持和leader一致的状态。当leader崩溃之后,其中一个副本会被提升为新的leader。
AR
分区中所有的副本统称为AR。AR=ISR+OSR
ISR
所有与leadder副本保持一定程度同步的副本(包括leader)组成ISR(In-Sync Replicas)。ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进行同步。同步期间follower会一定程度的滞后,这个可配置。
OSR
与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas)。在正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR=ISR,OSR集合为空。
HW
HW是High Watermak的缩写, 俗称高水位,它表示了一个特定消息的偏移量(offset),消费者只能拉取到这个offset之前的消息。
LEO
LEO是Log End Offset的缩写,它表示了当前日志文件中下一条待写入消息的offset。
Consumer
消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区分已经读取过的消息。偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka 会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。
消费者是消费组的一部分。群组保证每个分区只能被一个消费者使用。 如果一个消费者失效,消费组里的其他消费者可以接管失效消费者的工作,再平衡,分区重新分配。
再平衡
Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段
offset
是kafka用来确定消息是否被消费过的标识,在kafka内部体现就是一个递增的数字 。
kafka整体调优
提高生吞吐量
提高生产者吞吐量
buffer.memory
:发送消息的缓冲区大小,默认值是 32m,可以增加到 64m。batch.size
:默认是 16k。如果 batch 设置太小,会导致频繁网络请求,吞吐量下降;如果 batch 太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时。linger.ms
:这个值默认是 0,意思就是消息必须立即被发送。一般设置一个 5-100毫秒。如果 linger.ms 设置的太小,会导致频繁网络请求,吞吐量下降;如果linger.ms
太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时。compression.type
:默认是 none,不压缩,但是也可以使用 lz4 压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大 producer 端的 CPU 开销。- 增加分区
提高消费者吞吐量
- 调整 fetch.max.bytes 大小,默认是 50m。
- 调整 max.poll.records 大小,默认是 500 条。
- 保证消费者的处理能力。
保证数据的精准一次
生产者
- acks 设置为-1 (acks=-1)。
- 幂等性(enable.idempotence = true) + 事务 。
broker
- 分区副本大于等于 2 (–replication-factor 2)。
- ISR 里应答的最小副本数量大于等于 2 (min.insync.replicas = 2)。
消费者
- 事务 + 手动提交 offset (enable.auto.commit = false)。
- 消费者输出的目的地必须支持事务(MySQL、Kafka)。
保证数据的零丢失
生产者
- 不要使用producer.send(msg) ,而要使用producer.send(msg, callback)。记住,一定要使用带有回调通知的send方法。
- 设置acks= all(-1)。 acks 是Producer的一个参数,代表了你对“已提交”消息的定义。如果设置成all ,则表明所有副本Broker都要接收到消息,该消息才算是”已提交”。这是最高等级的“已提交”定义。
- 设置retries为-个较大的值。这里的retries同样是Producer的参数,对应前面提到的Producer自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了retries > 0的Producer能够自动重试消息发送,避兔消息失。
broker
- 设置unclean.leader.election.enable = false。它控制的是哪些Broker有资格竞选分区的Leader。如果一个Broker落后原先的Leader太多, 那么它一旦成为新的Leader,必然会造成消息的丢失。故一般都要将该参数设置成false ,即不允许这种情况的发生,
- 设置replication.factor >= 3。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
- 设置min.insync.replicas > 1。控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于1可以提升消息持久性。在实际环境中千万不要使用默认值1。
- 确保replication.factor > min.insync.replicas。 如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础.上完成。推荐设置成replication.factor = min.insync.replicas + 1。
消费者
- 确保消息消费完成再提交。Consumer 端有个参数enable.auto.commit ,最好把它设置成false ,并采用手动提交位移的方式。就像前面说的,这对于单Consumer多线程处理的场景而言是至关重要的。
合理的设置分区数
- 创建一个只有 1 个分区的 topic。
- 测试这个 topic 的 producer 吞吐量和 consumer 吞吐量。
- 假设他们的值分别是 Tp 和 Tc,单位可以是 MB/s。
- 然后假设总的目标吞吐量是 Tt,那么分区数 = Tt / min(Tp,Tc)。
例如:producer 吞吐量 = 20m/s;consumer 吞吐量 = 50m/s,期望吞吐量 100m/s;分区数 = 100 / 20 = 5 分区。分区数一般设置为:3-10 个。分区数不是越多越好,也不是越少越好,需要搭建完集群,进行压测,再灵活调整分区个数。
单条日志大于1M
kafka默认处理1M的数据,超过1M的话,可能会出现卡死等现象。
message.max.bytes
默认 1m,broker 端接收每个批次消息最大值。max.request.size
默认 1m,生产者发往 broker 每个请求消息最大值。针对 topic级别设置消息体的大小。replica.fetch.max.bytes
默认 1m,副本同步数据,每个批次消息最大值。fetch.max.bytes
默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受message.max.bytes
(broker config)ormax.message.bytes
(topic config)影响。
javaAPI单机应用
引入maven坐标
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.8.6</version>
</dependency>
同步发送消息
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
Map<String, Object> configs = new HashMap<>();
// 设置连接Kafka的初始连接用到的服务器地址,如果是集群,则可以通过此初始连接发现集群中的其他broker
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
// 设置key的序列化器
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
// 设置value的序列化器
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
/**
* acks=0 :生产者在成功写入消息之前是不会等待任何的来自服务器的响应。也就是说如果当中出现了错误,导致broker没有收到消息,那么生产者是无从得知的
* acks=1:只要集群的首领节点收到消息,生产者就会收到来自服务器成功的响应。若果消息不能够被首领节点接收(比如说首领节点崩溃,而新的首领尚未选出来),这时候生产者会收到一个错误响应
* acks=all / -1: 只有在集群所有的跟随副本都接收到消息后,生产者才会受到一个来自服务器的成功响应。这种模式是最安全的
*/
configs.put("acks", "1");
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(configs);
// 发送消息,同步等待消息的确认 主题名称,分区编号,现在只有一个分区,所以是0,数字作为key,字符串作为value
RecordMetadata recordMetadata = producer.send(new ProducerRecord<Integer, String>("topic_1", 0, 0, "SyncProducer")).get(3_000, TimeUnit.MILLISECONDS);
System.out.println(JSON.toJSONString(recordMetadata));
//关闭生产者
producer.close();
}
异步发送消息
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
Map<String, Object> configs = new HashMap<>();
// 设置连接Kafka的初始连接用到的服务器地址,如果是集群,则可以通过此初始连接发现集群中的其他broker
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
// 设置key的序列化器
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
// 设置value的序列化器
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configs.put(ProducerConfig.ACKS_CONFIG, "1");
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(configs);
//使用回调异步等待消息的确认 , 主题名称,分区编号,现在只有一个分区,所以是0,数字作为key,字符串作为value,
producer.send(new ProducerRecord<Integer, String>("topic_1", 0, 0, "AsyncProducer"), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception e) {
if (e == null) {
System.out.println("主题:" + metadata.topic());
System.out.println("分区:" + metadata.partition());
System.out.println("偏移量:" + metadata.offset());
System.out.println("序列化的key字节:" + metadata.serializedKeySize());
System.out.println("序列化的value字节:" + metadata.serializedValueSize());
System.out.println("时间戳:" + metadata.timestamp());
} else {
e.printStackTrace();
}
}
});
//关闭生产者
producer.close();
}
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。**注意:**消息发送失败会自动重试,不需要我们在回调函数中手动重试。
消费者消费消息
public static void main(String[] args) {
Map<String, Object> configs = new HashMap<>();
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
//指定消费组id,用于标识该消费者所属的消费组
configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer.demo");
//创建消费者对象
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<Integer, String>(configs);
final List<String> topics = Arrays.asList("topic_1");
consumer.subscribe(topics);
//订阅主题对应的分区
// ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
// topicPartitions.add(new TopicPartition("topic_1",0));
// consumer.assign(topicPartitions);
while (true) {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<Integer, String> record : records) {
System.out.println("========================================");
System.out.println("消息头字段:" + Arrays.toString(record.headers().toArray()));
System.out.println("消息的key:" + record.key());
System.out.println("消息的偏移量:" + record.offset());
System.out.println("消息的分区号:" + record.partition());
System.out.println("消息的序列化key字节数:" + record.serializedKeySize());
System.out.println("消息的序列化value字节数:" + record.serializedValueSize());
System.out.println("消息的时间戳:" + record.timestamp());
System.out.println("消息的时间戳类型:" + record.timestampType());
System.out.println("消息的主题:" + record.topic());
System.out.println("消息的值:" + record.value());
}
}
// 关闭消费者 consumer.close();
}
消息拦截器
消息经过发送会经过拦截器-》序列化器-》分区器。
拦截器一般不怎么需要使用,可以处理一些个性化操作。
消费者也有消息拦截器。
自定义拦截器
public class InterceptorOne<KEY, VALUE> implements ProducerInterceptor<KEY, VALUE> {
@Override
public ProducerRecord<KEY, VALUE> onSend(ProducerRecord<KEY, VALUE> record) {
System.out.println("拦截器1---go");
// 此处根据业务需要对相关的数据作修改
String topic = record.topic();
Integer partition = record.partition();
Long timestamp = record.timestamp();
KEY key = record.key();
VALUE value = record.value();
Headers headers = record.headers();
// 添加消息头
headers.add("interceptor", "interceptorOne".getBytes());
return new ProducerRecord<KEY, VALUE>(topic, partition, timestamp, key, value, headers);
}
/**
*该方法会在消息被应答之前或消息发送
失败时调用,并且通常都是在Producer回调逻辑触发之前。onAcknowledgement运行在
Producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢Producer的消息发
送效率。
*/
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception exception) {
System.out.println("拦截器1---back");
if (exception != null) {
// 如果发生异常,记录日志中
exception.printStackTrace();
}
}
//关闭Interceptor,主要用于执行一些资源清理工作。
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
//生产者发送消息的配置增加拦截器
//这是拦截器,可以配置多个,逗号分隔
configs.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.InterceptorOne");
消息分区
默认的分区器 DefaultPartitioner
。kafka在没有指定分区值的时候,会判断key是不是有值,如果key有值的情况下,将key的hashcode值与topic进行取余得到分区值。如果没有指定分区也没有key值的话,kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并且可能一直使用该分区,待该分区的bacth已满或者已完成一个批次的消息,kafka会再随机一个分区进行使用,和上次选择的分区不同。
自定义分区
public class MyPartition implements Partitioner {
/**
* 为指定的消息记录计算分区值
*
* @param topic 主题名称
* @param key 根据该key的值进行分区计算,如果没有则为null。
* @param keyBytes key的序列化字节数组,根据该数组进行分区计算。如果没有key,则为 null
* @param value 根据value值进行分区计算,如果没有,则为null
* @param valueBytes value的序列化字节数组,根据此值进行分区计算。如果没有,则为 null
* @param cluster 当前集群的元数据
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//所有的分区
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
System.out.println("topic:" + topic);
if (key != null) {
String keyString = key.toString();
System.out.println("key:" + keyString);
}
String valueString = value.toString();
System.out.println("value:" + valueString);
//需要发送的分区
int partition = 0;
if (valueString.contains("1")) {
partition = 1;
}
if (valueString.contains("2")) {
partition = 2;
}
System.out.println("发送的分区:" + partition);
return partition;
}
/*** 关闭分区器的时候调用该方法 */
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
//需要再消费者发送消息的配置中指定使用的分区器
//关联自定义分区策略
configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartition.class);
消息序列化器
kafka没有使用JDK提供的序列化器。因为JDK提供的序列化器很重,里面包含了很多保护数据安全的。
自定义序列化类
import java.nio.ByteBuffer;
import java.util.Map;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;
public class MySerializer implements Serializer<Student>{
@Override
public void configure(Map<String, ?> configs, boolean isKey){
//不做任何配置
}
@Override
public byte[] serialize(String topic, Student data){
try{
byte[] serializedName;//将name属性变成字节数组
int serializedLength;//表示name属性的值的长度
if(data == null){
return null;
}else{
if(data.getName() != null){
serializedName = data.getName();.getBytes("UTF-8");
serializedLength = serializedName.length;
}else{
serializedName = new byte[0];
serializedLength = 0;
}
}
//我们要定义一个buffer用来存储age的值,age是int类型,需要4个字节。还要存储name的值的长度(后面会分析为什么要存name的值的长度),用int表示就够了,也是4个字节,name的值是字符串,长度就有值来决定,所以我们要创建的buffer的大小就是这些字节数加在一起:4+4+name的值的长度
ByteBuffer buffer = ByteBuffer.allocate(4+4+serializedLength);
//put()执行完之后,buffer中的position会指向当前存入元素的下一个位置
buffer.putInt(data.getAge());
//由于在读取buffer中的name时需要定义一个字节数组来存储读取出来的数据,但是定义的这个数组的长度无法得到,所以只能在存name的时候也把name的长度存到buffer中
buffer.putInt(serializedLength);
buffer.put(serializedName);
return buffer.array();
}catch(Exception e){
throw new SerializationException("error when serializing..."+e);
}
}
@Override
public void close(){
//不需要关闭任何东西
}
}
自定义反序列化器
import java.nio.ByteBuffer;
import java.util.Map;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Deserializer;
public class MyDeserializer implements Deserializer<Student>{
@Override
public void configure(Map<String, ?> configs, boolean isKey){
}
@Override
public Student deserialize(String topic, byte[] data){
int age;
int nameLength;
String name;
try{
if(data == null){
return null;
}
if(data.length < 8){
throw new SerializationException("Size of data received by IntegerDeserializer is shorter than expected...");
}
ByteBuffer buffer = ByteBuffer.wrap(data);//wrap可以把字节数组包装成缓冲区ByteBuffer
//get()从buffer中取出数据,每次取完之后position指向当前取出的元素的下一位,可以理解为按顺序依次读取
age = buffer.getInt();
nameLength = buffer.getInt();
/*
* 定义一个字节数组来存储字节数组类型的name,因为在读取字节数组数据的时候需要定义一个与读取的数组长度一致的数组,要想知道每个name的值的长度,就需要把这个长度存到buffer中,这样在读取的时候可以得到数组的长度方便定义数组
*/
byte[] nameBytes = new byte[nameLength];
buffer.get(nameBytes);
name = new String(nameBytes, "UTF-8");
return new Student(name, age);
}catch(Exception e){
throw new SerializationException("error when deserializing..."+e);
}
}
@Override
public void close(){
}
}
生产者消费者配置
//生产者配置
//设置value的序列化类为自定义序列化类全路径
configs.put("value.serializer", "com.MySerializer");
//消费者配置
//设置value的反序列化类为自定义序列化类全路径
configs.put("value.deserializer", "com.MyDeserializer");
消费者自动提交offset
// 自动提交
configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// 提交时间间隔
configs.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000);
消费者手动提交offset
// 手动提交
configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
while (true){
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
// 手动提交
//同步提交 会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响 但是会用重试机制
//kafkaConsumer.commitSync();
//异步提交 发送完提交offset请求后,就去消费下一批数据了。但是如果失败没有重试机制。不会阻塞当前线程
kafkaConsumer.commitAsync();
}
Springboot+kafka
yml配置
spring:
kafka:
bootstrap-servers: 52.82.98.209:10903,52.82.98.209:10904
producer: # producer 生产者
retries: 0 # 重试次数
acks: 1 # 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
batch-size: 16384 # 批量大小
buffer-memory: 33554432 # 生产端缓冲区大小
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer: # consumer消费者
group-id: javagroup # 默认的消费组ID
enable-auto-commit: true # 是否自动提交offset
auto-commit-interval: 100 # 提交offset延时(接收到消息后多久提交offset)
# earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
# latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
# none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
auto-offset-reset: latest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
生产者
同步消息
@Autowired
private KafkaTemplate template;
@GetMapping("sendSync")
public String sendSync(@RequestParam("msg") String msg) throws ExecutionException, InterruptedException {
// ListenableFuture future = template.send(new ProducerRecord<String, String>("topic-spring-02", 0, 1, msg));
ListenableFuture future = template.send("topic-spring-02", msg);
// 同步等待broker的响应
SendResult<String, String> result = (SendResult<String, String>) future.get();
RecordMetadata metadata = result.getRecordMetadata();
logger.info("同步发送消息,broker的响应\n主题:{}\n分区{}\n偏移量{}\n序列化的key字节{}\n序列化的value字节{}\n时间戳:{}",
metadata.topic(), metadata.partition(), metadata.offset(), metadata.serializedKeySize(), metadata.serializedValueSize(), metadata.timestamp());
return "success";
}
异步消息
@GetMapping("sendAsync")
public String sendAsync(@RequestParam("msg") String msg) throws ExecutionException, InterruptedException {
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic-spring-02", msg);
ListenableFuture<SendResult<String, String>> future = template.send(record);
// 添加回调,异步等待响应
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> result) {
RecordMetadata metadata = result.getRecordMetadata();
logger.info("异步消息发送确认-成功,broker的响应\n主题:{}\n分区{}\n偏移量{}\n序列化的key字节{}\n序列化的value字节{}\n时间戳:{}",
metadata.topic(), metadata.partition(), metadata.offset(), metadata.serializedKeySize(), metadata.serializedValueSize());
}
@Override
public void onFailure(Throwable ex) {
ex.printStackTrace();
logger.info("异步消息发送确认,=========================发送失败=========================");
}
});
return "success";
}
异步消息全局监听
@GetMapping("sendAsync")
public String sendAsync(@RequestParam("msg") String msg) throws ExecutionException, InterruptedException {
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic-spring-02", msg);
ListenableFuture<SendResult<String, String>> future = template.send(record);
return "success";
}
@PostConstruct
public void init() {
/**
* 配置异步消息确认监听
*/
template.setProducerListener(new ProducerListener<String, String>() {
@Override
public void onSuccess(ProducerRecord<String, String> producerRecord, RecordMetadata metadata) {
logger.info("全局异步消息确认监听-成功,broker的响应\n主题:{}\n分区{}\n偏移量{}\n序列化的key字节{}\n序列化的value字节{}\n时间戳:{}",
metadata.topic(), metadata.partition(), metadata.offset(), metadata.serializedKeySize(), metadata.serializedValueSize());
}
@Override
public void onError(ProducerRecord<String, String> producerRecord, RecordMetadata metadata, Exception exception) {
exception.printStackTrace();
logger.info("全局异步消息确认监听-失败,broker的响应\n主题:{}\n分区{}\n偏移量{}\n序列化的key字节{}\n序列化的value字节{}\n时间戳:{}",
metadata.topic(), metadata.partition(), metadata.offset(), metadata.serializedKeySize(), metadata.serializedValueSize());
}
});
}
分区策略
使用之前的自定义分区代码
全局自定义分区配置
//全局分区策略配置
/**
* 复写{@link KafkaAutoConfiguration#kafkaProducerFactory(ObjectProvider)}
*
* @param customizers
* @return
*/
@Bean
@ConditionalOnMissingBean(ProducerFactory.class)
public ProducerFactory<?, ?> kafkaProducerFactory(
ObjectProvider<DefaultKafkaProducerFactoryCustomizer> customizers) {
// 配置参数
Map<String, Object> map = this.properties.buildProducerProperties();
//注意分区器在这里!!!
map.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartition.class);
DefaultKafkaProducerFactory<?, ?> factory = new DefaultKafkaProducerFactory<>(map);
String transactionIdPrefix = this.properties.getProducer().getTransactionIdPrefix();
if (transactionIdPrefix != null) {
factory.setTransactionIdPrefix(transactionIdPrefix);
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
局部自定义分区配置
//局部分区器配置 只在本类中发送kafka消息才会走自定义分区代码
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@PostConstruct
public void setKafkaTemplate() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
//注意分区器在这里!!!
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartition.class);
this.template = new KafkaTemplate<String, String>(new DefaultKafkaProducerFactory<>(props));
}
public KafkaTemplate getKafkaTemplate(){
return template;
}
序列化
- 前面用到的是Kafka自带的字符串序列化器(
org.apache.kafka.common.serialization.StringSerializer
) - 除此之外还有:ByteArray、ByteBuffer、Bytes、Double、Integer、Long 等
- 这些序列化器都实现了接口(
org.apache.kafka.common.serialization.Serializer
) - 基本上,可以满足绝大多数场景
- 生产者如何使用了自定义序列化,那么消费者也需要定义反序列化器
自定义序列化器
public class MySerializer implements Serializer {
@Override
public byte[] serialize(String s, Object o) {
String json = JSON.toJSONString(o);
return json.getBytes();
}
}
自定义反序列化器
public class MyDeserializer implements Deserializer {
@Override
public Object deserialize(String s, byte[] bytes) {
try {
String json = new String(bytes, "utf-8");
return JSON.parse(json);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
}
修改yml配置
spring:
kafka:
producer: # producer 生产者
value-serializer: com.kafka.boot.config.MySerializer
consumer: # consumer消费者
value-deserializer: com.kafka.boot.config.MyDeserializer
拦截器
kafka没有提供可配置拦截器的参数,如果需要用的拦截器,需要参考org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
复写一下kafkaProducerFactory
和kafkaConsumerFactory
生产者拦截器
public class MyProducerInterceptor<KEY, VALUE> implements ProducerInterceptor<KEY, VALUE> {
/**
* 消息发送之前调用
* @param record
* @return
*/
@Override
public ProducerRecord<KEY, VALUE> onSend(ProducerRecord<KEY, VALUE> record) {
System.out.println("生产者拦截器1---go");
// 此处根据业务需要对相关的数据作修改
String topic = record.topic();
Integer partition = record.partition();
Long timestamp = record.timestamp();
KEY key = record.key();
VALUE value = record.value();
Headers headers = record.headers();
// 添加消息头
headers.add("interceptor", "interceptorOne".getBytes());
return new ProducerRecord<KEY, VALUE>(topic, partition, timestamp, key, value, headers);
}
/**
* 当发送到服务器的记录已被确认时,或者当发送记录在发送到服务器之前失败时调用此方法
* @param recordMetadata
* @param exception
*/
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception exception) {
System.out.println("生产者拦截器1---back");
if (exception != null) {
// 如果发生异常,记录日志中
exception.printStackTrace();
}
}
/**
* 关闭Interceptor,主要用于执行一些资源清理工作
*/
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
消费者拦截器
public class MyConsumerInterceptor<KEY, VALUE> implements ConsumerInterceptor<KEY, VALUE> {
/**
该方法在poll方法返回之前调用。调用结束后poll方法就返回消息了。
该方法可以修改消费者消息,返回新的消息。拦截器可以过滤收到的消息或生成新的消息。
*/
@Override
public ConsumerRecords<KEY, VALUE> onConsume(ConsumerRecords<KEY, VALUE> consumerRecords) {
System.out.println("消费者拦截器1---go");
// 此处根据业务需要对相关的数据作修改
return consumerRecords;
}
/**
当消费者提交偏移量时,调用该方法。通常你可以在该方法中做一些记账类的动作,比如打日志等。
调用者将忽略此方法抛出的任何异常。
*/
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> map) {
}
/**
* 关闭Interceptor之前调用
*/
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
拦截器配置
@Autowired
private KafkaProperties properties;
/**
* kafka没有提供可配置拦截器的参数
* 需要复写{@link KafkaAutoConfiguration#kafkaProducerFactory(ObjectProvider)}
*
* @param customizers
* @return
*/
@Bean
@ConditionalOnMissingBean(ProducerFactory.class)
public ProducerFactory<?, ?> kafkaProducerFactory(
ObjectProvider<DefaultKafkaProducerFactoryCustomizer> customizers) {
// 配置参数
Map<String, Object> map = this.properties.buildProducerProperties();
// 增加过滤器 多个逗号分隔
map.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.kafka.MyProducerInterceptor");
DefaultKafkaProducerFactory<?, ?> factory = new DefaultKafkaProducerFactory<>(map);
String transactionIdPrefix = this.properties.getProducer().getTransactionIdPrefix();
if (transactionIdPrefix != null) {
factory.setTransactionIdPrefix(transactionIdPrefix);
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
/**
* {@link KafkaAutoConfiguration#kafkaConsumerFactory(ObjectProvider)}
* @param customizers
* @return
*/
@Bean
@ConditionalOnMissingBean(ConsumerFactory.class)
public ConsumerFactory<?, ?> kafkaConsumerFactory(ObjectProvider<DefaultKafkaConsumerFactoryCustomizer> customizers) {
// 配置参数
Map<String, Object> map = this.properties.buildConsumerProperties();
// 增加过滤器 多个逗号分隔
map.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.kafka.MyConsumerInterceptor");
DefaultKafkaConsumerFactory<Object, Object> factory = new DefaultKafkaConsumerFactory<>(map);
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
消费者
消费者组
/**
* 不指定group,默认取yml里配置的
可以指定组,指定分区
*/
@KafkaListener(topics = "topic-spring-02")
public void onMessage(ConsumerRecord<String, String> record) {
Optional<ConsumerRecord<String, String>> optional = Optional.ofNullable(record);
if (optional.isPresent()) {
logger.info("消费者消费\n消息头字段:{}\n消息的key:{}\n消息的偏移量:{}\n消息的分区号:{}\n消息的序列化key字节数:{}\n" +
"消息的序列化value字节数:{}\n消息的时间戳{}\n消息的时间戳类型:{}\n消费者消费消息的主题:{}\n消息的值:{}\n"
, Arrays.toString(record.headers().toArray()), record.key(), record.offset(), record.partition(), record.serializedKeySize()
, record.serializedValueSize(), record.timestamp(), record.timestampType(), record.topic(), record.value());
}
}
位移提交
自动提交
#之前在yml的消费者配置中已经配置了自动提交
spring:
kafka:
consumer: # consumer消费者
enable-auto-commit: true # 是否自动提交offset
auto-commit-interval: 100 # 提交offset延时(接收到消息后多久提交offset)
手动提交
全局设置手动提交
spring:
kafka:
consumer: # consumer消费者
enable-auto-commit: false # 这样设置完之后,所有的消费者都需要手动提交offset
局部设置手动提交
@Bean
public KafkaListenerContainerFactory<?> manualKafkaListenerContainerFactory() {
//使用yml配置然后进行修改
// Map<String, Object> configProps = this.properties.buildConsumerProperties();
//自定义配置
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 注意这里!!!设置手动提交
configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(configProps));
// ack模式:
// AckMode针对ENABLE_AUTO_COMMIT_CONFIG=false时生效,有以下几种:
//
// RECORD
// 每处理一条commit一次
//
// BATCH(默认)
// 每次poll的时候批量提交一次,频率取决于每次poll的调用频率
//
// TIME
// 每次间隔ackTime的时间去commit(跟auto commit interval有什么区别呢?)
//
// COUNT
// 累积达到ackCount次的ack去commit
//
// COUNT_TIME
// ackTime或ackCount哪个条件先满足,就commit
//
// MANUAL
// listener负责ack,但是背后也是批量上去
//
// MANUAL_IMMEDIATE
// listner负责ack,每调用一次,就立即commit
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
进行手动提交
@KafkaListener(topics = "topic-spring-02", groupId = "spring-kafka-consumer1", containerFactory = "manualKafkaListenerContainerFactory")
public void manualCommit(@Payload String message,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
Consumer consumer,
Acknowledgment ack) {
logger.info("手动提交偏移量 , partition={}, msg={}", partition, message);
// 同步提交
consumer.commitSync();
//异步提交
//consumer.commitAsync();
// ack提交也可以,会按设置的ack策略走(参考ack模式)
// ack.acknowledge();
}
//同步和异步提交可以混合使用
//异步提交如果失败不会星星重试
//一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题
//因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。
//但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。否则就会造成重复消费
//因此,在消费者关闭前一般会组合使用commitAsync()和commitSync()。
try {
logger.info("同步异步搭配 , partition={}, msg={}", partition, message);
//先异步提交
consumer.commitAsync();
//继续做别的事
} catch (Exception e) {
System.out.println("commit failed");
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}