前言
Flink常见的维表Join方式有四种:
- 预加载维表
- 热存储维表
- 广播维表
- 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 | ||
实现复杂度 | 低 | 中 | 低 | 低 | |
维表数据量 | 低 | 高 | 低 | 高 | |
维表更新频率 | 低 | 中 | 高 | 高 | |
维表更新实时性 | 低 | 中 | 高 | 高 | |
维表形式 | 热存储 | 实时流 | 实时流 | ||
是否依然外部存储 | 低 | 是 | 否 | 否 |