目录
数据分为无界流和有界流
事件驱动型
官方定义
特点
传统事件驱动型应用和flink 流式事件驱动应用对比
举例:欺诈检测
描述
实现
实现(要求大额消费与前一个小额消费时间间隔小于1min, 大额消费与小额消费即使是连续的,但如果间隔大于1min,也不判定为欺诈行为)
flink maven工程遇到的两个问题
数据分为无界流和有界流
Flink官网:
数据可以分为“有界流”或者“无界流”来处理。
- “无界流” 有定义流的开始,但没有定义流的结束。它们会无休止地产生数据。无界流的数据必须持续处理,即数据被摄取后需要立刻处理。我们不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。处理无界数据通常要求以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。
- “有界流” ,有定义流的开始,也有定义流的结束。有界流可以在摄取所有数据后再进行计算。有界流所有数据可以被排序,所以并不需要有序摄取。有界流处理通常被称为批处理。
个人理解:无界流的数据在源源不断的产生,而当你只分析取某个时间范围内的数据,好比昨天的交易,来计算交易总金额,处理的数据实际是无界数据中的一段有界数据;
Flink支持流式处理和批处理,其中,流式处理的应用类型分为三种:
事件驱动型
官方定义
事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。
个人理解:通过流式处理接收数据,监控数据来及时触发某个事件。这个处理的过程中支持持续记录保存的状态。
特点
1. 如何触发来源于接收的数据流,无需连接远程数据库,应用只需访问本地(内存或磁盘)即可获取流式数据;
2. 能够定期向远程持久化存储写入 checkpoint,保证系统的容错性
传统事件驱动型应用和flink 流式事件驱动应用对比
举例:欺诈检测
描述
信用卡欺诈行为:罪犯可以通过诈骗或者入侵安全级别较低系统来盗窃信用卡卡号。 用盗得的信用卡进行很小额度的例如一美元或者更小额度的消费进行测试。 如果测试消费成功,那么他们就会用这个信用卡进行大笔消费,来购买一些他们希望得到的,或者可以倒卖的财物。实现一个 flink应用,从持续不断地消费记录数据中分析出前一个账户出现先进行小额消费,再进行大额消费的行为。
如图,假设出现小于 $1 美元的交易后紧跟着一个大于 $500 的交易,就输出一个报警信息。那么交易记录 3 和 4可以输出一个,而 交易记录7 和 9虽然也是先小额再大额,但是间隔了消费记录8,就不生成 alert 。
实现
1. 构建maven工程,需要的依赖:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-java -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.12</artifactId>
<version>1.12.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-core -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-core</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>1.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.12</artifactId>
<version>1.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-walkthrough-common -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-walkthrough-common_2.12</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.2</version>
</dependency>
</dependencies>
2. 欺诈检测主流程类
package com.my.flink.fraud_dedector;
import com.sun.org.glassfish.gmbal.Description;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;
import org.apache.flink.walkthrough.common.sink.AlertSink;
import org.apache.flink.walkthrough.common.source.TransactionSource;
@Description("定义程序的数据流")
public class FraudDedectorJob {
public static void main(String[] args) throws Exception {
// 设置任务的执行环境,用于定义任务的属性、创建数据源以及最终启动任务的执行。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 创建数据源,从外部系统例如 Apache Kafka、Rabbit MQ 或者 Apache Pulsar 接收数据,然后将数据送到 Flink 程序中。
DataStream<Transaction> trans = env.addSource(new TransactionSource()).name("transactions");
// 将数据源的数据通过 keyBy 划分到多个并发的欺诈检测处理。这里以交易数据的账户作为 key拆分数据,保证一个账户的数据由一个task处理
// .process()定义拆分后的数据都经过 FraudDetector中定义的processElement函数处理
DataStream<Alert> alerts = trans.keyBy(Transaction::getAccountId).process(new FraudDetector()).name("fraud-detector");
// 输出结果到外部系统
alerts.addSink(new AlertSink());
// 开始运行任务,定义一个任务名叫 Fraud Detection
env.execute("Fraud Detection");
}
}
3. 对每条消费数据处理的欺诈监测核心类 FraudDetector。处理数据的类继承了 KeyedProcessFunction,核心方法是 processElement
- 在欺诈检测主流程类 FraudDedetorJob 中,指定了数据处理函数 process ,传入了一个 FraudDedector类(是 KeyedProcessFunction的子类),则对数据流中的每个数据都会执行 FraudDedector的processElement函数
- Flink 提供了不同的流数据处理类,如 KeyedProcessFunction、CoProcessFunctions、
BroadcastProcessFunctions
等。这里用的是针对 数据通过 keyBy按照 key拆分后的流的处理类 KeyedProcessFunction;继承Flink提供的这个类,实现自己需要的 processElement 方法来处理流数据。 - 处理流数据时需要知道上一条数据/处理到上一条数据的结果是什么样的。Flink 提供了 ValueState 类型来针对基于 key拆分的数据流的状态记录类型。valueState的作用域是属于它所属的key的,即按照key 拆分数据流后的各个key不同的数据流拥有自己的valueState来记录状态;
- valueState需要在 open()函数中注册状态,使用 update()来更新值、value()来获取值,默认值为 null, 通过 clear()可以清空 valueState设置value为null
package com.my.flink.fraud_dedector;
import com.sun.org.glassfish.gmbal.Description;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;
@Description("欺诈行为定义类")
public class FraudDetector extends KeyedProcessFunction<Long, Transaction, Alert> {
private static final long serialVersionUID = 1L;
private static final double SMALL_AMOUNT = 1.00;
private static final double LARGE_AMOUNT = 500.00;
private static final long ONE_MINUTE = 60 * 1000;
private transient ValueState<Boolean> flagState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Boolean> flagDescriptor = new ValueStateDescriptor<>(
"flag",
Types.BOOLEAN);
flagState = getRuntimeContext().getState(flagDescriptor);
}
@Override
public void processElement(
Transaction transaction,
Context context,
Collector<Alert> collector) throws Exception {
// 获取上一个数据记录的状态
Boolean lastTransactionWasSmall = flagState.value();
// 检查 flagState 即上条交易数据是否为小额交易:lastTransactionWasSmall为 true则为小额,否则不是小额
if (lastTransactionWasSmall != null) {
if (transaction.getAmount() > LARGE_AMOUNT) {
// 上次为小额交易lastTransactionWasSmall非空,此次为大额交易,则 alert
Alert alert = new Alert();
alert.setId(transaction.getAccountId());
collector.collect(alert);
// 这里比官方代码多了一条控制台输出,可以直接看到生成的 alert
System.out.println(alert.toString());
}
// 清空上次交易数据的状态,如果后续此次为小额交易,则会被设置为 true, 非小额则flagState会保持为空
flagState.clear();
}
if (transaction.getAmount() < SMALL_AMOUNT) {
// 判断本次交易是否为小额交易,是的话打上标签,不是则不设置
flagState.update(true);
}
}
}
4. 可以看到 控制台输出的 alert ,账户Id=3的消费记录存在欺诈行为
实现(要求大额消费与前一个小额消费时间间隔小于1min, 大额消费与小额消费即使是连续的,但如果间隔大于1min,也不判定为欺诈行为)
这就需要借助定时器来记录此次大额消费与上一个小额消费之间的时间间隔;
在一个小额消费出现时,注册一个定时器。定时器被触发时(1min到)会清除记录的小额消费 flagState。 当定时器触发前出现与小额消费连续的一次大额消费的时候,定时器将重新开始定时,flagState置空,等待开始计算下一个1min
Flink 中的 KeyedProcessFunction
支持设置计时器,该计时器在将来的某个时间点执行回调函数。
新的 processFunction实现类为 FraudDetectorWithTimer
package com.my.flink.fraud_dedector;
import com.sun.org.glassfish.gmbal.Description;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;
import java.io.IOException;
@Description("欺诈行为定义类")
public class FraudDetectorWithTimer extends KeyedProcessFunction<Long, Transaction, Alert> {
private static final long serialVersionUID = 1L;
private static final double SMALL_AMOUNT = 1.00;
private static final double LARGE_AMOUNT = 500.00;
private static final long ONE_MINUTE = 60 * 1000;
// java 的transient关键字为我们提供了便利,
// 你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中
private transient ValueState<Boolean> flagState;
private transient ValueState<Long> timerState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Boolean> flagDescriptor = new ValueStateDescriptor<>(
"flag",
Types.BOOLEAN);
flagState = getRuntimeContext().getState(flagDescriptor);
ValueStateDescriptor<Long> timerDescriptor = new ValueStateDescriptor<>(
"timer-state",
Types.LONG);
timerState = getRuntimeContext().getState(timerDescriptor);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Alert> out) throws Exception {
// 定时器处罚时,应当清空 小额消费标记和本身
flagState.clear();
timerState.clear();
}
@Override
public void processElement(
Transaction transaction,
Context context,
Collector<Alert> collector) throws Exception {
// 获取上一个数据记录的状态
Boolean lastTransactionWasSmall = flagState.value();
// 检查 flagState 即上条交易数据是否为小额交易:lastTransactionWasSmall为 true则为小额,否则不是小额
if (lastTransactionWasSmall != null) {
if (transaction.getAmount() > LARGE_AMOUNT) {
// 上次为小额交易lastTransactionWasSmall非空,此次为大额交易,则 alert
Alert alert = new Alert();
alert.setId(transaction.getAccountId());
collector.collect(alert);
// 这里比官方代码多了一条控制台输出,可以直接看到生成的 alert
System.out.println(alert.toString() );
}
// 清空上次交易数据的状态,包括上次是否为小额交易的 flagState及删除上次的定时器和 timeState记录
cleanUp(context);
}
if (transaction.getAmount() < SMALL_AMOUNT) {
// 判断本次交易是否为小额交易,是的话打上标签,不是则不设置
flagState.update(true);
// 注册当前交易为小额交易时,注册定时器并记录下定时器设置的时间为 timeState
long timer = context.timerService().currentProcessingTime() + ONE_MINUTE;
context.timerService().registerProcessingTimeTimer(timer);
timerState.update(timer);
}
}
private void cleanUp(Context ctx) throws IOException {
// 删除已注册的timer定时器
Long timer = timerState.value();
ctx.timerService().deleteProcessingTimeTimer(timer);
// 清空保存的小额交易的状态及触发时间状态
timerState.clear();
flagState.clear();
}
}
flink maven工程遇到的两个问题
java.lang.NoClassDefFoundError: org/apache/flink/streaming/api/functions/source/SourceFunction
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2688)
at java.lang.Class.privateGetMethodRecursive(Class.java:3035)
at java.lang.Class.getMethod0(Class.java:3005)
at java.lang.Class.getMethod(Class.java:1771)
at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
Caused by: java.lang.ClassNotFoundException: org.apache.flink.streaming.api.functions.source.SourceFunction
at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
Exception in thread "main"
解决办法
2. 第二个问题
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" java.lang.Exception: Could not create actor system
at org.apache.flink.runtime.clusterframework.BootstrapTools.startLocalActorSystem(BootstrapTools.java:263)
at org.apache.flink.runtime.rpc.akka.AkkaRpcServiceUtils$AkkaRpcServiceBuilder.createAndStart(AkkaRpcServiceUtils.java:341)
at org.apache.flink.runtime.minicluster.MiniCluster.createLocalRpcService(MiniCluster.java:792)
at org.apache.flink.runtime.minicluster.MiniCluster.start(MiniCluster.java:273)
at org.apache.flink.client.program.PerJobMiniClusterFactory.submitJob(PerJobMiniClusterFactory.java:74)
at org.apache.flink.client.deployment.executors.LocalExecutor.execute(LocalExecutor.java:81)
at org.apache.flink.streaming.api.environment.StreamExecutionEnvironment.executeAsync(StreamExecutionEnvironment.java:1940)
at org.apache.flink.streaming.api.environment.StreamExecutionEnvironment.execute(StreamExecutionEnvironment.java:1836)
at org.apache.flink.streaming.api.environment.LocalStreamEnvironment.execute(LocalStreamEnvironment.java:70)
at org.apache.flink.streaming.api.environment.StreamExecutionEnvironment.execute(StreamExecutionEnvironment.java:1822)
at com.my.flink.fraud_dedector.FraudDedectorJob.main(FraudDedectorJob.java:26)
Caused by: java.lang.VerifyError: Uninitialized object exists on backward branch 209
Exception Details:
Location:
scala/collection/immutable/HashMap$HashTrieMap.split()Lscala/collection/immutable/Seq; @249: goto
Reason:
Error exists in the bytecode
Bytecode:
0x0000000: 2ab6 0064 04a0 001e b200 c1b2 00c6 04bd
0x0000010: 0002 5903 2a53 c000 c8b6 00cc b600 d0c0
0x0000020: 00d2 b02a b600 38b8 0042 3c1b 04a4 0156
0x0000030: 1b05 6c3d 2a1b 056c 2ab6 0038 b700 d43e
0x0000040: 2ab6 0038 021d 787e 3604 2ab6 0038 0210
0x0000050: 201d 647c 7e36 05bb 0019 59b2 00c6 2ab6
0x0000060: 003a c000 c8b6 00d8 b700 db1c b600 df3a
0x0000070: 0619 06c6 001a 1906 b600 e3c0 008b 3a07
0x0000080: 1906 b600 e6c0 008b 3a08 a700 0dbb 00e8
0x0000090: 5919 06b7 00eb bf19 073a 0919 083a 0abb
0x00000a0: 0002 5915 0419 09bb 0019 59b2 00c6 1909
0x00000b0: c000 c8b6 00d8 b700 db03 b800 f13a 0e3a
0x00000c0: 0d03 190d b900 f501 0019 0e3a 1136 1036
0x00000d0: 0f15 0f15 109f 0027 150f 0460 1510 190d
0x00000e0: 150f b900 f802 00c0 0005 3a17 1911 1917
0x00000f0: b800 fc3a 1136 1036 0fa7 ffd8 1911 b801
0x0000100: 00b7 0069 3a0b bb00 0259 1505 190a bb00
0x0000110: 1959 b200 c619 0ac0 00c8 b600 d8b7 00db
0x0000120: 03b8 00f1 3a13 3a12 0319 12b9 00f5 0100
0x0000130: 1913 3a16 3615 3614 1514 1515 9f00 2715
0x0000140: 1404 6015 1519 1215 14b9 00f8 0200 c000
0x0000150: 053a 1819 1619 18b8 0103 3a16 3615 3614
0x0000160: a7ff d819 16b8 0100 b700 693a 0cbb 0105
0x0000170: 5919 0bbb 0105 5919 0cb2 010a b701 0db7
0x0000180: 010d b02a b600 3a03 32b6 010f b0
Stackmap Table:
same_frame(@35)
full_frame(@141,{Object[#2],Integer,Integer,Integer,Integer,Integer,Object[#118]},{})
append_frame(@151,Object[#139],Object[#139])
full_frame(@209,{Object[#2],Integer,Integer,Integer,Integer,Integer,Object[#118],Object[#139],Object[#139],Object[#139],Object[#139],Top,Top,Object[#25],Object[#62],Integer,Integer,Object[#116]},{Uninitialized[#159],Uninitialized[#159],Integer,Object[#139]})
full_frame(@252,{Object[#2],Integer,Integer,Integer,Integer,Integer,Object[#118],Object[#139],Object[#139],Object[#139],Object[#139],Top,Top,Object[#25],Object[#62],Integer,Integer,Object[#116]},{Uninitialized[#159],Uninitialized[#159],Integer,Object[#139]})
full_frame(@312,{Object[#2],Integer,Integer,Integer,Integer,Integer,Object[#118],Object[#139],Object[#139],Object[#139],Object[#139],Object[#2],Top,Object[#25],Object[#62],Integer,Integer,Object[#116],Object[#25],Object[#62],Integer,Integer,Object[#116]},{Uninitialized[#262],Uninitialized[#262],Integer,Object[#139]})
full_frame(@355,{Object[#2],Integer,Integer,Integer,Integer,Integer,Object[#118],Object[#139],Object[#139],Object[#139],Object[#139],Object[#2],Top,Object[#25],Object[#62],Integer,Integer,Object[#116],Object[#25],Object[#62],Integer,Integer,Object[#116]},{Uninitialized[#262],Uninitialized[#262],Integer,Object[#139]})
full_frame(@387,{Object[#2],Integer},{})
at scala.collection.immutable.HashMap$.scala$collection$immutable$HashMap$$makeHashTrieMap(HashMap.scala:181)
at scala.collection.immutable.HashMap$HashMap1.updated0(HashMap.scala:216)
at scala.collection.immutable.HashMap.updated(HashMap.scala:58)
at scala.collection.immutable.Map$Map4.updated(Map.scala:224)
at scala.collection.immutable.Map$Map4.$plus(Map.scala:225)
at scala.collection.immutable.Map$Map4.$plus(Map.scala:197)
at scala.collection.mutable.MapBuilder.$plus$eq(MapBuilder.scala:29)
at scala.collection.mutable.MapBuilder.$plus$eq(MapBuilder.scala:25)
at scala.collection.TraversableOnce.$anonfun$toMap$1(TraversableOnce.scala:316)
at scala.collection.TraversableOnce$$Lambda$130/379645464.apply(Unknown Source)
at scala.collection.immutable.List.foreach(List.scala:388)
at scala.collection.TraversableOnce.toMap(TraversableOnce.scala:315)
at scala.collection.TraversableOnce.toMap$(TraversableOnce.scala:313)
at scala.collection.AbstractTraversable.toMap(Traversable.scala:104)
at scala.concurrent.duration.Duration$.<init>(Duration.scala:88)
at scala.concurrent.duration.Duration$.<clinit>(Duration.scala)
at akka.util.Helpers$ConfigOps$.getDuration$extension(Helpers.scala:138)
at akka.util.Helpers$ConfigOps$.getMillisDuration$extension(Helpers.scala:133)
at akka.actor.ActorSystem$Settings.<init>(ActorSystem.scala:329)
at akka.actor.ActorSystemImpl.<init>(ActorSystem.scala:686)
at akka.actor.RobustActorSystem.<init>(RobustActorSystem.scala:47)
at akka.actor.RobustActorSystem$.internalApply(RobustActorSystem.scala:96)
at akka.actor.RobustActorSystem$.apply(RobustActorSystem.scala:70)
at akka.actor.RobustActorSystem$.create(RobustActorSystem.scala:55)
at org.apache.flink.runtime.akka.AkkaUtils$.createActorSystem(AkkaUtils.scala:125)
at org.apache.flink.runtime.akka.AkkaUtils.createActorSystem(AkkaUtils.scala)
at org.apache.flink.runtime.clusterframework.BootstrapTools.startActorSystem(BootstrapTools.java:276)
at org.apache.flink.runtime.clusterframework.BootstrapTools.startLocalActorSystem(BootstrapTools.java:260)
... 10 more
Process finished with exit code 1
解决办法:jdk问题,提升 jdk的版本; 原来是 jdk_1.8,升级到 1.11 就不报错了