目录
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,选取HBase、ZooKeeper、MySQL、Redis或Kafka 等,保存对应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.addAll(JSON.parseArray

本文探讨了SparkStreaming从Kafka获取数据的两种方式:Receiver和Direct。Receiver通过WAL保证数据零丢失但可能导致重复,而Direct方式借助checkpoint实现零丢失但需外部管理offset。解决方案是利用Consumer API的seek方法结合数据库事务,确保数据入库与offset更新的原子性,并通过ConsumerRebalanceListener处理分区重分配。
最低0.47元/天 解锁文章
965

被折叠的 条评论
为什么被折叠?



