Flink / Scala - 使用 Jedis、JedisPool 作为 Source 读取数据

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
实时计算 Flink 版,1000CU*H 3个月
简介: 现在有一批数据写入多台 Redis 相同 key 的队列中,需要消费 Redis 队列作为 Flink Source,为了提高可用性,下面基于 JedisPool 进行队列的消费。

一.引言

现在有一批数据写入多台 Redis 相同 key 的队列中,需要消费 Redis 队列作为 Flink Source,为了提高可用性,下面基于 JedisPool 进行队列的消费。队列数据示例: 1,2,3,4,5、A,B,C,D,E,程序将字符串解析并 split(",") 然后分别写到下游。

二.Flink Source By JedisPool

1.初始化 JedisPool

由于数据量较大,所以同时写入 N 台 Redis 队列,key 均相同,注意这里是 JedisPool 不是 JedisCluster,需要区分二者的概念。

def initJedisPool(host: String, port: Int): JedisPool = {
    val config = new JedisPoolConfig
    config.setMaxTotal(4)
    config.setMaxIdle(2)
    config.setMaxWaitMillis(1000)
    config.setTestOnBorrow(true)
    config.setTestOnReturn(true)
    jedisPool = new JedisPool(config, host, port)
    jedisPool
  }

image.gif

需要导入 Jedis 依赖:

<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.7.2</version>
        </dependency>

image.gif

这里 JedisPool 的几个参数可以参考之前的文章:JedisPool - Java.net.SocketException: Broken pipe (write failed),里面介绍了 TestOnBorrow、TestOnReturn 的含义。

2.实现 RedisSource

自定义 Source 需要继承 org.apache.flink.streaming.api.functions.source.RichSourceFunction 实现 run 方法和 cancel 方法即可,主要逻辑在 run 方法中,run 方法负责从队列中不断获取数据并 collect 到下游,除此之外,由于需要读取 redis client,所以还需要新增 open 方法进行 client 的初始化。

class JedisPoolSourceTest(host: String, port: Int) extends RichSourceFunction[String] {
  var jedisPool: JedisPool = _ // JedisPool
  val listKey = "testListKey" // 公用队列 Key
  override def open(parameters: Configuration): Unit = {
    initJedisPool(host, port) // 初始化 JedisPool
  }
  // 消费 Redis 队列产出数据
  override def run(sourceContext: SourceFunction.SourceContext[String]): Unit = {
    collectData(sourceContext)
  }
  // 关闭 Source
  override def cancel(): Unit = {
    jedisPool.close()
  }
}

image.gif

3.产出数据 CollectData

上面是继承 RichSourceFunction 完成了整体的架构,下面单独把 collectData 的数据产出逻辑分析一下:

def collectData(out: SourceFunction.SourceContext[String]): Unit = {
    var resource: Jedis = null
    try {
      resource = jedisPool.getResource // 获取 Jedis 实例
      // while True 保证持续消费
      while (true) {
        // 解析并产出
        val info = resource.lpop(listKey)
        if (info != null) {
          val sendRe = info.split(",")
          if (sendRe.nonEmpty) {
            sendRe.foreach(out.collect)
          }
        }
        // 如果队列为空则 Sleep 1s
        if (resource.llen(listKey) == 0) {
          TimeUnit.SECONDS.sleep(1)
        }
      }
    } catch {
      case e: Exception => {
        e.printStackTrace()
        resource.close() // 关闭资源
        collectData(out) // 出现异常递归重启,保证 Source 的可用性
      }
    }
  }

image.gif

collectData 为 run 方法的主类,主要用于消费队列并产出数据到下游,有几个点需要注意:

A.获取 Jedis 实例

由于本例中使用 JedisPool,所以这里采用 pool.getResource 的方式获取 Jedis,网上也有一些 demo 在这里直接初始化单独 JedisClient,这里采用 pool 的形式主要是考虑到稳定性的情况。

B.While True

这里没啥太多说的,通过 while true 实现不间断的消费队列数据。

C.Sleep(1)

这里考虑到队列为空时,如果频繁 while true 访问 Jedis 会造成高 QPS 且无用操作,所以这里检测到队列为空时会加入 1s 的延迟,这个时间也可以根据自己的任务场景灵活修改。

D.Try Catch + 递归

try-catch 逻辑内除关闭 resource 外还递归调用了 CollectData,该方法参考了 Spark-Streaming 的 receiver 方法,在故障时能够重新申请 redis 连接并重新读取数据,保证 Source 的稳定性,Spark-Streaming 的 receiver 可以参考:Spark Streaming Receiver restart 重启

4.合并多条数据源

由于数据量较大,所以数据分布式的写入到多台 Redis 队列中,上面已经实现了 JedisSource,下面则需要将多台 Redis 分别接入 JedisSource 并绑定合成一个统一的 Source。

var dataStream: DataStream[String] = null // 初始化原始流
    // 遍历多台 Redis 的 Host && Port
    testHostAndPorts.foreach { case (host, port) => { 
      if (dataStream == null) {
        dataStream = env.addSource[String](new JedisPoolSourceTest(host, port))
      } else {
        val newStream = env.addSource[String](new JedisPoolSourceTest(host, port))
        dataStream = dataStream.union(newStream) // union 合并流
      }
    }
    }

image.gif

通过 var 构造可变变量,然后不断将 RedisSource union 到 DataStream 中,最终形成统一流。

三.测试多 Redis Source

1.启动多台 client

启动多个 Jedis 连接并向队列 key 推数据:

redis-cli -h $host -p $port

image.gif

在不同客户端分别执行:

lpush testListKey 0,1,2,3,4
lpush testListKey A,B,C,D,E

image.gif

2.主函数 main

主函数逻辑比较简单,主要就是接受队列数据,"," split 并将每个元素输出,最终 print sink 到标准输出。

def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment // 执行环境
    // 合并多流
    var dataStream: DataStream[String] = null
    testHostAndPorts.foreach { case (host, port) => {
      if (dataStream == null) {
        dataStream = env.addSource[String](new JedisPoolSourceTest(host, port))
      } else {
        val newStream = env.addSource[String](new JedisPoolSourceTest(host, port))
        dataStream = dataStream.union(newStream)
      }
    }
    }
    // 标准输出
    dataStream.print()
    // 执行
    env.execute()

image.gif

上面通过 2 台 client 分别 lpush 了数据,这边接收测试也没有问题:

image.gif编辑

四.尝试与优化

1.多线程的尝试

上面采用 JedisPool + 单 Resource 的形式进行接受数据,其实最早使用 JedisPool 是为了接入多线程 Source,即在 run 方法内使用线程池提高生产效率:

def addTask(poolSize: Int, jedisPool: JedisPool, sourceContext: SourceFunction.SourceContext[String]): Unit = {
    val executor = Executors.newFixedThreadPool(2) // 初始化线程池
    (0 until poolSize).foreach(epoch => {
      executor.submit(new Runnable {
        override def run(): Unit = {
          val resource: Jedis = jedisPool.getResource
          while (true) {
            val info = resource.lpop(listKey)
            if (info != null) {
              info.split(",").foreach(sourceContext.collect)
            }
            if (resource.llen(listKey) == 0) {
              TimeUnit.SECONDS.sleep(1)
            }
          }
        }
      })
    })
  }

image.gif

这里 Runnable 和上面单 Resource 接队列消费逻辑相同,实现后发现可以多线程读取到 Jedis 队列的内容,随后程序发生堵塞,经过调试发现任务卡在 sourceContext.collect,不知道是不是 sourceContext 不支持多线程,有了解的同学也可以评论区讨论一下~

2.空闲数据源的优化

上述多台 redis 队列并不能保证全天无间断都有数据产出,在 EventTime 场景下,会出现流数据空闲的状态从而影响 WaterMark 的更新影响整个任务的时间推进,为了解决这个问题,可以使用新版的时间戳策略即 WatermarkStrategy:

WatermarkStrategy
      .forBoundedOutOfOrderness[T](Duration.ofSeconds(60))
      .withIdleness(Duration.ofMinutes(1))

image.gif

通过 org.apache.flink.api.common.eventtime.WatermarkStrategy 的 withIdleness 方法对数据流进行标记,当数据源在 Duration 规定的时间内未产出数据时将该 Source 标记为空闲状态,这样下游的数据也不需要等待该 Source 的 WaterMark 从而保证数据流的正常推进,待有新数据推入 Source 中,该数据流切换为活跃状态,重新向下游发送其真实 WaterMark 水印。

3.数据量过大

对于数据过多或过大的场景,lpop 可能会有一定的读取和网络压力,这时候可以采用 lrange + ltrim 的批量读取逻辑,将单条访问改为批量访问,减少 redis 读取压力。其思想与 hscan 代替 hgetAll 批量访问过大数据有一定类似,都是采取分治的思想,只不过前者是由一到多,后者是化整为零。

五.总结

绑定多台 Redis 源上线后,任务没有问题且支持空流处理,除了 Redis Source 外,还有 Redis Sink 相关的实现,大家可以参考: Flink / Scala - 使用 RedisSink 存储数据,这里使用 SharedJedisPool 代替了 Flink 自带的 RedisCommandsContainer,后续也会单独出一期 RedisCommandsContainer 的 Flink-Jedis 教程。

image.gif编辑

可以看到多台 Redis Source 上线后,这个作业图实在是 🚄。

相关实践学习
基于Hologres+Flink搭建GitHub实时数据大屏
通过使用Flink、Hologres构建实时数仓,并通过Hologres对接BI分析工具(以DataV为例),实现海量数据实时分析.
实时计算 Flink 实战课程
如何使用实时计算 Flink 搞定数据处理难题?实时计算 Flink 极客训练营产品、技术专家齐上阵,从开源 Flink功能介绍到实时计算 Flink 优势详解,现场实操,5天即可上手! 欢迎开通实时计算 Flink 版: https://cn.aliyun.com/product/bigdata/sc Flink Forward Asia 介绍: Flink Forward 是由 Apache 官方授权,Apache Flink Community China 支持的会议,通过参会不仅可以了解到 Flink 社区的最新动态和发展计划,还可以了解到国内外一线大厂围绕 Flink 生态的生产实践经验,是 Flink 开发者和使用者不可错过的盛会。 去年经过品牌升级后的 Flink Forward Asia 吸引了超过2000人线下参与,一举成为国内最大的 Apache 顶级项目会议。结合2020年的特殊情况,Flink Forward Asia 2020 将在12月26日以线上峰会的形式与大家见面。
目录
相关文章
|
2月前
|
SQL 人工智能 JSON
Flink 2.1 SQL:解锁实时数据与AI集成,实现可扩展流处理
简介:本文整理自阿里云高级技术专家李麟在Flink Forward Asia 2025新加坡站的分享,介绍了Flink 2.1 SQL在实时数据处理与AI融合方面的关键进展,包括AI函数集成、Join优化及未来发展方向,助力构建高效实时AI管道。
563 43
|
2月前
|
SQL 人工智能 JSON
Flink 2.1 SQL:解锁实时数据与AI集成,实现可扩展流处理
本文整理自阿里云的高级技术专家、Apache Flink PMC 成员李麟老师在 Flink Forward Asia 2025 新加坡[1]站 —— 实时 AI 专场中的分享。将带来关于 Flink 2.1 版本中 SQL 在实时数据处理和 AI 方面进展的话题。
190 0
Flink 2.1 SQL:解锁实时数据与AI集成,实现可扩展流处理
|
6月前
|
存储 消息中间件 Kafka
基于 Flink 的中国电信星海时空数据多引擎实时改造
本文整理自中国电信集团大数据架构师李新虎老师在Flink Forward Asia 2024的分享,围绕星海时空智能系统展开,涵盖四个核心部分:时空数据现状、实时场景多引擎化、典型应用及未来展望。系统日处理8000亿条数据,具备亚米级定位能力,通过Flink多引擎架构解决数据膨胀与响应时效等问题,优化资源利用并提升计算效率。应用场景包括运动状态识别、个体行为分析和群智感知,未来将推进湖仓一体改造与三维时空服务体系建设,助力数字化转型与智慧城市建设。
650 3
基于 Flink 的中国电信星海时空数据多引擎实时改造
|
2月前
|
SQL 关系型数据库 Apache
从 Flink 到 Doris 的实时数据写入实践 —— 基于 Flink CDC 构建更实时高效的数据集成链路
本文将深入解析 Flink-Doris-Connector 三大典型场景中的设计与实现,并结合 Flink CDC 详细介绍了整库同步的解决方案,助力构建更加高效、稳定的实时数据处理体系。
1069 0
从 Flink 到 Doris 的实时数据写入实践 —— 基于 Flink CDC 构建更实时高效的数据集成链路
|
3月前
|
存储 消息中间件 搜索推荐
京东零售基于Flink的推荐系统智能数据体系
摘要:本文整理自京东零售技术专家张颖老师,在 Flink Forward Asia 2024 生产实践(二)专场中的分享,介绍了基于Flink构建的推荐系统数据,以及Flink智能体系带来的智能服务功能。内容分为以下六个部分: 推荐系统架构 索引 样本 特征 可解释 指标 Tips:关注「公众号」回复 FFA 2024 查看会后资料~
247 1
京东零售基于Flink的推荐系统智能数据体系
|
7月前
|
Oracle 关系型数据库 Java
【YashanDB知识库】Flink CDC实时同步Oracle数据到崖山
本文介绍通过Flink CDC实现Oracle数据实时同步至崖山数据库(YashanDB)的方法,支持全量与增量同步,并涵盖新增、修改和删除的DML操作。内容包括环境准备(如JDK、Flink版本等)、Oracle日志归档启用、用户权限配置、增量日志记录设置、元数据迁移、Flink安装与配置、生成Flink SQL文件、Streampark部署,以及创建和启动实时同步任务的具体步骤。适合需要跨数据库实时同步方案的技术人员参考。
【YashanDB知识库】Flink CDC实时同步Oracle数据到崖山
|
8月前
|
Java 关系型数据库 MySQL
SpringBoot 通过集成 Flink CDC 来实时追踪 MySql 数据变动
通过详细的步骤和示例代码,您可以在 SpringBoot 项目中成功集成 Flink CDC,并实时追踪 MySQL 数据库的变动。
1874 45
|
7月前
|
消息中间件 关系型数据库 Kafka
阿里云基于 Flink CDC 的现代数据栈云上实践
阿里云基于 Flink CDC 的现代数据栈云上实践
118 1
|
11月前
|
存储 监控 数据处理
flink 向doris 数据库写入数据时出现背压如何排查?
本文介绍了如何确定和解决Flink任务向Doris数据库写入数据时遇到的背压问题。首先通过Flink Web UI和性能指标监控识别背压,然后从Doris数据库性能、网络连接稳定性、Flink任务数据处理逻辑及资源配置等方面排查原因,并通过分析相关日志进一步定位问题。
762 61
|
12月前
|
运维 数据处理 Apache
数据实时计算产品对比测评报告:阿里云实时计算Flink版
数据实时计算产品对比测评报告:阿里云实时计算Flink版