SparkStreaming 如何保证消费Kafka的数据不丢失不重复

目录

SparkStreaming接收Kafka数据的方式有两种:Receiver接收数据和采用Direct 方式。

(1)一个Receiver效率低,需要开启多个线程,手动合并数据再进行处理,并且Receiver方式为确保零数据丢失,需要开启WAL(预写日志)保证数据安全,这将同步保存所有收到的Kafka数据到HDFS,以便在发生故障时可以恢复所有数据。尽管WAL可以保证数据零丢失,但是不能保证exactly-once。Receivers接收完数据并保存到HDFS,但是在更新offset前,receivers失败了,Kafka以为数据没有接收成功,因为offset没有更新到ZooKeeper。随后receiver恢复了,从WAL可以读取的数据重新消费一次,因为使用Kafka高阶API,从ZooKeeper中保存的offsets开始消费

(2)Direct方式依靠checkpoint机制来保证。每次Spark Streaming消费了Kafka的数据后,将消费的Kafka offsets更新到checkpoint,消除了与ZooKeeper不一致的情况且可以从每个分区直接读取数据大大提高了并行能力。当你的程序挂掉或者升级的时候,就可以接着上次的读取,实现数据的零丢失。但是checkpoint太笨拙,因此可以自主管理offset,选取HBaseZooKeeperMySQLRedisKafka 等,保存对应topic下每个分区的offset,但是要注意当topic的新增分区的可能

  这里采用的方案:Spark Streamming使用Direct方式读取Kafka,利用数据库的事务, 将结果及offset一起写入到MySQL数据库中,保证是一个事务操作从而幂等性。

实现思路:
1)利用consumer api的seek方法可以指定offset进行消费,在启动消费者时查询数据库中记录的offset信息,如果是第一次启动,那么数据库中将没有offset信息,需要进行消费的元数据插入,然后从offset=0开始消费
2)关系型数据库具备事务的特性,当数据入库时,同时也将offset信息更新,借用关系型数据库事务的特性保证数据入库和修改offset记录这两个操作是在同一个事务中进行
3)使用ConsumerRebalanceListener来完成在分配分区时和Relalance时作出相应的处理逻辑

记录kafka信息表设计

create table kafka_info(
	topic_group_partition varchar(32) primary key, //主题+组名+分区号 这里冗余设计方便通过这个主键进行更新提升效率 
	topic_group varchar(30), //主题和组名
	partition_num tinyint,//分区号
	offsets bigint default 0 //offset信息
);

代码实现

import com.alibaba.fastjson.JSON;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.util.*;
public class AccurateConsumer {
   
    private static final Properties props = new Properties();
    private static final String GROUP_ID = "Test";
    static {
   
        props.put("bootstrap.servers", "192.168.142.139:9092");
        props.put("group.id", GROUP_ID);
        props.put("enable.auto.commit", false);//注意这里设置为手动提交方式
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    }
    final KafkaConsumer<String, String> consumer;
    //用于记录每次消费时每个partition的最新offset
    private Map<TopicPartition, Long> partitionOffsetMap;
    //用于缓存接受消息,然后进行批量入库
    private List<Message> list;
    private volatile boolean isRunning = true;
    private final String topicName;
    private final String topicNameAndGroupId;
    public AccurateConsumer(String topicName) {
   
        this.topicName = topicName;
        topicNameAndGroupId = topicName + "_" + GROUP_ID;
        consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList(topicName), new HandleRebalance());
        list = new ArrayList<>(100);
        partitionOffsetMap = new HashMap<>();
    }
    //这里使用异步提交和同步提交的组合方式
    public void receiveMsg() {
   
        try {
   
            while (isRunning) {
   
                ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
                if (!consumerRecords.isEmpty()) {
   
                    for (TopicPartition topicPartition : consumerRecords.partitions()) {
   
                        List<ConsumerRecord<String, String>> records = consumerRecords.records(topicPartition);
                        for (ConsumerRecord<String, String> record : records) {
   
                            //使用fastjson将记录中的值转换为Message对象,并添加到list中
                            list

你可能感兴趣的:(Spark,Spark)