消费MySQL数据 不重复消费_解决Flink消费Kafka信息,结果存储在Mysql的重复消费问题...

背景

最近项目中使用Flink消费kafka消息,并将消费的消息存储到mysql中,看似一个很简单的需求,在网上也有很多flink消费kafka的例子,但看了一圈也没看到能解决重复消费的问题的文章,于是在flink官网中搜索此类场景的处理方式,发现官网也没有实现flink到mysql的Exactly-Once例子,但是官网却有类似的例子来解决端到端的仅一次消费问题。这个现成的例子就是FlinkKafkaProducer011这个类,它保证了通过FlinkKafkaProducer011发送到kafka的消息是Exactly-Once的,主要的实现方式就是继承了TwoPhaseCommitSinkFunction这个类,关于TwoPhaseCommitSinkFunction这个类的作用可以先看上一篇文章:https://blog.51cto.com/simplelife/2401411。

实现思想

这里简单说下这个类的作用就是实现这个类的方法:beginTransaction、preCommit、commit、abort,达到事件(preCommit)预提交的逻辑(当事件进行自己的逻辑处理后进行预提交,如果预提交成功之后才进行真正的(commit)提交,如果预提交失败则调用abort方法进行事件的回滚操作),结合flink的checkpoint机制,来保存topic中partition的offset。

达到的效果我举个例子来说明下:比如checkpoint每10s进行一次,此时用FlinkKafkaConsumer011实时消费kafka中的消息,消费并处理完消息后,进行一次预提交数据库的操作,如果预提交没有问题,10s后进行真正的插入数据库操作,如果插入成功,进行一次checkpoint,flink会自动记录消费的offset,可以将checkpoint保存的数据放到hdfs中,如果预提交出错,比如在5s的时候出错了,此时Flink程序就会进入不断的重启中,重启的策略可以在配置中设置,当然下一次的checkpoint也不会做了,checkpoint记录的还是上一次成功消费的offset,本次消费的数据因为在checkpoint期间,消费成功,但是预提交过程中失败了,注意此时数据并没有真正的执行插入操作,因为预提交(preCommit)失败,提交(commit)过程也不会发生了。等你将异常数据处理完成之后,再重新启动这个Flink程序,它会自动从上一次成功的checkpoint中继续消费数据,以此来达到Kafka到Mysql的Exactly-Once。

具体实现代码三个类

StreamDemoKafka2Mysql.javaimport org.apache.flink.runtime.state.filesystem.FsStateBackend;

import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.flink.streaming.api.CheckpointingMode;

import org.apache.flink.streaming.api.datastream.DataStreamSource;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011;

import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumerBase;

import org.apache.flink.streaming.util.serialization.JSONKeyValueDeserializationSchema;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import java.util.Properties;

/**

* Created with IntelliJ IDEA.

* User: zzy

* Date: 2019/5/28

* Time: 8:40 PM

* To change this template use File | Settings | File Templates.

*

* 消费kafka消息,sink(自定义)到mysql中,保证kafka to mysql 的Exactly-Once

*/

@SuppressWarnings("all")

public class StreamDemoKafka2Mysql {

private static final String topic_ExactlyOnce = "mysql-exactly-Once-4";

public static void main(String[] args) throws Exception {

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

//设置并行度,为了方便测试,查看消息的顺序,这里设置为1,可以更改为多并行度

env.setParallelism(1);

//checkpoint的设置

//每隔10s进行启动一个检查点【设置checkpoint的周期】

env.enableCheckpointing(10000);

//设置模式为:exactly_one,仅一次语义

env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

//确保检查点之间有1s的时间间隔【checkpoint最小间隔】

env.getCheckpointConfig().setMinPauseBetweenCheckpoints(1000);

//检查点必须在10s之内完成,或者被丢弃【checkpoint超时时间】

env.getCheckpointConfig().setCheckpointTimeout(10000);

//同一时间只允许进行一次检查点

env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);

//表示一旦Flink程序被cancel后,会保留checkpoint数据,以便根据实际需要恢复到指定的checkpoint

//env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

//设置statebackend,将检查点保存在hdfs上面,默认保存在内存中。这里先保存到本地

// env.setStateBackend(new FsStateBackend("file:///Users/temp/cp/"));

//设置kafka消费参数

Properties props = new Properties();

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "zzy:9092");

props.put(ConsumerConfig.GROUP_ID_CONFIG, "flink-consumer-group2");

//kafka分区自动发现周期

props.put(FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS, "3000");

/*SimpleStringSchema可以获取到kafka消息,JSONKeyValueDeserializationSchema可以获取都消息的key,value,metadata:topic,partition,offset等信息*/

FlinkKafkaConsumer011 kafkaConsumer011 = new FlinkKafkaConsumer011<>(topic_ExactlyOnce, new JSONKeyValueDeserializationSchema(true), props);

//加入kafka数据源

DataStreamSource streamSource = env.addSource(kafkaConsumer011);

// System.out.println("streamSource:" + streamSource.print());

streamSource.print();

//数据传输到下游

streamSource.addSink(new MySqlTwoPhaseCommitSink()).name("MySqlTwoPhaseCommitSink");

//触发执行

env.execute(StreamDemoKafka2Mysql.class.getName());

}

}

2.MySqlTwoPhaseCommitSink.java

import org.apache.flink.api.common.ExecutionConfig;

import org.apache.flink.api.common.typeutils.TypeSerializer;

import org.apache.flink.api.common.typeutils.base.VoidSerializer;

import org.apache.flink.api.java.typeutils.runtime.kryo.KryoSerializer;

import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.flink.streaming.api.functions.sink.SinkFunction;

import org.apache.flink.streaming.api.functions.sink.TwoPhaseCommitSinkFunction;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.Timestamp;

import java.text.SimpleDateFormat;

import java.util.Date;

/**

* Created with IntelliJ IDEA.

* User: zzy

* Date: 2019/5/28

* Time: 8:47 PM

* To change this template use File | Settings | File Templates.

*

* 自定义kafka to mysql,继承TwoPhaseCommitSinkFunction,实现两阶段提交

*/

public class MySqlTwoPhaseCommitSink extends TwoPhaseCommitSinkFunction {

private static final Logger log = LoggerFactory.getLogger(MySqlTwoPhaseCommitSink.class);

public MySqlTwoPhaseCommitSink(){

super(new KryoSerializer<>(Connection.class,new ExecutionConfig()), VoidSerializer.INSTANCE);

}

/**

* 执行数据库入库操作 task初始化的时候调用

* @param connection

* @param objectNode

* @param context

* @throws Exception

*/

@Override

protected void invoke(Connection connection, ObjectNode objectNode, Context context) throws Exception {

log.info("start invoke...");

String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

log.info("===>date:" + date + " " + objectNode);

log.info("===>date:{} --{}",date,objectNode);

String value = objectNode.get("value").toString();

log.info("objectNode-value:" + value);

JSONObject valueJson = JSONObject.parseObject(value);

String value_str = (String) valueJson.get("value");

String sql = "insert into `mysqlExactlyOnce_test` (`value`,`insert_time`) values (?,?)";

PreparedStatement ps = connection.prepareStatement(sql);

ps.setString(1,value_str);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值