flink维表关联系列之Mysql维表关联:全量加载

在维表关联中定时全量加载是针对维表数据量较少并且业务对维表数据变化的敏感程度较低的情况下可采取的一种策略,对于这种方案使用有几点需要注意:

  1. 全量加载有可能会比较耗时,所以必须是一个异步加载过程
  2. 内存维表数据需要被流表数据关联读取、也需要被定时重新加载,这两个过程是不同线程执行,为了尽可能保证数据一致性,可使用原子引用变量包装内存维表数据对象,即AtomicReference
  3. 查内存维表数据非异步io过程

具体实例:广告流量统计,广告流量数据包含:广告位id,用户设备id,事件类型(点击、浏览),发生时间,现在需要统计每个广告主在每一个时间段内的点击、浏览数量,流量数据中只有广告位id, 广告位id与广告主id对应的关系在mysql 中,这是一个典型的流表关联维表过程,需要从mysql中获取该广告位id对应的广告主id, 然后再来统计。接下来看维表关联的实现代码:

class SideFlatMapFunction extends RichFlatMapFunction[AdData, AdData] {
  // 原子引用变量,读写为不同线程,保证数据一致性,全量加载维表数据
  private var sideInfo: AtomicReference[java.util.Map[Int, Int]] = _

  override def open(parameters: Configuration): Unit = {
    sideInfo = new AtomicReference[java.util.Map[Int, Int]]()
    // 第一次: 装载维度表数据到原子引用变量
    sideInfo.set(loadData)
    
    // 自定义线程池,单线程调度线程池,定时重新加载维度表数据 (异步加载)
    val executors=Executors.newSingleThreadScheduledExecutor()
    executors.scheduleAtFixedRate(new Runnable {
      override def run(): Unit = reload()
    },5,5, TimeUnit.MINUTES)
  }

  override def flatMap(value: AdData, out: Collector[AdData]): Unit = {
    val tid=value.tId
    // 关联维度表数据
    val aid=sideInfo.get().get(tid)
    var newV=AdData(aid,value.tId,value.clientId,value.actionType,value.time)
    out.collect(newV)
  }

  def reload()={
    try{
      println("do reload~")
      val newData=loadData()
      // 重新装载维度表数据到原子引用变量
      sideInfo.set(newData)
      println("reload ok~")
    }catch {
      case e:Exception=>{
        e.printStackTrace()
      }
    }
  }

  def loadData(): util.Map[Int, Int] = {
    val data = new util.HashMap[Int, Int]()
    
    Class.forName("com.mysql.jdbc.Driver")
    val con = DriverManager.getConnection("jdbc:mysql://localhost:3306/paul", "root", "123456")
    val sql = "select aid,tid from ads"
    val statement = con.prepareStatement(sql)
    val rs = statement.executeQuery()

    while (rs.next()) {
      val aid = rs.getInt("aid")
      val tid = rs.getInt("tid")
      data.put(tid, aid)
    }
    con.close()
    data
  }
}

主程序:

case class AdData(aId: Int, tId: Int, clientId: String, actionType: Int, time: Long)

object Demo1 {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    
    val kafkaConfig = new Properties()
    kafkaConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    kafkaConfig.put(ConsumerConfig.GROUP_ID_CONFIG, "test1");
    
    val consumer = new FlinkKafkaConsumer011[String]("topic1", new SimpleStringSchema(), kafkaConfig);
    
    env.addSource(consumer)
      .map(x => {
        val a: Array[String] = x.split(",")
        AdData(0, a(0).toInt, a(1), a(2).toInt, a(3).toLong) //默认aid为0
      })
      .flatMap(new SideFlatMapFunction)
      .print()

    env.execute()
  }
}

在kafka端生产数据:1,clientId1,1,1571646006000
控制台打印

> AdData(1,1,clientId1,1,1571646006000)

然后将MySQL维表中tid为1的aid 变为2,待一分钟后继续输入数据:1,clientId1,1,1571646006000
控制台打印

>AdData(2,1,clientId1,1,1571646006000)

说明维表数据的更新已经被加载了。
对于该demo还有两点值得思考:

  1. 异步加载过程是异步线程执行,如果异步线程加载抛出异常是无法被Task检测,也就是无法导致任务失败,那么就会导致使用的维表数据一直都是变化之前的,对于业务来说是无法容忍的,解决方式自定义一个维表关联的StreamOperator, 可获取到StreamTask, 然后z异步加载的异常处理中调用StreamTask.handleAsyncException方法,就可以导致任务失败,给用户发出警告。
  2. 维表全量加载是在每个task里面执行,那么就会导致每个task里面都有一份全量的维表数据,可采取优化方式是在维表关联前根据关联字段做keyBy操作,那么就会根据关联字段hash然后对并行度取余得到相同的值就会被分配到同一个task里面,所以在加载维表数据的时候也可以在每个task加载与其对应的维表数据, 就可以减少加载的数据量。其具体计算的规则是:
(MathUtils.murmurHash(key.hashCode()) % maxParallelism)*parallelism / maxParallelism

得到的值就是IndexOfThisSubtask 即task的索引,那么可使用同样的算法过滤维表数据。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雾岛与鲸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值