Flink 流计算进阶:手写一套通用的 Redis/MySQL /Kafka数据源封装工具类

一、redis篇幅

1、解决痛点

1、兼容redis集群和单机

2、异步IO,通过 AsyncDataStream.unorderedWait 提升吞吐量

3、通用性,避免重复造轮子


2、代码部分

复制代码
RedisClusterUtil工具类
java 复制代码
package com.juxin.util.redis;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.juxin.util.ConfigUtil;
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.time.Duration;
import java.util.Map;

/**
 * 功能: redis工具类
 * 弊端: 同步阻塞,若自创异步会导致还没拿到数据,流已经往下执行了
 *
 */
@Slf4j
public class RedisClusterUtil implements Serializable {
    private static final long serialVersionUID = 1L;//版本校验,Flink忽略类变化,强制读取ck旧数据

    private transient RedisClusterClient clusterClient; //跳过不序列化
    private transient StatefulRedisClusterConnection<String, String> connection;
    private transient RedisAdvancedClusterCommands<String, String> syncCmd; //同步
    // 新增:异步命令接口
    private transient RedisAdvancedClusterAsyncCommands<String, String> asyncCmd;


    // ✅ 初始化方法,由 Flink 的 open() 调用
    public void init() {
        if (clusterClient != null) {return;} // 已初始化,直接返回
        try {
            String host = ConfigUtil.get("redis.cluster.host");
            int port = NumberUtil.parseInt(ConfigUtil.get("redis.cluster.port"));
            if(!StrUtil.isEmptyIfStr(host) && port > 0){ //非空
                String password = ConfigUtil.get("redis.cluster.password");
                RedisURI uri = RedisURI.builder()
                        .withHost(host)
                        .withPort(port)
                        .withPassword(password.toCharArray())
                        .withTimeout(Duration.ofSeconds(3))
                        .build();
                clusterClient = RedisClusterClient.create(uri);
                connection = clusterClient.connect();
                syncCmd = connection.sync();
                asyncCmd = connection.async();   // 获取异步命令
            }
        } catch (Exception e) {
            throw new RuntimeException("Redis连接失败: " + e.getMessage(), e);
        }

    }
    // 提供获取异步命令的方法(或者直接公开 asyncCmd,但最好用 getter)
    public RedisAdvancedClusterAsyncCommands<String, String> getAsyncCmd() {
        return asyncCmd;
    }

    //获取
    public String get(String key) {
        try {
            return syncCmd.get(key);
        } catch (Exception e) {
            log.error("Redis get失败, key={}", key, e);
            return null;
        }
    }
    //设置
    public void set(String key, String value) {
        try {
            syncCmd.set(key, value);
        } catch (Exception e) {
            log.error("Redis set失败, key={}, value={}", key, value, e);
            throw new RuntimeException(e); // 必须成功就抛
        }
    }
    //批量设置
    public void mset(Map<String, String> kvMap) {
        try {
            syncCmd.mset(kvMap);
        } catch (Exception e) {
            log.error("Redis批量set失败, size={}", kvMap.size(), e);
            throw new RuntimeException(e);
        }
    }
    public void delete(String key) {
        try {
            syncCmd.del(key);
        } catch (Exception e) {
            log.error("Redis del失败, key={}", key);
            throw new RuntimeException(e); // 必须成功就抛
        }
    }

    // ✅ 关闭方法,由 Flink 的 close() 调用
    public void close() {
        try {
            if (connection != null) {
                connection.close();
            }
        } catch (Exception e) {
            log.error("关闭connection失败", e);
        }
        try {
            if (clusterClient != null) {
                clusterClient.shutdown();
            }
        } catch (Exception e) {
            log.error("关闭clusterClient失败", e);
        }
    }
}
复制代码
使用 Lettuce 异步客户端,通过 AsyncDataStream.unorderedWait 提升吞吐量
java 复制代码
package com.learnSelf.A01_redis;
import com.juxin.util.redis.RedisClusterUtil;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * Flink 异步 I/O 集成 Redis 示例(无序等待)
 * 使用 Lettuce 异步客户端,通过 AsyncDataStream.unorderedWait 提升吞吐量。
 */
public class RedisAsyncTest {

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

        // 2. 模拟输入数据(Redis 的 key)
        DataStream<String> keyStream = env.fromElements("redisKey1", "redisKey2", "redisKey3", "redisKey4");

        // 3. 应用异步 I/O 操作(无序等待)
        DataStream<Tuple2<String, String>> resultStream = AsyncDataStream.unorderedWait(
                keyStream,                          // 输入流
                new AsyncRedisGetFunction(),        // 异步函数实例
                1000L,                              // 超时时间(毫秒)
                TimeUnit.MILLISECONDS,              // 时间单位
                10                                  // 最大并发请求数(容量)
        );

        // 4. 打印结果(输出顺序与输入顺序无关,谁先返回谁先输出)
        resultStream.print();

        // 5. 执行作业
        env.execute("Redis 异步 I/O 测试");
    }
}

/**
 * 自定义异步函数:通过 Lettuce 异步客户端查询 Redis,返回 (key, value) 元组。
 * 必须继承 RichAsyncFunction 以获取生命周期(open/close)管理资源。
 */
class AsyncRedisGetFunction extends RichAsyncFunction<String, Tuple2<String, String>> {

    // transient 避免序列化(Flink 分发时传递),在 open 中重新初始化
    private transient RedisClusterUtil redisUtil;

    /**
     * 初始化方法:每个并行子任务启动时调用一次,创建 Redis 连接。
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        redisUtil = new RedisClusterUtil();
        redisUtil.init(); // 内部创建集群连接并获取异步命令接口
    }

    /**
     * 异步调用核心逻辑:对每条输入 key 发起非阻塞 Redis GET 请求。
     * @param key           输入 key
     * @param resultFuture  回调容器,用于提交结果或异常
     */
    @Override
    public void asyncInvoke(String key, ResultFuture<Tuple2<String, String>> resultFuture) throws Exception {
        // 获取 Lettuce 异步命令对象(线程安全)
        RedisAdvancedClusterAsyncCommands<String, String> asyncCmd = redisUtil.getAsyncCmd();

        // 发起异步 GET 请求,立即返回 RedisFuture(CompletionStage 的子类)
        RedisFuture<String> future = asyncCmd.get(key);

        // 注册成功回调:当 Redis 返回结果后,将 (key, value) 提交给 Flink
        future.thenAccept(value -> {
            // value 可能为 null(key 不存在),仍保留 null 传递给下游
            Tuple2<String, String> result = Tuple2.of(key, value);
            resultFuture.complete(Collections.singleton(result)); // 单条结果
        }).exceptionally(throwable -> {
            // 注册异常回调:将异常传递给 Flink,作业会根据配置决定是否失败
            resultFuture.completeExceptionally(throwable);
            return null;
        });
    }

    /**
     * 超时处理方法:当异步操作超过规定时间未完成时触发。
     * 此处返回一个包含 "TIMEOUT" 标记的默认值,避免作业卡死。
     */
    @Override
    public void timeout(String key, ResultFuture<Tuple2<String, String>> resultFuture) throws Exception {
        resultFuture.complete(Collections.singleton(Tuple2.of(key, "TIMEOUT")));
    }

    /**
     * 清理资源:每个并行子任务关闭时调用,释放 Redis 连接。
     */
    @Override
    public void close() throws Exception {
        if (redisUtil != null) {
            redisUtil.close(); // 关闭连接和客户端
        }
        super.close();
    }
}

相关推荐
马优晨5 小时前
Spring Boot + MyBatis + Redis 整合实战 —— 项目源码深度解析
spring boot·redis·mybatis·mybatis + redis·redis实战·spring实战·mybatis实战
ShiXZ21310 小时前
Redis 常用指令全集:redis-cli 实战速查手册
数据库·redis·缓存
老杨聊技术15 小时前
CentOS 7 安装 MySQL 8 保姆级教程
linux·mysql·centos
sunxr.22715 小时前
Mysql-----最后一次作业
数据库·mysql
古月方枘Fry1 天前
基于大模型+MySQL的innoai助手(可适配多数环境)
网络·数据库·mysql·aigc
为你学会写情书1 天前
Redis 从入门到实战:一篇搞定核心数据结构与 NestJS 集成
redis
无忧.芙桃1 天前
MySQL数据库原理与实践(四):基本查询
大数据·数据库·mysql
三8441 天前
get方法/post方法/SQL注入文字型/数字型
数据库·sql·mysql
啦啦啦啦啦zzzz1 天前
oat++框架应用之do、dao、service
服务器·c++·mysql·oatpp