【Flink流式计算框架】:Flink维表join

前言

Flink常见的维表Join方式有四种:

  1. 预加载维表
  2. 热存储维表
  3. 广播维表
  4. Temporal table function join

1.预加载维表

通过定义一个类实现RichMapFunction,在open()中读取维表数据加载到内存中,在map()方法中与维表数据进行关联。RichMapFunction中open方法里加载维表数据到内存的方式特点如下:

  • 优点:实现简单
  • 缺点:因为数据存于内存,所以只适合小数据量并且维表数据更新频率不高的情况下。虽然可以在open中定义一个定时器定时更新维表,但是还是存在维表更新不及时的情况。
object JoinDemo {

  def main(args: Array[String]): Unit = {

    // 创建执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 1. 从集合中读取数据
    val dataList = env.socketTextStream("192.168.72.111", 8888)

    val result = dataList.map(data => {
      val arr = data.split(",")
      User(arr(0).toInt, arr(1).toInt)
    }).map(new MyMapperFunction)
    result.print()
    env.execute("111")
  }
}

class MyMapperFunction extends RichMapFunction[User, result] {

  var dim =  mutable.HashMap(1->"北京",2->"上海",3->"广州",4->"深圳")

  override def open(parameters: Configuration): Unit = {
    dim += (5->"武汉")
  }

  override def map(value: User): result = {
    if(dim.contains(value.cityId)){
      result(value.id,value.cityId,dim.get(value.cityId).getOrElse(""))
    }else{
      result(value.id,value.cityId,null)
    }
  }

}

2.热存储维表

这种方式是将维表数据存储在Redis、HBase、MySQL等外部存储中,实时流在关联维表数据的时候实时去外部存储中查询,这种方式特点如下:

  • 优点:维度数据量不受内存限制,可以存储很大的数据量。
  • 缺点:因为维表数据在外部存储中,读取速度受制于外部存储的读取速度;另外维表的同步也有延迟。

2.1使用cache来减轻访问压力

在这里使用guava Cache,来存储一部分常访问的维表数据,以减少访问外部系统的次数。

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

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple2<Integer, Integer>> textStream = env.socketTextStream("192.168.72.111", 8888, "\n")
                .map(p -> {
                    //输入格式为:userId,1000,分别是用户Id和城市编号
                    String[] list = p.split(",");
                    return new Tuple2<Integer, Integer>(Integer.valueOf(list[0]), Integer.valueOf(list[1]));
                })
                .returns(new TypeHint<Tuple2<Integer, Integer>>() {
                });
        DataStream<Tuple3<Integer, Integer, String>> result = textStream.map(new MapJoinDemo1());
        result.print();
        env.execute("joinDemo2");
    }

    static class MapJoinDemo1 extends RichMapFunction<Tuple2<Integer, Integer>, Tuple3<Integer, Integer, String>> {
        LoadingCache<Integer, String> dim;

        @Override
        public void open(Configuration parameters) throws Exception {
            //使用google LoadingCache来进行缓存
            dim = CacheBuilder.newBuilder()
                    //最多缓存个数,超过了就根据最近最少使用算法来移除缓存
                    .maximumSize(1000)
                    //在更新后的指定时间后就回收
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    //指定移除通知
                    .removalListener(new RemovalListener<Integer, String>() {
                        @Override
                        public void onRemoval(RemovalNotification<Integer, String> removalNotification) {
                            System.out.println(removalNotification.getKey() + "被移除了,值为:" + removalNotification.getValue());
                        }
                    })
                    .build(
                            //指定加载缓存的逻辑
                            new CacheLoader<Integer, String>() {
                                @Override
                                public String load(Integer cityId) throws Exception {
                                    String cityName = readFromHbase(cityId);
                                    return cityName;
                                }
                            }
                    );

        }

        private String readFromHbase(Integer cityId) {
            //读取hbase
            //这里写死,模拟从hbase读取数据
            Map<Integer, String> temp = new HashMap<>();
            temp.put(1, "北京");
            temp.put(2, "上海");
            temp.put(3, "武汉");
            temp.put(4, "深圳");
            String cityName = "";
            if (temp.containsKey(cityId)) {
                cityName = temp.get(cityId);
            }

            return cityName;
        }

        @Override
        public Tuple3<Integer, Integer, String> map(Tuple2<Integer, Integer> value) throws Exception {
            //在map方法中进行主流和维表的关联
            String cityName = "";
            if (dim.get(value.f1) != null) {
                cityName = dim.get(value.f1);
            }
            return new Tuple3<>(value.f0, value.f1, cityName);
        }
    }
}

2.2使用异步IO来提高访问吞吐量

Flink与外部存储系统进行读写操作的时候可以使用同步方式,也就是发送一个请求后等待外部系统响应,然后再发送第二个读写请求,这样的方式吞吐量比较低,可以用提高并行度的方式来提高吞吐量,但是并行度多了也就导致了进程数量多了,占用了大量的资源。

Flink中可以使用异步IO来读写外部系统,这要求外部系统客户端支持异步IO,不过目前很多系统都支持异步IO客户端。但是如果使用异步就要涉及到三个问题:

  • 超时:如果查询超时那么就认为是读写失败,需要按失败处理;
  • 并发数量:如果并发数量太多,就要触发Flink的反压机制来抑制上游的写入。
  • 返回顺序错乱:顺序错乱了要根据实际情况来处理,Flink支持两种方式:允许乱序、保证顺序。
public class JoinDemo3 {
    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple2<Integer, Integer>> textStream = env.socketTextStream("localhost", 9000, "\n")
                .map(p -> {
                    //输入格式为:user,1000,分别是用户名称和城市编号
                    String[] list = p.split(",");
                    return new Tuple2<Integer, Integer>(Integer.valueOf(list[0]), Integer.valueOf(list[1]));
                })
                .returns(new TypeHint<Tuple2<Integer, Integer>>() {
                });


        DataStream<Tuple3<Integer, Integer, String>> orderedResult = AsyncDataStream
                //保证顺序:异步返回的结果保证顺序,超时时间1秒,最大容量2,超出容量触发反压
                .orderedWait(textStream, new JoinDemoAyncFunction(), 1000L, TimeUnit.MILLISECONDS, 2)
                .setParallelism(1);

        DataStream<Tuple3<Integer, Integer, String>> unorderedResult = AsyncDataStream
                //允许乱序:异步返回的结果允许乱序,超时时间1秒,最大容量2,超出容量触发反压
                .unorderedWait(textStream, new JoinDemoAyncFunction(), 1000L, TimeUnit.MILLISECONDS, 2)
                .setParallelism(1);

        orderedResult.print();
        unorderedResult.print();
        env.execute("joinDemo");
    }

    //定义个类,继承RichAsyncFunction,实现异步查询存储在mysql里的维表
    //输入用户名ID、城市ID,返回 Tuple3<用户ID、城市ID,城市名称>
    static class JoinDemoAyncFunction extends RichAsyncFunction<Tuple2<Integer, Integer>, Tuple3<Integer, Integer, String>> {
        // 链接
        private static String jdbcUrl = "jdbc:mysql://192.168.72.111:3306?useSSL=false";
        private static String username = "root";
        private static String password = "123456";
        private static String driverName = "com.mysql.jdbc.Driver";
        java.sql.Connection conn;
        PreparedStatement ps;

        @Override
        public void open(Configuration parameters) throws Exception {
            super.open(parameters);

            Class.forName(driverName);
            conn = DriverManager.getConnection(jdbcUrl, username, password);
            ps = conn.prepareStatement("select city_name from tmp.city_info where id = ?");
        }

        @Override
        public void close() throws Exception {
            super.close();
            conn.close();
        }

        //异步查询方法
        @Override
        public void asyncInvoke(Tuple2<Integer, Integer> input, ResultFuture<Tuple3<Integer, Integer, String>> resultFuture) throws Exception {
            // 使用 city id 查询
            ps.setInt(1, input.f1);
            ResultSet rs = ps.executeQuery();
            String cityName = null;
            if (rs.next()) {
                cityName = rs.getString(1);
            }
            List list = new ArrayList<Tuple2<Integer, String>>();
            list.add(new Tuple3<>(input.f0, input.f1, cityName));
            resultFuture.complete(list);
        }

        //超时处理
        @Override
        public void timeout(Tuple2<Integer, Integer> input, ResultFuture<Tuple3<Integer, Integer, String>> resultFuture) throws Exception {
            List list = new ArrayList<Tuple2<Integer, String>>();
            list.add(new Tuple3<>(input.f0, input.f1, ""));
            resultFuture.complete(list);
        }
    }
}

3.广播维表

利用Flink的Broadcast State将维度数据流广播到下游做join操作。特点如下:

  • 优点:维度数据变更后可以即时更新到结果中。
  • 缺点:数据保存在内存中,支持的维度数据量比较小。
public class JoinDemo4 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //定义主流
        DataStream<Tuple2<Integer, Integer>> textStream = env.socketTextStream("192.168.72.111", 8888, "\n")
                .map(p -> {
                    //输入格式为:userId,1000,分别是用户名称和城市编号
                    String[] list = p.split(",");
                    return new Tuple2<Integer, Integer>(Integer.valueOf(list[0]), Integer.valueOf(list[1]));
                })
                .returns(new TypeHint<Tuple2<Integer, Integer>>() {
                });
        //定义城市流
        DataStream<Tuple2<Integer, String>> cityStream = env.socketTextStream("192.168.72.111", 9999, "\n")
                .map(p -> {
                    //输入格式为:城市ID,城市名称
                    String[] list = p.split(",");
                    return new Tuple2<Integer, String>(Integer.valueOf(list[0]), list[1]);
                })
                .returns(new TypeHint<Tuple2<Integer, String>>() {
                });

        //将城市流定义为广播流
        final MapStateDescriptor<Integer, String> broadcastDesc = new MapStateDescriptor("broad1", Integer.class, String.class);
        BroadcastStream<Tuple2<Integer, String>> broadcastStream = cityStream.broadcast(broadcastDesc);

        DataStream result = textStream.connect(broadcastStream)
                .process(new BroadcastProcessFunction<Tuple2<Integer, Integer>, Tuple2<Integer, String>, Tuple3<Integer, Integer, String>>() {
                    //处理非广播流,关联维度
                    @Override
                    public void processElement(Tuple2<Integer, Integer> value, ReadOnlyContext ctx, Collector<Tuple3<Integer, Integer, String>> out) throws Exception {
                        ReadOnlyBroadcastState<Integer, String> state = ctx.getBroadcastState(broadcastDesc);
                        String cityName = "";
                        if (state.contains(value.f1)) {
                            cityName = state.get(value.f1);
                        }
                        out.collect(new Tuple3<>(value.f0, value.f1, cityName));
                    }

                    @Override
                    public void processBroadcastElement(Tuple2<Integer, String> value, Context ctx, Collector<Tuple3<Integer, Integer, String>> out) throws Exception {
                        System.out.println("收到广播数据:" + value);
                        ctx.getBroadcastState(broadcastDesc).put(value.f0, value.f1);
                    }
                });


        result.print();
        env.execute("joinDemo");
    }
}

4.Temporal table function join

Temporal table是持续变化表上某一时刻的视图,Temporal table function是一个表函数,传递一个时间参数,返回Temporal table这一指定时刻的视图。可以将维度数据流映射为Temporal table,主流与这个Temporal table进行关联,可以关联到某一个版本(历史上某一个时刻)的维度数据。

Temporal table function join的特点如下:

  • 优点:维度数据量可以很大,维度数据更新及时,不依赖外部存储,可以关联不同版本的维度数据。
  • 缺点:只支持在Flink SQL API中使用。
public class JoinDemo5 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //指定是EventTime
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        EnvironmentSettings bsSettings = EnvironmentSettings
                .newInstance()
                .useBlinkPlanner()
                .inStreamingMode()
                .build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env, bsSettings);

        //定义主流
        DataStream<Tuple2<Integer, Integer>> textStream = env.socketTextStream("192.168.72.111", 8888, "\n")
                .map(p -> {
                    //输入格式为:user_id,1000,分别是用户名称和城市编号
                    String[] list = p.split(",");
                    return new Tuple2<Integer, Integer>(Integer.valueOf(list[0]), Integer.valueOf(list[1]));
                })
                .returns(new TypeHint<Tuple2<Integer, Integer>>() {
                });
        //定义城市流
        DataStream<Tuple2<Integer, String>> cityStream = env.socketTextStream("192.168.72.111", 9999, "\n")
                .map(p -> {
                    //输入格式为:城市ID,城市名称
                    String[] list = p.split(",");
                    return new Tuple2<Integer, String>(Integer.valueOf(list[0]), list[1]);
                })
                .returns(new TypeHint<Tuple2<Integer, String>>() {
                });

        //转变为Table
        Table userTable = tableEnv.fromDataStream(textStream, "user_id,city_id,ps.proctime");
        Table cityTable = tableEnv.fromDataStream(cityStream, "city_id,city_name,ps.proctime");

        //定义一个TemporalTableFunction
        //注册表函数
        TemporalTableFunction dimCity = cityTable.createTemporalTableFunction("ps", "city_id");
        tableEnv.createTemporarySystemFunction("dimCity", dimCity);

        //关联查询
        Table result = tableEnv
                .sqlQuery("select u.user_id,u.city_id,d.city_name from " + userTable + " as u " +
                        ", Lateral table (dimCity(u.ps)) d " +
                        "where u.city_id=d.city_id");

        // 左表关联上右表的数据才会打印出来
        //打印输出
        DataStream resultDs = tableEnv.toAppendStream(result, Row.class);
        resultDs.print();
        env.execute("joinDemo");
    }
}

5.几种Join方式的对比

 预加载到内存热存储关联广播维表Temporal table function jsoin
实现复杂度 
维表数据量 
维表更新频率 
维表更新实时性 
维表形式 热存储实时流实时流 
是否依然外部存储

 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值