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();
    }
}

相关推荐
虹科网络安全19 小时前
RDI 多数据源管道:把分散的数据汇入统一 Redis 数据层
经验分享·redis·艾体宝·艾体宝it·艾体宝redis
我不会起名字32220 小时前
Redis 从 0 到 1(三十七·收官):36 篇浓缩成 1 张知识地图 + 50 项生产 Checklist
redis·go·知识体系·checklist·收官
2601_967987371 天前
安装 Redis 或 Redis Stack
linux·windows·redis·macos·安装
aiks_yuan1 天前
MySQL GROUP BY 很慢怎么优化:加了索引反而从 1 秒变 7 秒,Using temporary 该不该消(MySQL 8.2 实测 400 万行)
mysql
小番茄程序猿1 天前
Redis 编码压测 59 轮:HSET 快 23 倍是假象,SMEMBERS 慢 30 倍是真的
redis·后端
数据库小学妹1 天前
MySQL大字段治理:行溢出机制、覆盖索引与拆表
数据库·mysql·性能优化
鶴哥只手遮天1 天前
《Redis 8.10.3 深度剖析》系列 ·第 13 章 Set:三种编码、集合运算与随机采样
redis·后端·开源
这个DBA有点耶1 天前
MySQL 8.0不可见索引:删索引怕出事,不删又拖慢写入——中间态怎么用?
数据库·mysql·代码规范
瑞克-桑切斯1 天前
redis
数据库·redis·缓存
噜哥带你玩转 AI 大模型1 天前
面试加分项!CASE WHEN 两种写法、CTE 公共表表达式、四大类窗口函数,老哥一次讲透
mysql·窗口函数·cte·sql 进阶