Flink-Exactly-once一致性系列实践3

本文介绍如何使用 Apache Flink 实现 Exactly-once 一致性级别,具体演示了 Kafka 数据流到 Redis 的处理流程,包括自定义 TwoPhaseCommitSinkFunction 和解决过程中遇到的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Flink-Exactly-once系列实践-KafkaToRedis


前言

这里我们紧接着更新flink一致性的另一个板块也就是KafkaToRedis,本节我们依然是实现了自定义的两阶段提交,下面介绍我们整个实现的步骤。


一、Redis的事务性

redis实现事务可以通过jedis来使用multi()方法获得当前创建的一个事务,对于redis来说,事务并不具有隔离性和原子性,他更看起来是相当于一连串的命令按照顺序进入队列之后再顺序执行,当其中有失败的时候,依然是可以提交的,但是我们也可以discard,也就是取消事务。

二、编写RedisUtil

代码如下(示例):

public class RedisUtil {

    public static JedisPool jedisPool=null;

    public static JedisPoolConfig jedisPoolConfig;

    //确保拿到的jedis连接是唯一的,从而完成事务 不加入序列化
    private final transient Jedis jedis;

    private  transient Transaction jedisTransaction;
    //JedisPool配置类提前加载
    static {
        jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(100); //最大可用连接数
        jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
        jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
        jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
        jedisPoolConfig.setMinIdle(5); //最小闲置连接数
        jedisPoolConfig.setTestOnBorrow(false); //取连接的时候进行一下测试 pingpong
    }

    //无参构造
    public RedisUtil() throws IOException {
        InputStream in = RedisUtil.class.getClassLoader().getResourceAsStream("redis.properties");
        Properties properties = new Properties();
        properties.load(in);
        String port = properties.getProperty("redis.port");
        String timeout = properties.getProperty("redis.timeout");
        jedisPool = new JedisPool(jedisPoolConfig, properties.getProperty("redis.host"), Integer.parseInt(port), Integer.parseInt(timeout));
        //System.out.println("开辟连接池");
        jedis = jedisPool.getResource();
        jedis.auth("root");
    }

    //获取jedis
    public Transaction getTransaction(){
        if(this.jedisTransaction==null) {
            jedisTransaction = this.jedis.multi();
            System.out.println("========"+jedisTransaction);
            System.out.println(jedisTransaction);
        }
        return this.jedisTransaction;
    }

    public Jedis getjedis(){
        return this.jedis;
    }

    public void setjedisTransactionIsNull(){
        this.jedisTransaction=null;
    }
}

三、编写RedisExactlySink

代码如下(示例):

public class RedisExactlySink<T> extends TwoPhaseCommitSinkFunction<T, RedisUtil,Void> {
    //定义redis hash表名
    public static final String REDIS_HASH_MAP="WordAndWordCount";

    public static RedisUtil redisUtil;

    static {
        try {
            redisUtil = new RedisUtil();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //继承父类的构造参数,因为要初始化父类的内容
    public RedisExactlySink(){
        super(new KryoSerializer<>(RedisUtil.class,new ExecutionConfig()), VoidSerializer.INSTANCE);
    }

    @Override
    protected void invoke(RedisUtil transaction, T value, Context context) throws Exception {
        Transaction jedis = transaction.getTransaction();//拿到事务连接
        System.out.println(jedis);
        Class<?> aClass = value.getClass();//获取class
        Field[] fields = aClass.getDeclaredFields();//获取属性字段
        fields[0].setAccessible(true);
        fields[1].setAccessible(true);
        Object object1=fields[0].get(value);
        if(object1.toString().equals("error")){
            throw new RuntimeException("主动触发异常!!");
        }
        Object object2=fields[1].get(value);
        System.out.println("写入redis HashMap ");
        jedis.hset(REDIS_HASH_MAP,object1.toString(),object2.toString());
    }

    @Override
    protected RedisUtil beginTransaction() throws Exception {
        return redisUtil;
    }

    @Override
    protected void preCommit(RedisUtil transaction) throws Exception {
        System.out.println("正在执行预提交!!!");
    }

    @Override
    protected void commit(RedisUtil transaction) {
        Transaction jedistransaction = transaction.getTransaction();
        System.out.println(jedistransaction);
        try {
            System.out.println("事务提交");
            jedistransaction.exec();
            redisUtil.setjedisTransactionIsNull();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("jedis close!!!");
        }
    }

    @Override
    protected void abort(RedisUtil transaction) {
        Transaction jedistransaction = transaction.getTransaction();
        System.out.println(jedistransaction);
        try {
            System.out.println("取消事务");
            jedistransaction.discard();
            redisUtil.setjedisTransactionIsNull();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("jedis close!!!");
        }
    }
}

可以看到这里我们依然是继承了TwoPhaseCommitSinkFunction的方法
这个方法之前的文章介绍过,这里不做详细介绍。


四、编写主测类,实现单词统计并且写入Redis

public static void main(String[] args) throws Exception {
        //1.获取流式执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //2.设置并行度以及jedis的序列化
        env.setParallelism(1);
        env.getConfig().addDefaultKryoSerializer(Jedis.class, TBaseSerializer.class);

        //3.设置CheckPoint以及StateBackend
        CkAndStateBacked.setCheckPointAndStateBackend(env,"FS");

        //4.获取Kafka输入流
        InputStream in = KafkaToRedis.class.getClassLoader().getResourceAsStream("kafka.properties");
        ParameterTool parameterTool=ParameterTool.fromPropertiesFile(in);
        SimpleStringSchema simpleStringSchema = new SimpleStringSchema();
        Class<? extends SimpleStringSchema> aClass = simpleStringSchema.getClass();
        DataStream<String> kafkaDataStream = KafkaUtil.getKafkaDataStream(parameterTool, aClass, env);

        //5.map包装数据为value,1
        SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = kafkaDataStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                return new Tuple2<>(value, 1);
            }
        });

        //6.mapStream进行keyby并且聚合
        SingleOutputStreamOperator<Tuple2<String, Integer>> reduceStream = mapStream.keyBy(data -> data.f0)
                .reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                        return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
                    }
                });

        //7.reduceStream包装成POJO类
        SingleOutputStreamOperator<Pojo> pojoStream = reduceStream.map(data -> {
            Pojo pojo = new Pojo(data.f0, data.f1);
            return pojo;
        });

        //8.pojoStream输出到redis,这里以Hash表的形式类似
        // WordAndWordCount java 1 python 1
        pojoStream.addSink(new RedisExactlySink<Pojo>());

        //9.任务执行
        env.execute();
    }

五、测试过程以及图示

5.1启动redis,查看数据库

在这里插入图片描述

5.2启动kafka,创建生产者产生数据

在这里插入图片描述

5.3启动主程序,并且kafka输入数据

在这里插入图片描述
在这里插入图片描述
可以看到,我们这里设置的CheckPoint的时间间隔是20秒做一次,也就是每次CheckPoint,间隔内的这一段都相当于是同一段事务,要么成功,要么失败。之后我们输入了error,主动触发了异常,如图并没有写进去。
在这里插入图片描述

六、过程中遇到的BUG解决

6.1 “Could not get a resource since the pool is exhausted”

这里意思是因为资源池资源耗尽,无法从中获取到redis连接,对应这一步

jedis = jedisPool.getResource();

但是我这里并没有考虑并发,因此通过查询发现是

jedisPoolConfig.setTestOnBorrow(false); //取连接的时候进行一下测试 pingpong

如果设置为true就需要将redis和你的程序放到同一台机器上或者同一局域网上面或者关闭该模式。所以这里我们只需要将true改为false后解决这个问题。

6.2 “ERR EXEC without MULTI”

这里意思是,我们获取到jedis连接并没有开启事务,然后我们却执行了exec(),或者discard,这里是因为对于20秒内的CheckPoint的间隔内,其实我们是把jedis固定了然后,我们并且调用了如图的方法

    //获取jedis
    public Transaction getTransaction(){
        if(this.jedisTransaction==null) {
            jedisTransaction = this.jedis.multi();
            System.out.println("========"+jedisTransaction);
            System.out.println(jedisTransaction);
        }
        return this.jedisTransaction;
    }

这里当下次CheckPoint的时候,也就是下一个20秒之间,从这里获取连接就有问题了,因为我们拿到的还是jedis创建的jedisTransaction ,还是上一次的 事务连接,但是,我们上次CheckPoint结束之前,成功的化其实已经执行了

jedistransaction.exec();

这个步骤看源码会发现,他已经把当前连接里面的事务标志位,置为false

this.inTransaction = false;

然而我们拿到的还是上次这个已经没有开启事务的连接,从而也意味着只有第一次可以成功,后面就失败了,解决的方法就是再每次我们coomit或者abort的时候,我们紧接着调用一个将此事务jedis连接置空的方法,保证下次拿到的事务jedis连接是一个新的连接就ok了

@Override
    protected void commit(RedisUtil transaction) {
        Transaction jedistransaction = transaction.getTransaction();
        System.out.println(jedistransaction);
        try {
            System.out.println("事务提交");
            jedistransaction.exec();
            //这里调用一个方法,重置了事务连接
            redisUtil.setjedisTransactionIsNull();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("jedis close!!!");
        }
    }

调用的是如下方法。

    public void setjedisTransactionIsNull(){
        this.jedisTransaction=null;
    }

6.3 “Committing one of transactions failed, logging first encountered failure”

这个意思是提交其中一个事务失败,日志记录第一次遇到失败,这里的原因跟踪发现报错位置再之前我每次提交或者abort的时候,每次finally里面我都会写close();

finally {
            System.out.println("jedis close!!!");
            jedistransaction.close();
        }

后来发现,其实没必要再这里关闭的,对于事务连接不需要做这种操作,因此都去掉之后就一切便正常了!

总结

以上,就是Flink-Exactly-once系列实践-KafkaToRedis的大体实现了,包括整个代码以及验证,以及过程中遇到的BUG解决,有不足的地方还请见谅!

<think>嗯,用户问的是Flink如何保证Exactly-once语义。我需要先回忆一下Flink的相关知识。Exactly-once是流处理中非常重要的特性,确保每条数据只被处理一次,即使在故障恢复时也是如此。 首先,Flink的检查点机制应该是关键。检查点基于Chandy-Lamport算法,定期保存作业状态。每个检查点会插入barrier到数据流中,barrier将流分成不同的窗口,当所有任务都确认状态保存后,检查点才算完成。这样在故障恢复时,可以从最近的检查点重启,保证状态的一致性。 然后,可能需要提到状态后端,比如RocksDB,它支持异步快照,减少对处理性能的影响。不过状态后端具体是存储部分,可能和Exactly-once的实现关系不大,但检查点的持久化存储确实重要。 另外,两阶段提交协议(2PC)在Flink中的应用。比如在连接外部系统(如Kafka)时,Flink通过TwoPhaseCommitSinkFunction实现精确一次输出。这涉及到预提交和提交两个阶段,确保数据要么完全提交,要么回滚,避免部分写入。 还需要区分Exactly-once的不同层次:Flink内部的状态一致性,以及端到端的精确一次。内部通过检查点和恢复机制,而端到端需要外部系统的协同,比如支持事务的Sink。 可能用户需要具体的步骤或流程图说明。比如检查点的触发、barrier对齐、状态快照,故障恢复时的状态回滚和重放数据。这时候分步骤解释会更清晰。 还要注意不同版本的Flink是否有改进,比如在Aligned Checkpoint和Unaligned Checkpoint的区别,后者在反压严重时也能保证生成检查点,但可能会影响Exactly-once的保证吗?不过用户可能不需要这么深入,但可能需要简单提及。 另外,可能用户对Exactly-once和At-least-once的区别有疑惑,可以简要比较,但用户的问题主要关注Exactly-once的实现,所以重点应在Flink的机制。 总结下来,回答的结构应该是:先介绍Exactly-once的重要性,然后分检查点机制和两阶段提交,再讲内部和端到端的保证,可能最后举例说明,比如Kafka到Flink再到Kafka的案例。需要确保用中文,行内公式正确,结构清晰。</think>Flink 通过**分布式快照检查点(Checkpoint)**和**两阶段提交协议(2PC)**的结合实现端到端的 Exactly-once 语义。以下是其核心机制的分步解析: --- ### 一、Checkpoint 机制:保证内部状态一致性 1. **触发检查点** JobManager 周期性触发检查点(如每 10 秒),向 Source 节点插入**特殊标记(Barrier)**到数据流中,将数据流逻辑切分为窗口。 2. **Barrier 对齐** - 每个算子收到 Barrier 后暂停处理后续数据,**先缓存输入流的数据**。 - 当所有输入流的 Barrier 都到达时,算子将当前状态**异步持久化**到可靠存储(如 HDFS/S3)。 - 公式表示状态保存过程: $$ S_i = f(S_{i-1}, D_{window}) $$ 其中 $S_i$ 为第 $i$ 次检查点状态,$D_{window}$ 为当前窗口数据。 3. **恢复机制** 故障时,Flink 回滚到最近完整的检查点,从持久化状态重新处理后续数据,确保状态与数据流完全一致。 --- ### 二、端到端 Exactly-once:两阶段提交协议(2PC) 对于外部系统(如 Kafka、数据库),Flink 通过 **`TwoPhaseCommitSinkFunction`** 实现事务性写入: | 阶段 | 行为 | |---------------------|----------------------------------------------------------------------| | **1. 预提交(Pre-commit)** | Sink 将数据写入外部系统,但标记为**未提交**(如 Kafka 事务未提交)。 | | **2. 提交(Commit)** | 当 Checkpoint 完成时,JobManager 通知所有算子提交事务。 | | **回滚(Rollback)** | 若 Checkpoint 失败,事务自动回滚,外部系统丢弃未提交数据。 | --- ### 三、关键设计优化 1. **精确一次 vs 至少一次** - *Exactly-once*:通过 Barrier 对齐和事务提交确保一致性- *At-least-once*:Barrier 不对齐时可能重复处理数据。 2. **反压处理** - **Unaligned Checkpoint**(Flink 1.11+):允许 Barrier 跨越缓存数据,避免反压导致检查点超时。 3. **端到端场景示例** ```text Kafka → Flink → Kafka └── Source 记录消费偏移量(状态) └── Sink 开启 Kafka 事务写入 └── Checkpoint 成功时提交偏移量和事务 ``` --- ### 四、适用场景 - **高一致性要求**:金融交易、计费系统。 - **高吞吐场景**:Checkpoint 间隔需权衡吞吐和恢复时间。 通过以上机制,Flink 在分布式环境下实现了**状态一致性**和**端到端数据精确处理**,成为流处理领域的核心优势之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值