Flink消费Redis Stream数据

18 篇文章 1 订阅
3 篇文章 0 订阅

前言

对于流处理,感觉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独树一帜,在近些年引起了业内极大的兴趣和关注。Flink能够提供毫秒级别的延迟,同时保证了数据处理的低延迟、高吞吐和结果的正确性,还提供了丰富的时间类型和窗口计算、Exactly-once 语义支持,另外还可以进行状态管理,并提供了CEP(复杂事件处理)的支持。Flink在实时分析领域的优势,使得越来越多的公司开始将实时项目向Flink迁移,其社区也在快速发展壮大。目前,Flink已经成为各大公司实时领域的发力重点,特别是国内以阿里为代表的一众大厂,都在全力投入,不少公司为Flink社区贡献了大量源码。如今Flink已被很多人认为是大数据实时处理的方向和未来,很多公司也都在招聘和储备了解掌握Flink的人才。本教程将Flink理论与电商数据分析项目实战并重,对Flink基础理论知识做了系统的梳理和阐述,并通过电商用户行为分析的具体项目用多个指标进行了实战演练。为有志于增加大数据项目经验、扩展流式处理框架知识的工程师提供了学习方式。二、教程内容和目标本教程主要分为两部分:第一部分,主要是Flink基础理论的讲解,涉及到各种重要概念、原理和API的用法,并且会有大量的示例代码实现;第二部分,以电商作为业务应用场景,以Flink作为分析框架,介绍一个电商用户行为分析项目的开发实战。通过理论和实际的紧密结合,可以使学员对Flink有充分的认识和理解,在项目实战中对Flink和流式处理应用的场景、以及电商分析业务领域有更深刻的认识;并且通过对流处理原理的学习和与批处理架构的对比,可以对大数据处理架构有更全面的了解,为日后成长为架构师打下基础。三、谁适合学1、有一定的 Java、Scala 基础,希望了解新的大数据方向的编程人员2、有 Java、Scala 开发经验,了解大数据相关知识,希望增加项目经验的开发人员3、有较好的大数据基础,希望掌握Flink及流式处理框架的求职人员
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值