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