Flink消费Redis Stream数据

本文探讨了如何在不使用Kafka的情况下,利用Redis 5的Stream功能,通过Spring集成RedisTemplate和Redisson,实现Flink流处理系统中的数据源。重点介绍了配置、生产者和消费者组件的实现,并分享了自定义RedisSourceFunction以从Redis Stream读取数据的步骤。
该文章已生成可运行项目,

前言

对于流处理,感觉flink近乎苛刻的只对kafka友好。当然我对kafka也有天然的好感,但是相对于redis而言,kafka还是稍显复杂了一些。我们的生产环境中没有kafka,只有redis。装一套kafka集群可以吗。由于业务长期的累积,引入一套全新的架构真的是难如登天。所以只能委屈求全,在我们的业务系统中准备使用redis作为flink的数据源。

幸运的是,在redis5中已经有原生支持消息队列的数据存储结构了,即stream。但是现在网上介绍和使用redis stream的并不多。常用的redis客户端redisTemplatejedis还没有支持,只有RedissonLettuce支持了。

所以这先抛砖引玉,如果各位读者有更好的redis source解决方案可以介绍一下,感谢。

Redis配置

为了方便介绍,我这里使用Spring注入的方式定义各个对象,各位完全不必如此定义。

 package it.aspirin.demo.config;
 ​
 import io.lettuce.core.RedisClient;
 import io.lettuce.core.RedisURI;
 import io.lettuce.core.api.StatefulRedisConnection;
 import io.lettuce.core.api.sync.RedisCommands;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 ​
 ​
 @EnableAutoConfiguration
 @Configuration
 public class RedisConfig {
 ​
     @Value("${redis.database.flink}")
     private int flinkDb;
 ​
     @Value("${spring.redis.host}")
     private String host;
 ​
     @Value("${spring.redis.port}")
     private int port;
 ​
     @Value("${spring.redis.password}")
     private String password;
 ​
     @Value("${spring.redis.timeout}")
     private int timeout;
 ​
     /**
      * stream的各种操作命令主要使用RedisCommands对象进行
      * @return
      */
     @Bean(name = "streamRedisCommands")
     public RedisCommands<String, String> getRedisTemplate(){
         RedisURI redisURI = new RedisURI();
         redisURI.setHost(host);
         redisURI.setPort(port);
         redisURI.setDatabase(flinkDb);
         RedisClient redisClient = RedisClient.create(redisURI);
         StatefulRedisConnection<String, String> connect = redisClient.connect();
         return connect.sync();
     }
 ​
     @Bean
     public RedisConnectionFactory redisConnectionFactory() {
         return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
     }
 ​
     @Bean
     public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
         RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
         redisTemplate.setConnectionFactory(redisConnectionFactory);
         // 可以配置对象的转换规则,比如使用json格式对object进行存储。
         redisTemplate.setKeySerializer(new StringRedisSerializer());
         redisTemplate.setValueSerializer(new StringRedisSerializer());
         return redisTemplate;
     }
 }

封装redis生产者。flink是消息队列的消费者,因此下面对象,flink中并不会用到。

 package it.aspirin.demo.redis;
 ​
 import io.lettuce.core.api.sync.RedisCommands;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 ​
 import javax.annotation.Resource;
 ​
 /**
  * 往redis中生产数据
  */
 @Component
 public class RedisProducer {
     private final Logger logger = LoggerFactory.getLogger(RedisProducer.class);
 ​
     @Resource(name = "streamRedisCommands")
     public void setRedisClient(RedisCommands<String, String> redisSyncCommands) {
         RedisProducer.redisSyncCommands = redisSyncCommands;
     }
 ​
     private static RedisCommands<String, String> redisSyncCommands;
 ​
     public void send(String streamKey, String... message) {
         try {
             //第一个参数为stream的key,后面是内容
             String recordId = redisSyncCommands.xadd(streamKey,  message);
             logger.info("send message successful {}", recordId);
         } catch (Exception e) {
             throw new RuntimeException(e.getMessage());
         }
     }
 }

封装消费者。

 package it.aspirin.demo.redis;
 ​
 import io.lettuce.core.Consumer;
 import io.lettuce.core.StreamMessage;
 import io.lettuce.core.XReadArgs;
 import io.lettuce.core.api.sync.RedisCommands;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 ​
 import javax.annotation.Resource;
 import java.util.List;
 ​
 /**
  * Redis 消费者
  */
 @Component
 public class RedisConsumer {
     private final Logger logger = LoggerFactory.getLogger(RedisConsumer.class);
 ​
     @Resource(name = "streamRedisCommands")
     public void setRedisClient(RedisCommands<String, String> redisCommands) {
         RedisConsumer.redisCommands = redisCommands;
     }
 ​
     private static RedisCommands<String, String> redisCommands;
 ​
     /**
      * 判断是否存在消费者组
      *
      * @param groupName 消费者组名称
      * @return
      */
     public boolean exists(String groupName) {
         Long exists = redisCommands.exists(groupName);
         return exists.intValue() == 0;
     }
 ​
     // 普通消费 -- 最后一条消息
     public void consumer(String consumerGroup, String streamKey) {
         List<StreamMessage<String, String>> streamSmsSend = redisCommands.xread(XReadArgs.StreamOffset.from(streamKey, "0"));
         for (StreamMessage<String, String> message : streamSmsSend) {
             System.out.println(message);
             redisCommands.xack(streamKey, consumerGroup, message.getId());
         }
     }
 ​
     public void createGroup(String consumerGroup, String streamKey) {
 ​
         // 创建分组
         redisCommands.xgroupCreate(XReadArgs.StreamOffset.from(streamKey, "0"), consumerGroup);
     }
 ​
 ​
     public void consumerGroup(String consumerGroup, String streamKey) {
         // 按组消费
         List<StreamMessage<String, String>> xReadGroup = redisCommands.xreadgroup(Consumer.from(consumerGroup, "consumer_1"), XReadArgs.StreamOffset.lastConsumed(streamKey));
         for (StreamMessage<String, String> message : xReadGroup) {
             System.out.println("ass - " + message);
             // 告知 redis,消息已经完成了消费
             redisCommands.xack(streamKey, consumerGroup, message.getId());
         }
     }
 ​
     /**
      * 读取redis中的数据
      *
      * @param consumerGroup 消费组
      * @param streamKey     stream对应的key
      * @return
      */
     public List<StreamMessage<String, String>> getMessage(String consumerGroup, String streamKey) {
         // 按组消费
         return redisCommands.xreadgroup(Consumer.from(consumerGroup, "consumer_1"), XReadArgs.StreamOffset.lastConsumed(streamKey));
     }
 ​
 }

自定义redis source function

 package it.aspirin.demo.flink.source;
 ​
 import io.lettuce.core.StreamMessage;
 import io.lettuce.core.api.sync.RedisCommands;
 import it.aspirin.demo.redis.RedisConsumer;
 import it.aspirin.demo.utl.AppUtil;
 import org.apache.flink.api.java.tuple.Tuple2;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 ​
 /**
  * 读取redis stream中的数据消费
  */
 public class RedisSourceFunction extends RichParallelSourceFunction<Tuple2<String, String>> {
     private final Logger logger = LoggerFactory.getLogger(RedisSourceFunction.class);
     private final String consumerGroup = "consumer-group-1";
     private final String streamKey = "stream1";
     private RedisConsumer consumer;
     private RedisCommands<String,String> redisCommands;
 ​
     /**
      * 创建消费者组
      * @param parameters
      * @throws Exception
      */
     @Override
     public void open(Configuration parameters) throws Exception {
         consumer = AppUtil.context.getBean(RedisConsumer.class);
         redisCommands = AppUtil.context.getBean(RedisCommands.class);
         //如果消费者组不存在,则创建
         if (!consumer.exists(consumerGroup)) {
             consumer.createGroup(consumerGroup, streamKey);
         }
     }
 ​
     @Override
     public void close() throws Exception {
         super.close();
     }
 ​
     /**
      * 下面是消费并解析redis中的数据,然后将数据发往flink下游算子
      * @param sourceContext
      * @throws Exception
      */
     @Override
     public void run(SourceContext<Tuple2<String, String>> sourceContext) throws Exception {
         try{
             while (true) {
                 List<StreamMessage<String, String>> messages = consumer.getMessage(consumerGroup, streamKey);
                 for (StreamMessage<String, String> msg : messages) {
                     Map<String, String> body = msg.getBody();
                     Set<String> keySet = body.keySet();
                     for (String key : keySet) {
                         sourceContext.collect(new Tuple2<>(key, body.get(key)));
                         //因为没有找到让redis中数据过期的方法,因此当消费完一条数据以后将redis中的数据删除,这并不是很严谨的方式
                         redisCommands.xdel(streamKey, msg.getId());
                         Long xlen = redisCommands.xlen(streamKey);
                         logger.info("xlen = {}", xlen);
                     }
                 }
             }
         }catch (Exception e){
             String message = e.getMessage();
             if (message.contains("Connection reset by peer")){
                 logger.error("redis maybe shutdown "+ e);
             }
         }
     }
 ​
     @Override
     public void cancel() {
 ​
     }
 }

注意事项

  • 只能说上面方式可以消费redis队列中的数据,但是不能保证性能很好,如有可以优化的地方,欢迎指正

  • 我们没有有找到如何使redis stream中的数据过期,如果数据是长期存储的,需要确定redis是否吃得消。我的解决办法是消费一条数据,接着将该数据删除,这并不是一种很好的处理方式。

  • 还有很多跟stream操作相关的api,如有需要可以自行学习,redisCommands中以x开头的命令都是与stream相关的命令。

本文章已经生成可运行项目
### 如何使用 Flink 和 Scala 实现读写 Redis 数据Flink 中,可以通过 `Flink-Connector-Redis` 来实现与 Redis 的交互。以下是一个完整的示例,展示如何使用 Flink 和 Scala 读取和写入 Redis 数据。 #### 1. 引入依赖 首先,在项目中引入 `Flink-Connector-Redis` 的依赖。以下是 Maven 配置的示例[^2]: ```xml <dependency> <groupId>org.apache.bahir</groupId> <artifactId>flink-connector-redis_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> ``` #### 2. 写入 Redis 数据 写入 Redis 数据时,需要定义一个自定义的 `RedisMapper`,用于指定数据存储的方式。以下是一个简单的写入示例[^3]: ```scala import org.apache.flink.streaming.api.scala._ import org.apache.flink.streaming.connectors.redis.RedisSink import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper} object WriteToRedis { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment // 创建 Redis 配置 val conf = new FlinkJedisPoolConfig.Builder().setHost("localhost").build() // 模拟输入数据流 val stream: DataStream[(String, Int)] = env.fromElements(("word1", 1), ("word2", 2)) // 将数据写入 Redis stream.addSink(new RedisSink[(String, Int)]( conf, new RedisMapper[(String, Int)] { override def getCommandDescription: RedisCommandDescription = new RedisCommandDescription(RedisCommand.HSET, "word_count") override def getKeyFromData(data: (String, Int)): String = data._1 override def getValueFromData(data: (String, Int)): String = data._2.toString } )) env.execute("Write to Redis") } } ``` #### 3. 从 Redis 读取数据Redis 读取数据时,可以使用 `RedisSourceFunction` 或者通过自定义逻辑读取 Redis 哈希表中的数据。以下是一个基于 `FlatMapFunction` 的读取示例[^1]: ```scala import org.apache.flink.api.java.tuple.Tuple2 import org.apache.flink.streaming.api.scala._ import org.apache.flink.api.common.functions.FlatMapFunction import org.apache.flink.util.Collector import scala.collection.JavaConverters._ object ReadFromRedis { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment // 模拟 Redis 数据源(此处仅为示例,实际需要替换为 RedisSource) val redisDataStream: DataStream[Map[String, String]] = env.fromElements(Map("word1" -> "5", "word2" -> "10")) redisDataStream.flatMap(new FlatMapFunction[Map[String, String], Tuple2[String, Int]] { override def flatMap(input: Map[String, String], out: Collector[Tuple2[String, Int]]): Unit = { input.asScala.foreach { case (key, value) => out.collect(Tuple2(key, value.toInt)) } } }).print() env.execute("Read from Redis") } } ``` #### 4. Redis 配置调整 为了确保 Flink 能够正确连接到 Redis,可能需要对 Redis 配置文件进行一些调整[^5]: - 取消 IP 绑定:将 `bind 127.0.0.1` 注释掉。 - 关闭保护模式:将 `protected-mode yes` 修改为 `no`。 - 允许后台启动:将 `daemonize no` 修改为 `yes`。 #### 5. 完整流程 结合上述代码,可以实现从 Redis 中读取单词频率并计算出现次数最多的单词。最终结果可以通过 Flink 的 `print()` 方法输出或进一步处理。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值