概念
Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。(流处理,指的是网络采样信息)
Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以收集并处理用户在网站中的所有动作流数据以及物联网设备的采样信息。
该平台提供了消息的订阅与发布的消息队列,一般用作系统间解耦、异步通信、削峰填谷等作用。同时Kafka又提供了Kafka streaming插件包实现了实时在线流处理。相比较一些专业的流处理框架不同,Kafka Streaming计算是运行在应用端,具有简单、入门要求低、部署方便等优点。
消息队列是一种在分布式和大数据开发中不可或缺的中间件。在分布式开发或者大数据开发中通常使用消息队列进行缓冲、系统间解耦和削峰填谷等业务场景,常见的消息队列工作模式大致会分为两大类:
-
至多一次:消息生产者将数据写入消息系统,然后由消费者负责去拉去消息服务器中的消息,一旦消息被确认消费之后 ,由消息服务器主动删除队列中的数据,这种消费方式一般只允许被一个消费者消费,并且消息队列中的数据不允许被重复消费。
-
没有限制:同上诉消费形式不同,生产者发不完数据以后,该消息可以被多个消费者同时消费,并且同一个消费者可以多次消费消息服务器中的同一个记录。主要是因为消息服务器一般可以长时间存储海量消息。
Kafka就属于没有限制这一类。
Kafka集群以Topic形式负责分类集群中的Record,每一个Record属于一个Topic。每个Topic底层都会对应一组分区的日志用于持久化Topic中的Record。同时在Kafka集群中,Topic的每一个日志的分区都一定会有1个Borker担当该分区的Leader,其他的Broker担当该分区的follower,Leader负责分区数据的读写操作,follower负责同步改分区的数据。这样如果分区的Leader宕机,改分区的其他follower会选取出新的leader继续负责该分区数据的读写。其中集群的中Leader的监控和Topic的部分元数据是存储在Zookeeper中。
- 分区数:将一个topic的数据,分散地存在n个文件当中
- 副本因子:每个分区总共的数量。
零拷贝
数据直接从内核空间发送出去,并没有走用户空间。
Memory Mapped Files(mmp)内存映射文件
将数据写入内核空间,就等价写到磁盘。什么时候刷数据由操作系统去决定,应用程序并不会因为等待IO而挂起。
Kafka消费者组
概念:消费者使用Consumer Group名称标记自己,并且发布到Topic的每条记录都会传递到每个订阅Consumer Group中的一个消费者实例。如果所有Consumer实例都具有相同的Consumer Group,那么Topic中的记录会在该ConsumerGroup中的Consumer实例进行均分消费;如果所有Consumer实例具有不同的ConsumerGroup,则每条记录将广播到所有Consumer Group进程。
更常见的是,我们发现Topic具有少量的Consumer Group,每个Consumer Group可以理解为一个“逻辑的订阅者”。每个Consumer Group均由许多Consumer实例组成,以实现可伸缩性和容错能力。这无非就是发布-订阅模型,其中订阅者是消费者的集群而不是单个进程。这种消费方式Kafka会将Topic按照分区的方式均分给一个Consumer Group下的实例,如果ConsumerGroup下有新的成员介入,则新介入的Consumer实例会去接管ConsumerGroup内其他消费者负责的某些分区,同样如果一下ConsumerGroup下的有其他Consumer实例宕机,则由改ConsumerGroup其他实例接管。
栗子:消费者1订阅topic01,消费者2订阅topic01,消费者1与消费者2的Consumer Group为a, Topic01的分区数为2,那么topic01分区1的消息会发送给消费者1,分数2的消息会发送给消费者2。
Kafka集群配置
修改zookeeper的配置文件
# 修改dataDir(可选),数据存放的位置
dataDir=...
# 在末尾添加集群的配置
server.1=CentOSA:2888:3888
server.2=CentOSB:2888:3888
server.3=CentOSC:2888:3888
启动zookeeper
# 启动
zkServer.sh start zoo.cfg
# 查看是否启动成功
zkServer.sh status zoo.cfg
修改Kafka config/server.properties
# 设置监听数据源的端口
listeners=PLAINTEXT://CentOSA:9092
# 修改日志目录(可选)
log.dirs=...
# 配置zookeeper
zookeeper.connect=CentOSA:2181,CentOSB:2181,CentOSC:2181
启动Kafka
./bin/kafka-server-start.sh -daemon ./config/server.properties
Kafka集群Topic测试
创建Topic
# --partitions 分区数
# --replication-factor 副本因子数(不可超过Broker节点的个数,即集群中的服务器数)
kafka-topics.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --create --topic topic01 --partitions 3 --replication-factor 3
查看Topic,消费者组
# 查看所有的Topic
kafka-topics.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --list
# 查看某个Topic的详情
kafka-topics.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --describe --topic topic01
# 查看所有消费者组
kafka-consumer-groups.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --list
# 查看某个消费者组
kafka-consumer-groups.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --describe --group g1
# 其余命令
--list, --describe, --delete, --reset-offsets
修改Topic, 分区数只能增加,不能减少
kafka-topics.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --alter --topic topic03 --partitions 2
删除Topic,会将日志文件改名,文件名末尾接上delete
kafka-topics.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --delete --topic topic03
订阅
# --property print.key=true 是否打印key
# --property print.value=true 是否打印值
# --property key.separator=, 打印键值之间的分隔符
kafka-console-consumer.sh --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 --topic topic01 --group g1 --property print.key=true --property print.value=true --property key.separator=,
生产
kafka-console-producer.sh --broker-list CentOSA:9092,CentOSB:9092,CentOSC:9092 --topic topic01
Kafka基础API
导包,配置文件
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 日志相关的包 -->
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- 用于序列化自定义对象 -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
# log4j.properties
log4j.rootLogger = info,console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} %c - %m%n
Topic 基本操作 DML管理
// 1. 创建KafkaAdminClient
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092");
KafkaAdminClient adminClient = (KafkaAdminClient) KafkaAdminClient.create(props);
// 创建Topic信息
// 默认异步创建
// adminClient.createTopics(Arrays.asList(new NewTopic("topic01", 3, (short) 3)));
// 此方式为同步创建
CreateTopicsResult topic01 = adminClient.createTopics(Arrays.asList(new NewTopic("topic02", 3, (short) 3)));
topic01.all().get();
// 删除Topic,和创建一样是异步,想要同步的方式也一样
// adminClient.deleteTopics(Arrays.asList("topic01", "topic02"));
//
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 查看Topic列表
ListTopicsResult topicsResult = adminClient.listTopics();
try {
Set<String> names = topicsResult.names().get();
for (String name : names) {
System.out.println(name);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 查看Topic详细信息
DescribeTopicsResult dtr = adminClient.describeTopics(Arrays.asList("topic01"));
Map<String, TopicDescription> topicDescriptionMap = dtr.all().get();
for (Map.Entry<String, TopicDescription> entry : topicDescriptionMap.entrySet()) {
System.out.println(entry.getValue() + "" + entry.getValue());
}
// 关闭AdminClient
adminClient.close();
生产者
// 配置Kafka的信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建Kafka生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
// 向Kafka中发送数据
for (int i = 0; i < 10; i ++) {
kafkaProducer.send(new ProducerRecord<>("topic", "hello world " + "i" + i));
}
kafkaProducer.close();
注意事项:生产者在代码末尾记得将producer给close,否则会发生消息丢失的情况
消费者
消费策略:subscribe(利用组管理机制)/assign(手动指定消费分区)
// 创建 KafkaConsumer
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "g2");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 订阅相关的Topics
consumer.subscribe(Pattern.compile("^topic.*"));
while (true) {
// 设置抓取消息的时间
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
if (! consumerRecords.isEmpty()) {
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()) {
ConsumerRecord<String, String> record = recordIterator.next();
String topic = record.topic();
int partition = record.partition();
long offset = record.offset();
String key = record.key();
String value = record.value();
long timestamp = record.timestamp();
System.out.println(topic + " " + partition + " " + offset + " " + key + " " + value + " " + timestamp);
}
}
}
除此之外还可以手动指定消费的分区,此时就不需要配置group.id,即不需要这行代码props.put(ConsumerConfig.GROUP_ID_CONFIG, "g2");
// 订阅相关的Topics,手动指定消费分区,失去组管理特性
TopicPartition topicPartition = new TopicPartition("topic01", 0);
List<TopicPartition> partitions = Arrays.asList(topicPartition);
consumer.assign(partitions);
// 指定消费分区的位置,从0位置开始消费
// consumer.seekToBeginning(partitions);
// 指定消费分区的位置,从指定位置(1位置)开始消费
// consumer.seek(topicPartition, 1);
自定义分区,作用就是在生产者生产消息时,指定送入topic的哪个分区
- 没有key的消息,默认的机制是轮询分区
- 有key的是根据key的hash值进行分区的
-
编写配置类,实现Partitioner类,该实例实现的是一个基于轮询的方式
package com.zwh.partitioner; import org.apache.kafka.clients.producer.Partitioner; import org.apache.kafka.common.Cluster; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.utils.Utils; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * @author: zwh * @Date: 2021/11/02 10:09 */ public class UserDefinePartitioner implements Partitioner { private AtomicInteger partitionNum = new AtomicInteger(0); /** * 返回分区号 * @param topic * @param key * @param keyBytes * @param value * @param valueBytes * @param cluster * @return */ @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 获取所有分区 List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); int numPartitions = partitions.size(); if (keyBytes == null) { int andAdd = partitionNum.getAndIncrement(); // & 是为了防止负数 return (andAdd & Integer.MAX_VALUE) % numPartitions; } else { return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions; } } @Override public void close() { System.out.println("close"); } @Override public void configure(Map<String, ?> map) { System.out.println("configure"); } }
-
使用,添加配置即可 props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, UserDefinePartitioner.class.getName());
序列化
自定义序列化类,实现Deserializer接口,使用commons-lang3包下的SerializationUtils来实现
public class UserDefineSerializer implements Serializer<Object> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println("configs");
}
@Override
public byte[] serialize(String topic, Object data) {
return SerializationUtils.serialize((Serializable) data);
}
@Override
public void close() {
System.out.println("close");
}
}
自定义反序列化类
public class UserDefineDeserializer implements Deserializer<Object> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println("configs");
}
@Override
public Object deserialize(String topic, byte[] data) {
return SerializationUtils.deserialize(data);
}
@Override
public void close() {
System.out.println("close");
}
}
使用,即在配置中设置即可
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserDefineSerializer.class.getName());
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDefineDeserializer.class.getName());
拦截器
自定义拦截器,即实现ProducerInterceptor接口
public class UserDefineProducerInterceptor implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord record) {
return new ProducerRecord(record.topic(), record.key(), record.value() + "mashibing.com");
}
/**
* 成功发送或者失败都会调用该方法
* @param record,发送成功会返回的源数据
* @param e
*/
@Override
public void onAcknowledgement(RecordMetadata record, Exception e) {
System.out.println("metadata:" + record + ", exception:" + e);
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
使用,即在生产者中添加配置
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, UserDefineProducerInterceptor.class.getName());
Kafka高级API
消费者offset的使用
Kafka消费者默认对于未订阅的topic的offset的时候,也就是系统并没有存储该消费者的消费分区的记录信息,默认Kafka消费者的默认首次消费策略:latest,即 auto.offset.reset=latest
。如果系统保存了消费者某个topic的offset的时候,消费者会从该offset读取。
- earliest: 自动将偏移量重置为最早的偏移量
- latest: 自动将偏移量重置为最新的偏移量
- none: 如果未找到消费者组的先前偏移量,则向消费者抛出异常
手动控制消费者偏移量的提交
Kafka消费者在消费数据的时候默认会定期的提交消费的偏移量,这样就可以保证所有的消息至少可以被消费者消费1次,用户可以通过以下两个参数配置:
enable.auto.commit = true 默认
auto.commit.interval.ms = 5000 默认
如果用户需要自己管理offset的自动提交,可以关闭offset的自动提交,手动管理offset提交的偏移量,注意用户提交的offset偏移量永远都要比本次消费的偏移量+1,因为提交的offset是kafka消费者下一次抓取数据的位置。
具体代码实现,通过消费者下面的方法来实现
/**
* @param offsets,存放相应分区数对应的offset,callback,提交完的回调
*/
public void commitAsync(final Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)
小提示: 消费者启动后,会拿着offset去找初始消费的位置,此后自动消费topic中的数据。也就是说启动后无需offset也可消费topic中的数据,只有再次启动才需要拿着offset去寻找要消费的位置。
生产者acks/retries机制
Kafka生产者在发送完一个的消息之后,要求Broker在规定的额时间Ack应答,如果没有在规定时间内应答,Kafka生产者会尝试n次重新发送消息。默认 acks=1
- acks=1: Leader会将Record写到其本地日志中,但会在不等待所有Follower完全确认的情况下做出响应。在这种情况下,如果Leader确认记录后立即失败,且是在Follower复制记录之前失败,则记录将丢失。
- acks=0: 生产者根本不会等待服务器的任何确认。该记录将立即添加到套接字缓冲区中并视为已发送。在这种情况下,不能保证服务器已收到记录。
- acks=all: 这意味着Leader将等待全套同步副本确认记录。这保证了只要至少一个同步副本仍处于活动状态,记录就不会丢失。这是最有力的保证。这等效于acks = -1设置。
如果生产者在规定的时间内,并没有得到Kafka的Leader的Ack应答,Kafka可以开启reties机制。
request.timeout.ms = 30000 默认
单位为ms
retries = 2147483647 默认
int最大值
可能产生的问题: 数据重复的问题,解决方案即幂等。
幂等
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证生产者发送的消息,不会丢失,而且不会重复。实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:
- 唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识;
- 记录下已处理过的请求标识:光有唯一标识还不够,还需要记录哪些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复记录,拒绝掉。
幂等又称exactly once。要停止多次处理消息,必须仅将其持久化到Kafka Topic中一次。在初始化期间,kafka会给生产者生成一个唯一的ID成为Producer ID或PID。
PID和序列号与消息捆绑在一起,然后发送给Broker。由于序列号从0开始单调递增,因此,仅当消息的序列号比该PID/TopicPartition中最后提交的消息正好大1时,Broker才会接受该消息。如果不是这种情况,则Broker认定是生产者重新发送该消息。
enable.idempotence=false
默认
注意:在使用幂等性的时候,要求必须开启retries=true和acks=all并且max.in.flight.requests.per.connection要设置小于等于5(默认为5),为了保证严格一致性(有序性),建议设置为1。max.in.flight.requests.per.connection在未接收到n个回应,生产者就阻塞。
备注: ack/retries 和 幂等均是在生产者方进行设置。
Kafka事务
Kafka的幂等性,只能保证一条记录的在分区发送的原子性,但是如果要保证多条记录(多分区)之间的完整性,这个时候就需要开启kafka的事务操作。
在Kafka0.11.0.0除了引入的幂等性的概念,同时也引入了事务的概念。通常Kafka的事务分为 生产者事务Only、消费者&生产者事务。一般来说默认消费者消费的消息的级别是read_uncommited数据,这就有可能读取到事务失败的数据。所以想要开启事务,需要设置消费者的事务隔离级别为read_committed。
生产者事务Only栗子:例如一个事务需要插入3条数据,分别插入到分区1, 2, 3,其中有一个失败了,那么其余两个也应该回滚。
消费者&生产者事务栗子:例如一个消费生产者(某个业务)消费kafka中的消息后,将数据再写入新的kafka,写入失败,那么刚刚消费的消息应该需要被重复消费。实际使用较多。该事务中需要关闭消费者自动提交offset的设置。
isolation.level=read_uncommitted
默认
该选项有两个值read_committed | read_uncommitted,如果想要开启事务控制,消费端必须将事务的隔离级别设置为read_committed
开启生产者事务的时候,只需要指定transactional.id属性即可,一旦开启了事务,默认生产者就已经开启了幂等性。但是要求"transactional.id"的取值必须是唯一的(可以使用UUID),同一时刻只能有一个"transactional.id"存在,其他的将会被关闭。
生产者Only
-
生产者事务代码
将if中的代码注释打开,那么消费者开启事务后无法消费到消息。
public class Producer { public static void main(String[] args) { KafkaProducer<String, String> producer = initProducer(); try { producer.initTransactions(); producer.beginTransaction(); // 向Kafka中发送数据 for (int i = 0; i < 10; i++) { // if (i == 8) { // int a = 5 / 0; // } producer.send(new ProducerRecord<>("topic", "transactionMessage" + i)); producer.flush(); } producer.commitTransaction(); } catch (Exception e) { System.err.println(e.getMessage()); producer.abortTransaction(); } } public static KafkaProducer<String, String> initProducer() { // 配置Kafka的信息 Properties properties = new Properties(); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置 transactional.id properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // kafka重试机制和幂等 properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); properties.put(ProducerConfig.ACKS_CONFIG, -1); properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 20000); properties.put(ProducerConfig.RETRIES_CONFIG, 3); // 创建Kafka生产者对象 return new KafkaProducer<>(properties); } }
-
消费生产者事务代码
public class ConsumerTransaction { public static void main(String[] args) { // 创建 KafkaConsumer Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.GROUP_ID_CONFIG, "g3"); props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); // 订阅相关的Topics consumer.subscribe(Collections.singleton("topic")); while (true) { // 设置抓取消息的时间 ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1)); if (!consumerRecords.isEmpty()) { Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator(); while (recordIterator.hasNext()) { ConsumerRecord<String, String> record = recordIterator.next(); String topic = record.topic(); int partition = record.partition(); long offset = record.offset(); String key = record.key(); String value = record.value(); long timestamp = record.timestamp(); System.out.println(topic + " " + partition + " " + offset + " " + key + " " + value + " " + timestamp); } } } } }
消费者&生产者事务
- 消费生产者事务代码
public class ConsumerProducer { /** * 消费topic中的数据,将topic中的数据写入topic01中 * @param args */ public static void main(String[] args) { KafkaProducer<String, String> producer = createProducer(); String group5 = "g5"; KafkaConsumer<String, String> g5 = createConsumer(group5); g5.subscribe(Collections.singleton("topic")); while (true) { ConsumerRecords<String, String> records = g5.poll(Duration.ofSeconds(5)); try { producer.initTransactions(); if (!records.isEmpty()) { Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>(16); producer.beginTransaction(); records.forEach(record -> { String topic = record.topic(); long offset = record.offset(); int partition = record.partition(); String key = record.key(); String value = record.value(); ProducerRecord<String, String> producerRecord = new ProducerRecord<>("topic01", key, value + "--- from topic to topic01"); producer.send(producerRecord); // 偏移量为当前偏移量 +1 offsets.put(new TopicPartition(topic, partition), new OffsetAndMetadata(offset + 1)); producer.flush(); }); // 事务处理完毕,提交偏移量,提交事务 producer.commitTransaction(); producer.sendOffsetsToTransaction(offsets, group5); } } catch (Exception e) { producer.abortTransaction(); System.out.println(e.getMessage()); } } } public static KafkaConsumer<String, String> createConsumer(String groupId) { // 创建 KafkaConsumer Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); return new KafkaConsumer<>(props); } public static KafkaProducer<String, String> createProducer() { // 配置Kafka的信息 Properties properties = new Properties(); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置 transactional.id properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // kafka重试机制和幂等 properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); properties.put(ProducerConfig.ACKS_CONFIG, -1); properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 20000); properties.put(ProducerConfig.RETRIES_CONFIG, 3); // 创建Kafka生产者对象 return new KafkaProducer<>(properties); } }
kafka配置
生产者配置
# 达到指定大小(byte)再发送到kafka
batch.size=16384
# 达到指定时间(ms)再发送到kafka
linger.ms=0
# kakfa集群地址
bootstrap.servers=ip:port,ip:port,...
# 键序列化方式
key.serializer=
# 值序列化方式
value.serializer=
# 事务id,使用事务时需要配置
transactional.id=
# 幂等,默认不开启
enable.idempotence=false
# ack确认机制,默认1(主机确认),0(什么都不确认),-1(主从全部确认)
acks=1
# 等待响应时间
request.timeout.ms=30000
# 重试次数
retries=0
消费者配置
# kakfa集群地址
bootstrap.servers=ip:port,ip:port,...
# 键序列化方式
key.serializer=
# 值序列化方式
value.serializer=
# 分组id
group.id=
# 事务级别,默认read_uncommitted,使用事务时需设置为read_committed
isolation.level=read_uncommitted
# 自动提交offset
enable.auto.commit=true
# 自动提交offset时间间隔
auto.commit.interval.ms=5000
# 读取offset的位置,默认lastest,即最新位置;可设置为earliest,即起始位置
auto.offset.reset=lastest
SpringBoot整合Kafka
导包, yml文件配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring.kafka.bootstrap-servers=CentOSA:9092,CentOSB:9092,CentOSC:9092
# 重试次数
spring.kafka.producer.retries=5
# 开启应答
spring.kafka.producer.acks=all
# 缓冲区大小
spring.kafka.producer.batch-size=16384
# 数据发送未达到batch-size等待时间,默认为0
spring.kafka.linger-ms=5
spring.kafka.producer.buffer-memory=33554432
# 事务控制
spring.kafka.producer.transaction-id-prefix=transaction-id-
# 序列化包
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 开启幂等
spring.kafka.producer.properties.enable.idempotence=true
# 消费者组
spring.kafka.consumer.group-id=group1
# 设置偏移量
spring.kafka.consumer.auto-offset-reset=earliest
# 自动提交
spring.kafka.consumer.enable-auto-commit=true
# 提交间隔
spring.kafka.consumer.auto-commit-interval=100
# 隔离级别
spring.kafka.consumer.properties.isolation.level=read_committed
# 序列化包
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
生产者代码
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@RequestMapping("/send")
public void send(String topic, String value) {
kafkaTemplate.send(topic, value);
}
}
生产者事务
- 通过kafkaTemplate的executeInTransaction创建事务
@RequestMapping("/transaction") public void transaction(String topic, String value) { kafkaTemplate.executeInTransaction(new KafkaOperations.OperationsCallback<String, String, Object>() { @Override public Object doInOperations(KafkaOperations<String, String> kafkaOperations) { kafkaOperations.send(new ProducerRecord<>(topic, value)); return null; } }); }
- 通过@Transactional注解来实现事务
@Service @Transactional public class KafkaTransaction { @Autowired private KafkaTemplate<String, String> kafkaTemplate; public void sendByTransaction(String topic, String value) { kafkaTemplate.send(topic, value); } }
消费者代码
@Component
public class KafkaConsumer {
@KafkaListener(topics = {"topiczwh"})
public void consume(ConsumerRecord<String, String> record) {
System.out.println("record: " + record);
}
/**
* 将消费的消息再放入新的kafka中
* @param record
* @return
*/
@KafkaListener(topics = {"topiczwh"})
@SendTo("topichhh")
public String consume2(ConsumerRecord<String, String> record) {
return record.value() + "zwh....";
}
@KafkaListener(topics = {"topichhh"})
public void consume3(ConsumerRecord<String, String> record) {
System.out.println("topichhh: " + record);
}
}
Kafka主备复制
LEO(log end offset):下一个数据的位置 HW:水位线,已经备份好的数据