为了展示所提供的API,我们将以一个示例开始,然后介绍其完整功能。作为正在运行的示例,我们将使用这样的情况,其中有一系列不同颜色和形状的对象,并且我们希望找到遵循某种模式的相同颜色的对象对,例如矩形后跟三角形。我们假设这组有趣的模式会随着时间而演变。
在此示例中,第一个流将包含Item带有Color和Shape属性的type元素。另一个流将包含Rules。
从流开始Items,我们只需要键入它的Color,因为我们要对相同颜色的。这将确保相同颜色的元素最终出现在同一台物理计算机上。
// key the items by color
KeyedStream<Item, Color> colorPartitionedStream = itemStream
.keyBy(new KeySelector<Item, Color>(){...});
移至Rules,应将包含它们的流广播到所有下游任务,并且这些任务应将它们存储在本地,以便它们可以针对所有传入的Items进行评估。下面的代码段将使用广播规则流,并且使用提供的MapStateDescriptor,将创建存储规则的广播状态。
// a map descriptor to store the name of the rule (string) and the rule itself.
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint<Rule>() {}));
// broadcast the rules and create the broadcast state
BroadcastStream<Rule> ruleBroadcastStream = ruleStream
.broadcast(ruleStateDescriptor);
最后,根据Rules中的Item流中的传入元素评估,我们需要:
BroadcastStream可以通过调用connect()来将流(键控或非键控)与非广播流(以键BroadcastStream为参数)进行连接。我们可以调用特殊类型CoProcessFunction的process()方法,这将返回一个BroadcastConnectedStream。该函数将包含我们的匹配逻辑。函数的确切类型取决于非广播流的类型:
鉴于我们的非广播流已加密,以下代码段包含上述调用:
注意:应该在非广播流上调用connect,并以BroadcastStream作为参数。
DataStream<String> output = colorPartitionedStream
.connect(ruleBroadcastStream)
.process(
// type arguments in our KeyedBroadcastProcessFunction represent:
// 1. the key of the keyed stream
// 2. the type of elements in the non-broadcast side
// 3. the type of elements in the broadcast side
// 4. the type of the result, here a string
new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
// my matching logic
}
);
与CoProcessFunction的情况一样,这些函数有两种实现的处理方法;负责处理广播流进入元件的processBroadcastElement()和用于非广播的processElement()。这些方法的完整签名如下所示:
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
}
public abstract class KeyedBroadcastProcessFunction<KS, IN1, IN2, OUT> {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
public void onTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception;
}
首先要注意的是,这两个功能都需要实现方法processBroadcastElement()和processElement()用于处理广播侧元素和非广播侧元素。
两种方法在提供上下文方面有所不同。非广播方有ReadOnlyContext,而广播方有Context。
这两个上下文(ctx在下面的枚举中):
不同之处在于每个人对广播状态的访问类型。广播方对此具有读写访问权限,而非广播方具有只读访问权限(因此具有名称)。原因是在Flink中没有跨任务通信。因此,为确保我们操作的所有并行实例中,广播状态中的内容相同,我们仅向广播端提供读写访问权限,广播端在所有任务中看到的元素相同,因此我们需要对每个任务进行计算该端的传入元素在所有任务中都相同。忽略此规则将破坏状态的一致性保证,从而导致结果不一致,并且常常难以调试结果。
注意:在所有并行实例中,processBroadcast()中实现的逻辑必须具有相同的确定性行为!
最后,由于KeyedBroadcastProcessFunction事实是在键控流上运行,因此它公开了某些功能,这些功能不适用于BroadcastProcessFunction。那是:
注意:仅在KeyedBroadcastProcessFunction的processElement()处才可以注册计时器。在processBroadcastElement()方法中是不可能的,因为没有与广播元素关联的键。
回到我们的原始示例,我们KeyedBroadcastProcessFunction可能如下所示:
new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
// store partial matches, i.e. first elements of the pair waiting for their second element
// we keep a list as we may have many first elements waiting
private final MapStateDescriptor<String, List<Item>> mapStateDesc =
new MapStateDescriptor<>(
"items",
BasicTypeInfo.STRING_TYPE_INFO,
new ListTypeInfo<>(Item.class));
// identical to our ruleStateDescriptor above
private final MapStateDescriptor<String, Rule> ruleStateDescriptor =
new MapStateDescriptor<>(
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint<Rule>() {}));
@Override
public void processBroadcastElement(Rule value,
Context ctx,
Collector<String> out) throws Exception {
ctx.getBroadcastState(ruleStateDescriptor).put(value.name, value);
}
@Override
public void processElement(Item value,
ReadOnlyContext ctx,
Collector<String> out) throws Exception {
final MapState<String, List<Item>> state = getRuntimeContext().getMapState(mapStateDesc);
final Shape shape = value.getShape();
for (Map.Entry<String, Rule> entry :
ctx.getBroadcastState(ruleStateDescriptor).immutableEntries()) {
final String ruleName = entry.getKey();
final Rule rule = entry.getValue();
List<Item> stored = state.get(ruleName);
if (stored == null) {
stored = new ArrayList<>();
}
if (shape == rule.second && !stored.isEmpty()) {
for (Item i : stored) {
out.collect("MATCH: " + i + " - " + value);
}
stored.clear();
}
// there is no else{} to cover if rule.first == rule.second
if (shape.equals(rule.first)) {
stored.add(value);
}
if (stored.isEmpty()) {
state.remove(ruleName);
} else {
state.put(ruleName, stored);
}
}
}
}
描述了提供的API之后,本节重点介绍使用广播状态时要记住的重要事项。这些是: