假设学校的财务系统要出一个新功能,类似于年度账单。统计每个学生过去一年往一卡通中的总充值金额。
其实这种需求完全不用开窗,可以直接使用批处理,groupBy()后reduce()即可。
当然,也可以使用流处理通过开窗实现聚合。下面分别介绍。
public static void main(String[] args) throws Exception {
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
List list = new ArrayList<>();
list.add(new Deposit(1,100));
list.add(new Deposit(2,100));
list.add(new Deposit(3,100));
list.add(new Deposit(1,50));
list.add(new Deposit(2,60));
list.add(new Deposit(1,60));
list.add(new Deposit(1,50));
DataSource source = env.fromCollection(list);
//这两种只支持Tuple类型的数据
//source.aggregate(Aggregations.SUM,1);
//source.sum(1);
ReduceOperator reduceMoney = source.groupBy("studentID")
.reduce(new ReduceFunction() {
@Override
public Deposit reduce(Deposit value1, Deposit value2) throws Exception {
value1.setMoney(value1.getMoney() + value2.getMoney());
return value1;
}
});
reduceMoney.print();
}
public static class Deposit{
private int studentID;
private float money;
private String dateTime;
public Deposit() {
}
public Deposit(final int studentID, final float money) {
this.studentID = studentID;
this.money = money;
}
public Deposit(final int studentID, final float money, final String dateTime) {
this.studentID = studentID;
this.money = money;
this.dateTime = dateTime;
}
public int getStudentID() {
return this.studentID;
}
public void setStudentID(final int studentID) {
this.studentID = studentID;
}
public float getMoney() {
return this.money;
}
public void setMoney(final float money) {
this.money = money;
}
public String getDateTime() {
return this.dateTime;
}
public void setDateTime(final String dateTime) {
this.dateTime = dateTime;
}
@Override
public String toString() {
return "Deposit{" +
"studentID=" + studentID +
", money=" + money +
'}';
}
}
结果:
Deposit{studentID=1, money=260.0}
Deposit{studentID=2, money=160.0}
Deposit{studentID=3, money=100.0}
小结:
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
List list = new ArrayList<>();
list.add(new BatchReduce.Deposit(1,100));
list.add(new BatchReduce.Deposit(2,100));
list.add(new BatchReduce.Deposit(3,100));
list.add(new BatchReduce.Deposit(1,50));
list.add(new BatchReduce.Deposit(2,60));
list.add(new BatchReduce.Deposit(1,60));
list.add(new BatchReduce.Deposit(1,50));
DataStreamSource source = env.fromCollection(list);
SingleOutputStreamOperator sum = source.keyBy("studentID")
.sum("money");
sum.print();
env.execute("stream reduce job");
}
结果:
6> Deposit{studentID=2, money=100.0}
6> Deposit{studentID=3, money=100.0}
5> Deposit{studentID=1, money=100.0}
5> Deposit{studentID=1, money=150.0}
5> Deposit{studentID=1, money=210.0}
5> Deposit{studentID=1, money=260.0}
6> Deposit{studentID=2, money=160.0}
小结:
窗口根据不同的标准可以做不同的划分,按照是否是keyed stream可以分成window和windowAll两种;这两种类型下按照开窗条件划分又有基于时间的timewindow/timeWindowAll,也有基于数量的countwindow/countWindowAll。
其中windowAll类型的窗口是单并行度的。
这里因为要根据studentID分组,因此采用的是countwindow。
窗口的聚合函数也有多种,对于每种的具体用法可以看官网,也可以看源码:
其中reduce和aggregate是分别需要传入自定义的ReduceFunction和AggregateFunction,这两种窗口函数采用的是递增聚合的方式,比全量缓存聚合函数ProcessWindowFunction要高效,性能也好。这个在另一篇也有介绍。
对于sum()来说,底层采用的也是aggregate()方法。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
List list = new ArrayList<>();
list.add(new BatchReduce.Deposit(1,100));
list.add(new BatchReduce.Deposit(2,100));
list.add(new BatchReduce.Deposit(3,100));
list.add(new BatchReduce.Deposit(1,50));
list.add(new BatchReduce.Deposit(2,60));
list.add(new BatchReduce.Deposit(1,60));
list.add(new BatchReduce.Deposit(1,50));
DataStreamSource source = env.fromCollection(list);
SingleOutputStreamOperator sum = source.keyBy("studentID")
.countWindow(2)
.sum("money");
sum.print();
env.execute("stream reduce job");
}
结果:
6> Deposit{studentID=2, money=160.0}
5> Deposit{studentID=1, money=150.0}
5> Deposit{studentID=1, money=110.0}
可以看到,因为设置了窗口大小为2,对于id为3的同学由于只有一条数据,因此达不到触发条件,导致数据“丢失”。
而对于id为1的学生,总共有四条数据,因此开了两个窗口,但是只返回了当前窗口的计算结果,没有累加所有窗口的结果,也不满足需求。
这个我们可以通过countWindow的源码证明,默认的触发机制是窗口元素数量:
可以看到countWindow创建的是全局窗口GlobalWindows,并指定了触发器PurgingTrigger(全局窗口必须指定触发器,默认是永远不触发的)。
其中PurgingTrigger类源码如下:
可以看到,PurgingTrigger类起到的类似于转换作用,就是将传入的任何触发器转换成一个purging类型的触发器,返回FIRE_AND_PURGE(触发计算,然后清除窗口内的元素)。
下面看一下CountTrigger触发器的源码,看一下触发器是如何定义的:
/**
* A {@link Trigger} that fires once the count of elements in a pane reaches the given count.
*
* @param The type of {@link Window Windows} on which this trigger can operate.
*/
@PublicEvolving
public class CountTrigger extends Trigger
可以看到,它的主要部分就是onElement()方法,用了一个ReducingStateDescriptor状态数据来对窗口中的数据量进行累加,当数据量达到指定的窗口大小时,就会clear清空状态数据并触发窗口函数。
对于onEventTime()和onProcessingTime()都是返回的TriggerResult.CONTINUE,也就是不触发。
小结:
下面先从解决不同per-window的数据无法汇总开始。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
List list = new ArrayList<>();
list.add(new BatchReduce.Deposit(1,100));
list.add(new BatchReduce.Deposit(2,100));
list.add(new BatchReduce.Deposit(3,100));
list.add(new BatchReduce.Deposit(1,50));
list.add(new BatchReduce.Deposit(2,60));
list.add(new BatchReduce.Deposit(1,60));
list.add(new BatchReduce.Deposit(1,50));
DataStreamSource source = env.fromCollection(list);
/*SingleOutputStreamOperator sum = source.keyBy("studentID")
.countWindow(2)
.sum("money");*/
SingleOutputStreamOperator sum = source.keyBy(new KeySelector() {
@Override
public Integer getKey(BatchReduce.Deposit value) throws Exception {
return value.getStudentID();
}
})
.countWindow(2)
.process(new ProcessWindowFunction() {
private ValueState> valueState;
@Override
public void open(Configuration parameters){
// 创建 ValueStateDescriptor
ValueStateDescriptor descriptor = new ValueStateDescriptor("depositSumStateDesc",
TypeInformation.of(new TypeHint>() {}));
// 基于 ValueStateDescriptor 创建 ValueState
valueState = getRuntimeContext().getState(descriptor);
}
@Override
public void process(Integer tuple, Context context, Iterable elements, Collector out) throws Exception {
context.windowState();
Tuple2 currentState = valueState.value();
// 初始化 ValueState 值
if (null == currentState) {
currentState = new Tuple2<>(elements.iterator().next().getStudentID(), 0f);
}
float sum = 0f;
for (BatchReduce.Deposit deposit:elements){
sum += deposit.getMoney();
}
currentState.f1 = currentState.f1 + sum;
// 更新 ValueState 值
valueState.update(currentState);
BatchReduce.Deposit deposit = new BatchReduce.Deposit();
deposit.setStudentID(currentState.f0);
deposit.setMoney(currentState.f1);
out.collect(deposit);
}
});
sum.print();
env.execute("stream reduce job");
}
结果:
6> Deposit{studentID=2, money=160.0}
5> Deposit{studentID=1, money=150.0}
5> Deposit{studentID=1, money=260.0}
可以看到,对于id为1的同学,第二个窗口输出的汇总结果是包含第一个窗口的汇总数据的。
但对于id为3的同学来说,由于不满足默认触发器的触发条件,导致一直不输出。
下面就通过自定义触发器解决这个问题,让在满足数据量或满足超时时间时,触发窗口函数。
(暂未完成。。。)