一、背景与目标
现实中盗刷常见"试探性小额 →紧跟大额采购 "的模式。本文用 Apache Flink DataStream API 构建一个有状态 、低延迟 、可扩展的实时欺诈检测器,实现:
- 按账户维度分区处理;
- 检测"小于 1** 后**紧接着** 出现 **大于 500"的交易;
- V2 版本加入1 分钟时效限制;
- 输出实时告警(本文用日志 Sink 演示,生产可对接 Kafka/ES/报警系统)。
二、环境与项目骨架
先决条件:
- Java 11
- Maven 3.x
用 Archetype 生成骨架(官方 Walkthrough 项目):
bash
mvn archetype:generate \
-DarchetypeGroupId=org.apache.flink \
-DarchetypeArtifactId=flink-walkthrough-datastream-java \
-DarchetypeVersion=2.1.0 \
-DgroupId=frauddetection \
-DartifactId=frauddetection \
-Dversion=0.1 \
-Dpackage=spendreport \
-DinteractiveMode=false
导入 IDE 后可直接运行。骨架已包含:
flink-streaming-java
:流处理核心依赖flink-walkthrough-common
:演示数据源/实体类/日志 Sink
三、数据流 Job 总体结构
数据流由三段组成:
- Source :
TransactionSource
(演示用,生成无限交易流); - KeyBy + Process :按
accountId
分区,FraudDetector
实现告警逻辑; - Sink :
AlertSink
(演示用,输出日志)。
Job 入口
java
public class FraudDetectionJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
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");
}
}
关键点:
keyBy
确保同一账户 的事件进入同一算子实例,以便维护独立状态;- 生产环境可把 Sink 换成 Kafka/ES/告警系统。
四、V1:用 ValueState
实现"小额→大额"模式告警
思路:
为每个账户维护一个 flagState:Boolean
,当遇到小额 交易时置位;下一笔若为大额则告警,并清理标记。否则模式被打断,同样清理。
为什么必须用"有状态"?
- 简单的成员变量无法按 key 隔离,更无法容错(故障重启后丢失);
ValueState
由 Flink 托管,具备按 key 隔离 与容错恢复能力。
关键片段(V1 逻辑)
java
// 获取当前账户的"上一笔是否小额"标记
Boolean lastSmall = flagState.value();
if (lastSmall != 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:引入定时器,限定1 分钟时间窗
新需求: 小额与大额必须在1 分钟内 连续出现才判定欺诈。
实现方式:
- 标记置位时,同时注册"处理时间 + 1 分钟"的定时器;
- 定时器触发时清理标记;
- 如果在到期前模式结束/被打断,需取消定时器。
为什么用定时器?
KeyedProcessFunction
的 TimerService
提供低成本 时间回调;配合状态即可实现状态过期 与时间窗约束。
关键片段(V2 新增)
java
// 置位标记时注册定时器,并把时间戳记到 timerState
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 ts, OnTimerContext ctx, Collector<Alert> out) {
timerState.clear();
flagState.clear();
}
// 封装清理逻辑:删除定时器 + 清空状态
private void cleanUp(Context ctx) throws Exception {
Long timer = timerState.value();
ctx.timerService().deleteProcessingTimeTimer(timer);
timerState.clear();
flagState.clear();
}
处理时间 vs 事件时间 :本文用处理时间 简化演示。若来自 Kafka 的交易存在乱序与延迟,建议切换为事件时间 + 水位线 ,再配合
onTimer
实现更稳健的时效约束。
六、运行与验证(含 IDE 配置与常见错误)
运行方式
- 直接在 IDE 中运行
FraudDetectionJob.main
; - 或打包后用
flink run
提交到本地/集群。
期望日志(演示 Source 下,账户 3 会触发告警)
INFO AlertSink - Alert{id=3}
INFO AlertSink - Alert{id=3}
...
IDE 报错:java.lang.NoClassDefFoundError
- IntelliJ IDEA :
Run > Edit Configurations > Modify options > 勾选 "Include dependencies with 'Provided' scope'"
这样在 IDE 内运行时会把 "provided" 依赖也加入 Classpath。
七、生产化实践清单(Checkpoint/容错/监控)
-
Checkpoint 与状态后端
- 开启周期性 Checkpoint(如 30s);
- 选择合适的状态后端(RocksDB / HashMapStateBackend);
- 配置外部化 Checkpoint/Savepoint 目录(HDFS/S3)。
-
一致性与语义
- Source/Sink 支持两阶段提交时可实现端到端 Exactly-Once;
- Kafka Source + Kafka Sink/数据库 Sink 的行为需核对语义保证。
-
时间语义
- 若存在乱序/延迟:使用事件时间 + 水位线;
- 需要将
ProcessingTimeTimer
替换为EventTimeTimer
并设置assignTimestampsAndWatermarks(...)
。
-
可观测性
- 指标:每分钟告警数、延迟、状态大小、反压;
- 日志与告警通道(飞书/Slack/邮件/SMS)。
-
规则与热更新
- 将金额阈值、时间窗转为外部配置(动态刷新);
- 进一步:抽象规则引擎/DSL,实现多模式与多维度(地域、商户类型、IP 风险)组合。
八、进阶扩展方向
1) 接入 Kafka 实时交易
java
// 伪代码:以 Kafka 作为 Source
FlinkKafkaConsumer<Transaction> consumer = new FlinkKafkaConsumer<>(
"transactions", new TransactionDeserializationSchema(), props);
DataStream<Transaction> transactions = env
.addSource(consumer)
.assignTimestampsAndWatermarks(/* 事件时间水位线策略 */);
2) 用 Flink CEP 做复杂序列模式
- 例如:小额 →(0~1 笔任意交易)→ 大额;
- 多步序列、循环、迭代,CEP 会更直观。
3) 侧输出与多级告警
- 低/中/高三级告警,用
OutputTag
侧输出到不同通道与处理链路。
4) 合规与隐私
- 透明化可解释;
- 脱敏与最小化存取(只保留告警所需字段)。
九、FAQ 与排错速查
-
Q:为何
flagState.value()
会是null
?A:初始未设置或已被
clear()
。ValueState
是可空的,用null
判断"未置位"。 -
Q:为何不直接用成员变量?
A:算子实例会处理多个 key;成员变量既不能按 key 隔离,也无容错。
-
Q:处理时间与事件时间如何选择?
A:无乱序场景可用处理时间;存在乱序/延迟请用事件时间 + 水位线。
-
Q:如何防止"重复告警"?
A:模式被触发或被打断后立即清理状态;必要时可加去重键或冷却时间。
十、完整示例代码
下列代码在骨架项目上可直接运行(演示 Source + 日志 Sink)。实际使用时把 Source/Sink 替换为企业内 Kafka/ES/告警系统即可。
FraudDetectionJob.java
java
package spendreport;
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;
public class FraudDetectionJob {
public static void main(String[] args) throws Exception {
// 获取执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 演示用交易数据源
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");
}
}
FraudDetector.java
(V2:含定时器)
java
package spendreport;
import org.apache.flink.api.common.functions.OpenContext;
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.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;
/**
* 基于"上一笔小额 + 当前大额(1 分钟内)"的欺诈检测器
* - 按账户维度维护状态
* - 置位标记时注册 1 分钟处理时间定时器
* - 定时器触发或模式完成/打断时清理状态
*/
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;
// Keyed State:上一笔是否小额
private transient ValueState<Boolean> flagState;
// Keyed State:定时器时间戳(用于取消)
private transient ValueState<Long> timerState;
@Override
public void open(OpenContext openContext) {
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> out) throws Exception {
// 读取当前账户的"上一笔是否小额"标记
Boolean lastWasSmall = flagState.value();
// 若上一笔为小额,则当前为大额时触发告警;随后清理(包含删除定时器)
if (lastWasSmall != null) {
if (transaction.getAmount() > LARGE_AMOUNT) {
Alert alert = new Alert();
alert.setId(transaction.getAccountId());
out.collect(alert);
}
cleanUp(context);
}
// 当前这笔是小额?置位并注册 1 分钟定时器
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) {
// 超时:未等到大额,清理状态
timerState.clear();
flagState.clear();
}
// 统一清理:删除定时器 + 清空状态
private void cleanUp(Context ctx) throws Exception {
Long timer = timerState.value();
if (timer != null) {
ctx.timerService().deleteProcessingTimeTimer(timer);
}
timerState.clear();
flagState.clear();
}
}
结语
至此,我们完成了一个可工作的实时欺诈检测 应用:V1 用 ValueState
实现模式识别,V2 引入定时器实现时效约束 。在生产中,你可以进一步接入 Kafka、采用事件时间 + 水位线 以适配乱序、引入 Flink CEP 支持更复杂序列模式,并把金额/时间窗/商户类型等做成规则可配置 甚至策略平台化。