此实例来自于Flink官网 Fraud Detection with the DataStream API | Apache Flink
一、场景
-
在当今数字时代,信用卡欺诈行为越来越被重视。
-
罪犯可以通过诈骗或者入侵安全级别较低系统来盗窃信用卡卡号。
-
用盗得的信用卡进行很小额度的例如一美元或者更小额度的消费进行测试。 如果测试消费成功,那么他们就会用这个信用卡进行大笔消费,来购买一些他们希望得到的,或者可以倒卖的财物。
package spendreport;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.walkthrough.common.sink.AlertSink;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;
import org.apache.flink.walkthrough.common.source.TransactionSource;
public class FraudDetectionJob {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//创建数据源 信用卡 ID (accountId),交易发生的时间 (timestamp) 以及交易的金额(amount)
DataStream<Transaction> transactions = env
.addSource(new TransactionSource())
.name("transactions");
//按照账户进行分类
DataStream<Alert> alerts = transactions
.keyBy(Transaction::getAccountId)
//欺诈检测函数
.process(new FraudDetector())
.name("fraud-detector");
//发送告警信息
alerts
.addSink(new AlertSink())
.name("send-alerts");
env.execute("Fraud Detection");
}
}
package spendreport;
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;
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;
@Override
public void processElement(Transaction transaction,Context context,Collector<Alert> collector) throws Exception {
Alert alert = new Alert();
//初始函数 只要有有交易记录就会触发告警
alert.setId(transaction.getAccountId());
collector.collect(alert);
}
}
二、V1.0
-
对于一个账户,如果出现小于 $1 美元的交易后紧跟着一个大于 $500 的交易,就输出一个报警信息
-
欺诈检测器需要在多个交易事件之间记住一些信息
-
仅当一个大额的交易紧随一个小额交易的情况发生时,这个大额交易才被认为是欺诈交易。 在多个事件之间存储信息就需要使用到状态
-
最直接的实现方式是使用一个 boolean 型的标记状态来表示是否刚处理过一个小额交易。 当处理到该账户的一个大额交易时,你只需要检查这个标记状态来确认上一个交易是是否小额交易即可。
-
然而,仅使用一个标记作为
FraudDetector
的类成员来记录账户的上一个交易状态是不准确的。 Flink 会在同一个FraudDetector
的并发实例中处理多个账户的交易数据-
假设,当账户 A 和账户 B 的数据被分发的同一个并发实例上处理时,账户 A 的小额交易行为可能会将标记状态设置为真,随后账户 B 的大额交易可能会被误判为欺诈交易。
-
当然,我们可以使用如
Map
这样的数据结构来保存每一个账户的状态,但是常规的类成员变量是无法做到容错处理的,当任务失败重启后,之前的状态信息将会丢失。 这样的话,如果程序曾出现过失败重启的情况,将会漏掉一些欺诈报警。
-
-
为了应对这个问题,Flink 提供了一套支持容错状态的原语,这些原语几乎与常规成员变量一样易于使用。Flink 中最基础的状态类型是
ValueState
,这是一种能够为被其封装的变量添加容错能力的类型。ValueState
是一种 keyed state,也就是说它只能被用于 keyed context 提供的 operator 中,即所有能够紧随DataStream#keyBy
之后被调用的operator。 一个 operator 中的 keyed state 的作用域默认是属于它所属的 key 的。-
这个例子中,key 就是当前正在处理的交易行为所属的信用卡账户(key 传入 keyBy() 函数调用),而
FraudDetector
维护了每个帐户的标记状态。ValueState
需要使用ValueStateDescriptor
来创建,ValueStateDescriptor
包含了 Flink 如何管理变量的一些元数据信息。状态在使用之前需要先被注册。 状态需要使用open()
函数来注册状态。
-
-
public class FraudDetector extends KeyedProcessFunction<Long, Transaction, Alert> { private static final long serialVersionUID = 1L; 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(); //上一笔交易时小额交易 if (lastTransactionWasSmall != null) { //大额交易 if (transaction.getAmount() > LARGE_AMOUNT) { Alert alert = new Alert(); alert.setId(transaction.getAccountId()); //输出告警信息 collector.collect(alert); } //清空状态 flagState.clear(); } //小额交易 if (transaction.getAmount() < SMALL_AMOUNT) { //更新状态 flagState.update(true); } } }
三、V2.0 状态 + 时间
-
骗子们在小额交易后不会等很久就进行大额消费,这样可以降低小额测试交易被发现的几率。
-
比如,假设你为欺诈检测器设置了一分钟的超时
-
当标记状态被设置为
true
时,设置一个在当前时间一分钟后触发的定时器。 -
当定时器被触发时,重置标记状态。
-
当标记状态被重置时,删除定时器
-
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;
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;
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 processElement(Transaction transaction,Context context,Collector<Alert> collector) throws Exception {
//获取状态
Boolean lastTransactionWasSmall = flagState.value();
if (lastTransactionWasSmall != null) {
//超过大额交易
if (transaction.getAmount() > LARGE_AMOUNT) {
//输出告警信息
Alert alert = new Alert();
alert.setId(transaction.getAccountId());
collector.collect(alert);
}
//清空状态
cleanUp(context);
}
//小于最小交易额
if (transaction.getAmount() < SMALL_AMOUNT) {
//改变状态值
flagState.update(true);
//注册定时器
long timer = context.timerService().currentProcessingTime() + ONE_MINUTE;
context.timerService().registerProcessingTimeTimer(timer);
timerState.update(timer);
}
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Alert> out) {
// remove flag after 1 minute
timerState.clear();
flagState.clear();
}
private void cleanUp(Context ctx) throws Exception {
// delete timer
Long timer = timerState.value();
//删除定时器
ctx.timerService().deleteProcessingTimeTimer(timer);
//清空所有状态
timerState.clear();
flagState.clear();
}
}