Flink实现自定义RedisSink保存字节数组类型的数据

Flink 的 Redis 依赖 flink-connector-redis 里默认实现只能保存 String 类型的数据, 但很多时候开发需要保存更多其他类型的数据, 比如保存 ProtoBuf 数据的时候会更多选择将 ProtoBuf 对象转换成字节数组进行保存. 所以这里会简单实现自定义 RedisSink 保存字节数组的代码.

依赖

<dependency>
    <groupId>org.apache.bahir</groupId>
    <artifactId>flink-connector-redis_2.11</artifactId>
    <version>1.1-SNAPSHOT</version>
</dependency>

实现时为了方便直接将所有的类都放到同一个java文件

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.Function;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommand;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommandDescription;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;

import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * Flink 任务
 */
public class SinkRedis {

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

        // Kafka Source 配置
        KafkaSource<String> source = KafkaSource.<String>builder()
                .setBootstrapServers("127.0.0.1:9092")
                .setTopics("test")
                .setGroupId("SourceKafka")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
                .setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000")
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .build();

        // Redis Sink 配置
        MyRedisSink<String> redisSink = new MyRedisSink<>(
                "JedisPool",
                new MyRedisMapper<String>() {
                    // 保存数据的命令
                    @Override
                    public RedisCommandDescription getCommandDescription() {
                        return new RedisCommandDescription(RedisCommand.SET);
                    }
                    // 键值对的key
                    @Override
                    public byte[] getKeyFromData(String data) {
                        return data.split(" ")[0].getBytes(StandardCharsets.UTF_8);
                    }
                    // 键值对的value
                    @Override
                    public byte[] getValueFromData(String data) {
                        return data.split(" ")[1].getBytes(StandardCharsets.UTF_8);
                    }
                }
        );

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        env.fromSource(source, WatermarkStrategy.noWatermarks(), "kafka").addSink(redisSink);

        env.execute();
    }
}

/**
 * 自定义实现RedisSink, IN是输入数据的类型, 输出数据的类型是字节数组
 */
class MyRedisSink<IN> extends RichSinkFunction<IN> {

    private static final long serialVersionUID = 1L;
    private static final Logger LOG = LoggerFactory.getLogger(MyRedisSink.class);

    private String flag; // 单机还是集群模式, 值是JedisPool或者JedisCluster, 可以自由扩展
    private RedisCommand redisCommand; // Redis命令, 简单实现了set, 可以自由扩展
    private MyRedisMapper<IN> redisSinkMapper; // 解析输入数据的逻辑
    private MyRedisCommandsContainer redisCommandsContainer; // 创建Redis客户端容器

    public MyRedisSink(String flag, MyRedisMapper<IN> redisSinkMapper) {
        this.flag = flag;
        this.redisSinkMapper = redisSinkMapper;
        this.redisCommand = redisSinkMapper.getCommandDescription().getCommand();
    }

    public void invoke(IN input, Context context) {
        byte[] key = this.redisSinkMapper.getKeyFromData(input);
        byte[] value = this.redisSinkMapper.getValueFromData(input);
        switch (this.redisCommand) {
            case SET:
                this.redisCommandsContainer.set(key, value);
                break;
            default:
                throw new IllegalArgumentException("Cannot process such data type: " + this.redisCommand);
        }
    }

    public void open(Configuration parameters) throws Exception {
        try {
            if (flag == null || (Objects.equals("JedisPool", flag) && Objects.equals("JedisCluster", flag))) {
                throw new RuntimeException("Flag type must be sure");
            }
            this.redisCommandsContainer = MyRedisCommandsContainerBuilder.build(flag);
            this.redisCommandsContainer.open();
        } catch (Exception e) {
            LOG.error("Redis has not been properly initialized: ", e);
            throw e;
        }
    }
    
    public void close() throws IOException {
        if (this.redisCommandsContainer != null) {
            this.redisCommandsContainer.close();
        }
    }
}

/**
 * 输入数据解析
 */
interface MyRedisMapper<T> extends Function, Serializable {

    RedisCommandDescription getCommandDescription(); // Redis命令

    byte[] getKeyFromData(T data); // key

    byte[] getValueFromData(T data); // value
}

/**
 * Redis客户端构造器
 */
class MyRedisCommandsContainerBuilder {

    public MyRedisCommandsContainerBuilder() {

    }

    public static MyRedisCommandsContainer build(String flag) {
        if (Objects.equals("JedisPool", flag)) {
            return buildJedisPool();
        } else if (Objects.equals("JedisCluster", flag)) {
            return buildJedisCluster();
        }
        return null;
    }

    // 单机
    private static MyRedisCommandsContainer buildJedisPool() {

        // 连接池配置
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(8); // 资源池中的最大连接数
        config.setMaxIdle(8); // 资源池允许的最大空闲连接数
        config.setMinIdle(0); // 资源池确保的最少空闲连接数
        config.setBlockWhenExhausted(true); // 当资源池用尽后调用者是否要等待,只有当值为true时下面的maxWaitMillis才会生效
        config.setMaxWaitMillis(30000); // 当资源池连接用尽后,调用者的最大等待时间
        config.setTestWhileIdle(true); // 是否开启空闲资源检测
        config.setTimeBetweenEvictionRunsMillis(60000); // 空闲资源的检测周期
        config.setMinEvictableIdleTimeMillis(900000); // 资源池中资源的最小空闲时间,达到此值后空闲资源将被移除
        config.setNumTestsPerEvictionRun(3); // 做空闲资源检测时,每次检测资源的个数

        return new MyRedisContainer(new JedisPool(config, "127.0.0.1", 6379));
    }

    // 集群
    private static MyRedisCommandsContainer buildJedisCluster() {

        // 连接池配置
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(100); // 资源池中的最大连接数
        config.setMaxIdle(50); // 资源池允许的最大空闲连接数
        config.setMinIdle(20); // 资源池确保的最少空闲连接数
        config.setBlockWhenExhausted(true); // 当资源池用尽后调用者是否要等待,只有当值为true时下面的maxWaitMillis才会生效
        config.setMaxWaitMillis(30000); // 当资源池连接用尽后,调用者的最大等待时间
        config.setTestWhileIdle(true); // 是否开启空闲资源检测
        config.setTimeBetweenEvictionRunsMillis(60000); // 空闲资源的检测周期
        config.setMinEvictableIdleTimeMillis(900000); // 资源池中资源的最小空闲时间,达到此值后空闲资源将被移除
        config.setNumTestsPerEvictionRun(5); // 做空闲资源检测时,每次检测资源的个数
        // 集群信息
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.175.130", 7201));
        nodes.add(new HostAndPort("192.168.175.131", 7201));
        nodes.add(new HostAndPort("192.168.175.132", 7201));

        return new MyRedisClusterContainer(new JedisCluster(nodes, config));
    }
}

/**
 * 规范, 由单机或者集群Redis具体实现
 */
interface MyRedisCommandsContainer extends Serializable {

    void open() throws Exception; 

    void set(byte[] key, byte[] value);

    void close() throws IOException;
}

/**
 * 单机Redis
 */
class MyRedisContainer implements MyRedisCommandsContainer, Closeable {

    private static final long serialVersionUID = 1L;
    private static final Logger LOG = LoggerFactory.getLogger(MyRedisContainer.class);

    private transient JedisPool jedisPool;

    public MyRedisContainer(JedisPool jedisPool) {
        Objects.requireNonNull(jedisPool, "Jedis Pool can not be null");
        this.jedisPool = jedisPool;
    }

    private Jedis getInstance() {
        return this.jedisPool.getResource();
    }

    @Override
    public void open() {
        this.getInstance().echo("Test");
    }

    @Override
    public void close() {
        if (this.jedisPool != null) {
            this.jedisPool.close();
        }
    }

    @Override
    public void set(byte[] key, byte[] value) {
        Jedis jedis = null;
        try {
            jedis = this.getInstance();
            jedis.set(key, value);
        } catch (Exception e) {
            if (LOG.isErrorEnabled()) {
                LOG.error("Cannot send Redis message with command SET to key {} error message {}", key, e.getMessage());
            }
            throw e;
        } finally {
            this.releaseInstance(jedis);
        }
    }

    private void releaseInstance(Jedis jedis) {
        if (jedis != null) {
            try {
                jedis.close();
            } catch (Exception e) {
                LOG.error("Failed to close (return) instance to pool", e);
            }
        }
    }
}

/**
 * 集群Redis
 */
class MyRedisClusterContainer implements MyRedisCommandsContainer, Closeable {

    private static final long serialVersionUID = 1L;
    private static final Logger LOG = LoggerFactory.getLogger(MyRedisClusterContainer.class);

    private transient JedisCluster jedisCluster;

    public MyRedisClusterContainer(JedisCluster jedisCluster) {
        Objects.requireNonNull(jedisCluster, "Jedis cluster can not be null");
        this.jedisCluster = jedisCluster;
    }

    @Override
    public void open() {
        this.jedisCluster.echo("Test");
    }

    @Override
    public void close() throws IOException {
        this.jedisCluster.close();
    }

    @Override
    public void set(byte[] key, byte[] value) {
        try {
            this.jedisCluster.set(key, value);
        } catch (Exception e) {
            if (LOG.isErrorEnabled()) {
                LOG.error("Cannot send Redis message with command SET to key {} error message {}", key, e.getMessage());
            }
            throw e;
        }
    }
}

这里只是简单写的demo, 只有单机和集群的Redis实现, 命令实现也只有set, 以及很多硬编码的地方, 还有很多可以优化和改进的地方, 大家可以根据自己实际需求点进源码里面仿照着实现更多功能.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值