kafka - 简单了解

初识Kafka

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以收集并处理用户在网站中的所有动作流数据以及物联网设备的采样信息,一般用作系统间解耦异步通信削峰填谷等作用。同时Kafka又提供了Kafka streaming插件包实现了实时在线流处理。相比较一些专业的流处理框架不同,Kafka Streaming计算是运行在应用端,具有简单、入门要求低、部署方便等优点。

  • 消息队列Message Queue
  • Kafka Streaming 流处理

Message Queue是什么?

消息队列是一种在分布式和大数据开发中不可或缺的中间件。在分布式开发或者大数据开发中通常使用消息队列进行缓冲、系统间解耦和削峰填谷等业务场景,常见的消息队列工作模式大致会分为两大类:

  • 至多一次:消息生产者将数据写入消息系统,然后由消费者负责去拉取消息服务器中的消息,一旦消息被确认消费之后 ,由消息服务器主动删除队列中的数据,这种消费方式一般只允许被一个消费者消费,并且消息队列中的数据不允许被重复消费。
  • 没有限制:同上诉消费形式不同,生产者发完数据以后,该消息可以被多个消费者同时消费,并且同一个消费者可以多次消费消息服务器中的同一个记录,主要是因为消息服务器一般可以长时间存储海量消息。

Kafka 基础架构

Kafka集群以Topic形式负责分类集群中的Record,每一个Record属于一个Topic。每个Topic底层都会对应一组分区的日志用于持久化Topic中的Record。同时在Kafka集群中,Topic的每一个日志的分区都一定会有1个Borker担当该分区的Leader,其他的Broker担当该分区的follower,Leader负责分区数据的读写操作,follower负责同步该分区的数据。这样如果分区的Leader宕机,该分区的其他follower会选取出新的leader继续负责该分区数据的读写。其中集群的中Leader的监控和Topic的部分元数据是存储在Zookeeper中。

name description
topic 主题,用来把record分类
record 记录,由key、value、timestamp组成
broker 每一台物理机
leader 领导者,负责读写
follower 跟随者,负责备份

kafka由4个核心API构成。

name description
Producer API 发布record到一至多个topic
Consumer API 订阅一到多个topic,并消费record
Streams API 转化流,将输入转化为输出
Connertor API 连接kafka

在Kafka中,客户端和服务器之间的通信是通过简单,高性能,与语言无关的TCP协议完成的。该协议已版本化,并与旧版本保持向后兼容性。

Topics and Logs

Kafka中所有消息是通过Topic为单位进行管理,每个Kafka中的Topic通常会有多个订阅者,负责订阅发送到该Topic中的数据。Kafka负责管理集群中每个Topic的一组日志分区数据。

每组日志分区是一个有序的不可变的的日志序列,分区中的每一个Record都被分配了唯一的序列编号称为是offset,Kafka 集群会持久化所有发布到Topic中的Record信息,该Record的持久化时间是通过配置文件指定,默认是168小时。

log.retention.hours=168

Kafka底层会定期的检查日志文件,然后将过期的数据从log中移除,由于Kafka使用硬盘存储日志文件,因此使用Kafka长时间缓存一些日志文件是不存在问题的。

Kafka中对Topic实现日志分区的有以下目的:

  • 首先,它们允许日志扩展到超出单个服务器所能容纳的大小。每个单独的分区都必须适合托管它的服务器,但是一个Topic可能有很多分区,因此它可以处理任意数量的数据。
  • 其次每个服务器充当其某些分区的Leader,也可能充当其他分区的Follwer,因此群集中的负载得到了很好的平衡。

生产者&消费者

生产者将数据发布到相应的Topic,负责选择将哪个记录分发到Topic中的哪个Partition。例如可以round-robin方式完成此操作,然而这种仅是为了平衡负载。也可以根据某些语义分区功能(例如基于记录中的Key)进行此操作。
在这里插入图片描述
在消费者消费Topic中数据的时候,每个消费者会维护本次消费对应分区的偏移量,消费者会在消费完一个批次的数据之后,会将本次消费的偏移量提交给Kafka集群,因此对于每个消费者而言可以随意的控制改消费者的偏移量。因此在Kafka中,消费者可以从一个topic分区中的任意位置读取队列数据,由于每个消费者控制了自己的消费的偏移量,因此多个消费者之间彼此相互独立。

消费者使用Consumer Group名称标记自己,并且发布到Topic的每条记录都会传递到每个订阅Consumer Group中的一个消费者实例。消费者实例可以在单独的进程中或单独的机器上。

  • 如果所有Consumer实例都具有相同的Consumer Group,那么Topic中的记录会在该Consumer Group中的Consumer实例进行均分消费。
  • 如果所有Consumer实例具有不同的ConsumerGroup,则每条记录将广播到所有Consumer Group进程。

更常见的是,我们发现Topic具有少量的Consumer Group,每个Consumer Group可以理解为一个“逻辑的订阅者”。每个Consumer Group均由许多Consumer实例组成,以实现可伸缩性和容错能力。这无非就是发布-订阅模型,其中订阅者是消费者的集群而不是单个进程。

这种消费方式Kafka会将Topic按照分区的方式均分给一个Consumer Group下的实例,如果Consumer Group下有新的成员介入,则新介入的Consumer实例会去接管Consumer Group内其他消费者负责的某些分区,同样如果一下Consumer Group下的有其他Consumer实例宕机,则由该Consumer Group其他实例接管。

由于Kafka的Topic的分区策略,因此Kafka仅提供分区中记录的有序性,也就意味着相同Topic的不同分区记录之间无顺序。因为针对于绝大多数的大数据应用和使用场景, 使用分区内部有序或者使用key进行分区策略已经足够满足绝大多数应用场景。但是,如果您需要记录全局有序,则可以通过只有一个分区Topic来实现,尽管这将意味着每个ConsumerGroup只有一个Consumer进程。

顺序写&mmap&Zero Copy

Kafka的特性之一就是高吞吐率,但是Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,但是Kafka即使是普通的服务器,Kafka也可以轻松支持每秒百万级的写入请求,超过了大部分的消息中间件,这种特性也使得Kafka在日志处理等海量数据场景广泛应用。Kafka会把收到的消息都写入到硬盘中,防止丢失数据。为了优化写入速度Kafka采用了两个技术顺序写入和MMFile 。

  • 因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。这样省去了大量的内存开销以及节省了IO寻址的时间。但是单纯的使用顺序写入,Kafka的写入性能也不可能和内存进行对比,因此Kafka的数据并不是实时的写入硬盘中 。
  • Kafka充分利用了现代操作系统分页存储来利用内存提高I/O效率。Memory Mapped Files(后面简称mmap)也称为内存映射文件,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page实现文件到物理内存的直接映射。完成MMP映射后,用户对内存的所有操作会被操作系统自动的刷新到磁盘上,极大地降低了IO使用率。
  • Kafka服务器在响应客户端读取的时候,底层使用Zero Copy技术,直接将磁盘内容无需拷贝到用户空间,而是直接将数据通过内核空间传递输出,数据并没有抵达用户空间。

环境搭建

单机-CentOS 6

安装JDK1.8+:
rpm -qa | grep jdk 查看是否已安装jdk
rpm -ivh jdk-8u191-linux-x64.rpm 安装jdk,默认安装在/usr/java目录
rpm -e ‘rpm -qa | grep jdk’ 删除jdk
vi .bashrc 设置环境变量

JAVA_HPME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH

source .bashrc 加载环境变量
echo $JAVA_HOME 检测是否生效
配置主机名和IP映射:
rm -rf /etc/udev/rule.d/70-persistent-net.rules 克隆的主机需要执行这步
vi /etc/sysconfig/network 修改主机名,重启生效
vi /etc/hosts 配置主机名和IP映射关系
ping CentOS 检测映射是否正确
关闭防火墙&防火墙开机自启动:
service iptables status 查看防火墙状态
service iptables start 启动防火墙
service iptables stop 关闭防火墙
chkconfig --list 查看开机自启动应用
chkconfig iptables off 禁止开机自启动
安装&启动Zookeeper:
tar -zxf zookeeper-3.4.6.tar.gz -C /usr/ 解压到指定目录
cd /usr/zookeeper-3.4.6/
cp conf/zoo_sample.cfg conf/zoo.cfg 默认启动文件
vi conf/zoo.cfg 修改服务数据目录

dataDir=/root/zkdata

mkdir /root/zkdata 创建服务数据目录
./bin/zkServer.sh 查看可选指令
./bin/zkServer.sh start zoo.cfg 以指定文件启动
jps 查看java进程
./bin/zkServer.sh status zoo.cfg 查看zk状态
./bin/zkServer.sh stop zoo.cfg 关闭zk
** 安装&启动|关闭Kafka:**
tar -zxf kafka_2.11-2.2.0.tgz -C /usr/ 解压到指定目录
cd /usr/kafka_2.11-2.2.0/
vi config/server.properties

listeners=PLAINTEXT://CentOS:9092   #kafak底层监听地址
log.dirs=/usr/kafka-logs            #存储数据的目录
zookeeper.connect=CentOS:2181       #连接zk

./bin/kafka-server-start.sh -daemon config/server.properties 以后台方式启动
jps 查看java进程
ls /usr/kafka-logs/ 查看log

集群-CentOS 6

主机间SCP:
rm -rf .ssh
scp /etc/hosts CentOSB:/etc/
同步时钟:
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-6.repo
yum makecache
yum install ntp -y
ntpdate ntp1.aliyun.com
clock -w
配置集群:
vi /usr/zookeeper-3.4.6/conf/zoo.cfg 加入服务节点信息

server.1=CentOSA:2888:3888   #2888 数据同步端口 3888 主从选举端口
server.2=CentOSB:2888:3888
server.3=CentOSC:2888:3888

echo 1 > /root/zkdata/myid 每台机器对应唯一的id
vi /usr/kafka_2.11-2.2.0/config/server.properties

borker.id 
zookeeper.connect=CentOSA:2181,CentOSB:2181,CentOSC:2181

Topic管理

创建:./bin/kafka-topics.sh --bootstrap-server CentOS:9092 --create --topic topic01 --partitions 3 --replication-factor 3
查看:./bin/kafka-topics.sh --bootstrap-server CentOS:9092 --list
详情:./bin/kafka-topics.sh --bootstrap-server CentOS:9092 --describe --topic topic01
修改:./bin/kafka-topics.sh --bootstrap-server CentOS:9092 --alter --topic topic01 --partitions 2
删除:./bin/kafka-topics.sh --bootstrap-server CentOS:9092 --delete --topic topic01
生产:./bin/kafka-console-producer.sh --broker-list CentOS:9092 --topic topic01
订阅:./bin/kafka-console-consumer.sh --bootstrap-server CentOS:9092 --topic topic01 --group g1 --property print.key=true --property print.value=true --property key.separator=,
消费组:./bin/kafka-consumer-groups.sh --bootstrap-server CentOS:9092 --list g1
消费组:./bin/kafka-consumer-groups.sh --bootstrap-server CentOS:9092 --describe --group g1

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.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管理

//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信息
CreateTopicsResult createTopicsResult = adminClient.createTopics(Arrays.asList(new NewTopic("akuma02", 3, (short) 3)));
createTopicsResult.all().get(); // 同步创建

//查看Topic列表
ListTopicsResult topicsResult = adminClient.listTopics();
Set<String> names = topicsResult.names().get();
for (String name : names) {
    System.out.println(name);
}

// 删除Topic
DeleteTopicsResult deleteTopics = adminClient.deleteTopics(Arrays.asList("akuma02", "akuma01"));
deleteTopics.all().get(); // 同步删除

// 查看Topic详细信息
DescribeTopicsResult dtr = adminClient.describeTopics(Arrays.asList("akuma01"));
Map<String, TopicDescription> topicDescriptionMap = dtr.all().get();
for (Map.Entry<String, TopicDescription> entry : topicDescriptionMap.entrySet()) {
    System.out.println(entry.getKey() + "\t" + entry.getValue());
}

//关闭AdminClient
adminClient.close();

生产者

// 创建KafkaProducer
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(props);

for (int i = 0; i < 30; i++) {
    ProducerRecord<String, String> record = new ProducerRecord<>("akuma01", "key" + i, "value" + i);
    // 发送消息给服务器
    producer.send(record);
}
// 关闭生产者
producer.close();

消费者

// 创建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, "g1");

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

// 订阅相关的Topics
consumer.subscribe(Pattern.compile("^akuma.*"));
-----------------------------------------------------------------------------------------------
-List<TopicPartition> partitions = Arrays.asList(new TopicPartition("akuma01", 0));
-// 订阅相关的Topics,手动指定消费分区,失去组管理的特性
-consumer.assign(partitions);
-// 指定消费分区的位置
-//consumer.seekToBeginning(partitions);
-consumer.seek(new TopicPartition("akuma01", 0), 1);
-----------------------------------------------------------------------------------------------   
// 遍历消息队列
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 + "\t" + partition + "," + offset + "\t" + key + " " + value + " " + timestamp);
        }
    }
}

自定义分区

自定义分区策略

public class UserDefinePartitioner implements Partitioner {
    private AtomicInteger counter = new AtomicInteger(0);

    /**
     * 返回分区号
     */
    @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 andIncrement = counter.getAndIncrement();
            return (andIncrement & 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, ?> configs) {
        System.out.println("configure");
    }
}

在生产者的基础上

props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, UserDefinePartitioner.class.getName());

序列化

自定义序列化和反序列化类

public class UserDefineSerializer implements Serializer<Object> {
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        System.out.println("configure");
    }

    @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("configure");
    }

    @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, UserDefineSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserDefineSerializer.class.getName());
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, UserDefineDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDefineDeserializer.class.getName());

拦截器

定义拦截器

public class UserDefineProducerInterceptor implements ProducerInterceptor {
    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        return new ProducerRecord(record.topic(), record.key(), record.value() + " -- akuma");
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        System.out.println("metadata: " + metadata + ", exception: " + exception);
    }

    @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
earliest - 自动将偏移量重置为最早的偏移量
latest - 自动将偏移量重置为最新的偏移量
none - 如果未找到消费者组的先前偏移量,则向消费者抛出异常

Kafka消费者在消费数据的时候默认会定期的提交消费的偏移量,这样就可以保证所有的消息至少可以被消费者消费1次,用户可以通过以下两个参数配置:

enable.auto.commit = true 默认
auto.commit.interval.ms = 5000 默认

如果用户需要自己管理offset的自动提交,可以关闭offset的自动提交,手动管理offset提交的偏移量,注意用户提交的offset偏移量永远都要比本次消费的偏移量+1,因为提交的offset是kafka消费者下一次抓取数据的位置。

自动提交offset

// 默认是latest,如果系统没有消费者的偏移量,系统会读取最新的offset作为读取的位置
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
// earliest如果系统没有消费者的偏移量,系统会读取该分区最早的偏移量
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
// 配置offset自动提交的时间间隔,10s自动提交offset
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 10000);
// offset偏移量自动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);

手动提交offset

......
// 关闭offset偏移量自动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
......
// 在读取consumerRecords时
// 记录分区的消费元数据信息
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
......
// 记录消费分区的偏移量元数据,一定在提交的时候偏移量信息offset+1
offsets.put(new TopicPartition(topic, partition), new OffsetAndMetadata(offset+1));
// 提交消费者偏移量
consumer.commitAsync(offsets, new OffsetCommitCallback() {
    @Override
    public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
        System.out.println("offset:" + offsets + ",exception" + exception);
    }
});

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可以开启retries机制。

request.timeout.ms = 30000 默认
retries = 2147483647 默认

生产者设置Ack&Retries机制

// 设置kafka Acks以及retries
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 不包含第一次发送,如果尝试发送3次失败,则系统放弃发送。
props.put(ProducerConfig.RETRIES_CONFIG, 3);
// 将检测超时的时间设置为1ms
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 1);

消费者需关闭自动提交offset

幂等性

HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证上生产者发送的消息,不会丢失,而且不会重复。实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:

  • 唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识。
  • 记录下已处理过的请求标识:光有唯一标识还不够,还需要记录下那些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复记录,拒绝掉。

幂等又称为exactly once。要停止多次处理消息,必须仅将其持久化到Kafka Topic中仅仅一次。在初始化期间,kafka会给生产者生成一个唯一的ID称为Producer ID或PID。
PID和序列号与消息捆绑在一起,然后发送给Broker。由于序列号从零开始并且单调递增,因此,仅当消息的序列号比该PID / TopicPartition对中最后提交的消息正好大1时,Broker才会接受该消息。如果不是这种情况,则Broker认定是生产者重新发送该消息。

enable.idempotence= false 默认

注意:在使用幂等性的时候,要求必须开启retries=true和acks=all

生产者设置Idempotence机制

// 设置kafka Acks以及retries
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 不包含第一次发送,如果尝试发送3次失败,则系统放弃发送。
props.put(ProducerConfig.RETRIES_CONFIG, 3);
// 将检测超时的时间设置为1ms
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 1);
// 开启Kafka的幂等性
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 如果有1个发送不成功就阻塞
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);

消费者需关闭自动提交offset

事务控制

Kafka的幂等性,只能保证一条记录的在分区发送的原子性,但是如果要保证多条记录(多分区)之间的完整性,这个时候就需要开启kafk的事务操作。
在Kafka0.11.0.0除了引入的幂等性的概念,同时也引入了事务的概念。通常Kafka的事务分为 生产者事务Only、消费者&生产者事务。一般来说默认消费者消费的消息的级别是read_uncommited数据,这有可能读取到事务失败的数据,所有在开启生产者事务之后,需要用户设置消费者的事务隔离级别。

isolation.level = read_uncommitted 默认
该选项有两个值read_committed|read_uncommitted,如果开始事务控制,消费端必须将事务的隔离级别设置为read_committed

开启的生产者事务的时候,只需要指定transactional.id属性即可,一旦开启了事务,默认生产者就已经开启了幂等性。但是要求"transactional.id"的取值必须是唯一的,同一时刻只能有一个"transactional.id"存储在,其他的将会被关闭。

生产者事务Only

生产者

// 必须配置事务ID,必须是唯一的
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction-id" + UUID.randomUUID().toString());
// 配置Kafka批处理大小
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 1024);
// 等待5ms,如果batch中的数据不足1024
props.put(ProducerConfig.LINGER_MS_CONFIG, 5);
// 配置Kafka的重试机制和幂等性
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 20000);
......
// 初始化事务
producer.initTransactions();

try {
    // 开启事务
    producer.beginTransaction();

    for (int i = 0; i < 10; i++) {
    //      if (i == 8) {
    //          int j = 10 / 0;
    //      }
        ProducerRecord<String, String> record =
                new ProducerRecord<>("topic01", 1, "transaction" + i, "right data " + i);
        producer.send(record);
        producer.flush();
    }
    // 事务提交
    producer.commitTransaction();
} catch (Exception e) {
    System.out.println("出现错误~" + e.getMessage());
    // 终止事务
    producer.abortTransaction();
} finally {
    // 关闭生产者
    producer.close();
}

消费者

// 设置消费者的消费事务的隔离级别read_committed
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
// 设置消费者的消费事务的隔离级别read_committed
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_uncommitted");

消费者&生产者事务

public class KafkaProducerTransactionsProducerAndConsumer {
    public static void main(String[] args) {

        KafkaProducer<String, String> producer = buildKafkaProducer();
        KafkaConsumer<String, String> consumer = buildKafkaConsumer("g1");

        // 初始化事务
        producer.initTransactions();
        consumer.subscribe(Arrays.asList("topic01"));
        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            if (!consumerRecords.isEmpty()) {
                Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
                Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();

                // 开启事务控制
                producer.beginTransaction();
                try {
                    // 迭代数据,进行业务处理
                    while (recordIterator.hasNext()) {
                        ConsumerRecord<String, String> record = recordIterator.next();
                        // 储存元数据
                        offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
                        ProducerRecord<String, String> pRecord = new ProducerRecord<>("topic02", record.key(), record.value() + " ma edu onlinr");
                        producer.send(pRecord);
                    }
                    // 提交事务
                    producer.sendOffsetsToTransaction(offsets, "g1"); // 提交消费者的偏移量
                    producer.commitTransaction();
                } catch (Exception e) {
                    System.out.println("错误~ " + e.getMessage());
                    producer.abortTransaction();
                }
            }
        }
    }

    public static KafkaProducer<String, String> buildKafkaProducer() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 必须配置事务ID,必须是唯一的
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction-id" + UUID.randomUUID().toString());
        // 配置Kafka批处理大小
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 1024);
        // 等待5ms,如果batch中的数据不足1024
        props.put(ProducerConfig.LINGER_MS_CONFIG, 5);
        // 配置Kafka的重试机制和幂等性
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 20000);

        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        return producer;
    }

    public static KafkaConsumer<String, String> buildKafkaConsumer(String groupId) {
        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);

        // 设置消费者的消费事务的隔离级别read_committed
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
        // 必须关闭消费者端的offset自动提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        return consumer;
    }
}
发布了15 篇原创文章 · 获赞 0 · 访问量 374

猜你喜欢

转载自blog.csdn.net/weixin_44601009/article/details/104168626