3. 金融行业(二)
各位好,欢迎来到金融行业的第二讲。上一章我们聊了银行核心账户体系,今天咱们来点更刺激的——实时风控。
说实话,金融风控是我个人觉得最有挑战性的场景之一。你想想看,用户点一下“转账”按钮,后台要在几十毫秒内判断这笔交易是不是诈骗、有没有盗刷、会不会洗钱。慢了不行,误判了更不行。我当年在支付公司做第一个实时风控项目时,上线第一天就被业务方追着问:“你们系统到底能不能扛住双十一?”嗯,那会儿心里是真没底。
3.1 实时风控的核心挑战
先看几个数字。一个中等规模的支付平台,每天的交易量大概在几千万笔。每笔交易从进入系统到返回结果,留给风控的时间窗口通常不超过100毫秒。在这100毫秒里,你要完成数据清洗、规则匹配、模型推理、风险打分、结果回传——说白了,就是一场与时间的赛跑。
| 指标 | 传统批处理 | 实时流处理 |
|---|---|---|
| 延迟 | 分钟~小时级 | 毫秒~秒级 |
| 吞吐量 | 受限于批窗口 | 百万级TPS |
| 状态管理 | 依赖外部DB | 内置状态后端 |
| 容错机制 | 重跑整个批次 | 精确一次语义 |
传统做法是每天跑一次离线规则,第二天出黑名单。但现在的欺诈手段,从交易发生到资金转移,可能只需要几秒钟。所以,实时流处理成了唯一的选择。
3.2 为什么选Flink?
市面上能做实时计算的框架不少,Storm、Spark Streaming、Flink……我个人习惯用Flink,原因有三:
- 真正的流处理:Flink是逐条处理数据,而不是微批次。延迟能压到个位数毫秒。
- 强大的状态管理:风控需要维护用户画像、设备指纹、历史行为等状态。Flink的Keyed State和Broadcast State用起来非常顺手。
- 精确一次语义:金融场景最怕丢数据或重复计算。Flink的Checkpoint机制能保证数据不重不漏。
3.3 系统整体架构
下面这张图是我在多个项目中沉淀下来的实时风控架构,你可以把它当成一个参考模板。
整个流程其实不复杂:数据从多个源头进入消息队列,Flink消费后依次执行清洗、规则匹配、模型推理、风险聚合,最后输出决策结果。关键点在于——每一步都不能成为瓶颈。
3.4 规则引擎集成
规则引擎是风控系统的“第一道防线”。简单规则(比如“单笔转账超过5万”或“IP地址来自高风险国家”)直接拦截,不需要走模型推理,能省下大量计算资源。
我常用的方案有两种:
- Drools:适合复杂规则,支持DRL语言,但性能一般,单机每秒能处理几千条规则。
- 自研轻量规则引擎:用Groovy或SpEL表达式,配合Flink的BroadcastStream动态更新规则。性能好,延迟低,但需要自己维护规则管理平台。
下面是一个简单的Flink规则匹配代码片段,用BroadcastStream实现动态规则更新:
// 规则流:从Kafka读取规则变更
DataStream<Rule> ruleStream = env
.addSource(new FlinkKafkaConsumer<>("rules", new RuleDeserializer(), props))
.broadcast(ruleStateDescriptor);
// 交易流:主数据流
DataStream<Transaction> txStream = env
.addSource(new FlinkKafkaConsumer<>("transactions", new TxDeserializer(), props))
.keyBy(Transaction::getUserId);
// 连接两条流,进行规则匹配
DataStream<RiskResult> resultStream = txStream
.connect(ruleStream)
.process(new RuleMatchProcessFunction());
// 处理函数内部逻辑
public static class RuleMatchProcessFunction
extends BroadcastProcessFunction<Transaction, Rule, RiskResult> {
@Override
public void processElement(Transaction tx, ReadOnlyContext ctx, Collector<RiskResult> out) {
// 从BroadcastState中读取当前生效的规则
ReadOnlyBroadcastState<String, Rule> state = ctx.getBroadcastState(ruleStateDescriptor);
for (Map.Entry<String, Rule> entry : state.immutableEntries()) {
Rule rule = entry.getValue();
if (rule.evaluate(tx)) {
out.collect(new RiskResult(tx.getTxId(), rule.getAction(), rule.getScore()));
}
}
}
@Override
public void processBroadcastElement(Rule rule, Context ctx, Collector<RiskResult> out) {
// 更新规则状态
ctx.getBroadcastState(ruleStateDescriptor).put(rule.getRuleId(), rule);
}
}
这段代码的核心思路是:规则和交易数据分别走不同的Kafka Topic,规则流通过Broadcast广播到所有并行实例,交易流按用户ID分组。这样每条交易进来时,本地内存里已经有完整的规则集,不需要远程调用。
3.5 反欺诈模型部署
规则引擎搞不定的复杂场景,就得靠模型了。比如检测“团伙欺诈”——多个账号在同一设备上登录,或者短时间内从不同IP发起相似交易。这种模式靠写规则是写不出来的。
模型部署这块,我踩过不少坑。最早我们直接用Python Flask起一个HTTP服务,Flink通过AsyncIO调用。但线上流量一上来,HTTP连接数和序列化开销就成了瓶颈。
后来换成了TensorFlow Serving + gRPC,延迟稳定在10ms以内。再后来,Flink 1.13开始支持PyFlink + UDF,可以直接在Flink算子内调用Python模型,省去了网络开销。
| 部署方式 | 延迟 | 吞吐量 | 运维复杂度 |
|---|---|---|---|
| HTTP服务 | 20~50ms | 低 | 低 |
| gRPC服务 | 5~15ms | 中 | 中 |
| PyFlink UDF | 2~10ms | 高 | 高 |
3.6 状态管理与容错
实时风控离不开状态。比如“过去5分钟内同一设备登录了多少个账号”或者“用户当前累计交易金额是否超过阈值”。这些都需要在Flink里维护状态。
Flink提供了多种状态后端:
- MemoryStateBackend:存在TaskManager内存里,适合开发测试。
- FsStateBackend:存在文件系统,适合生产环境。
- RocksDBStateBackend:存在本地磁盘,支持超大状态(TB级别)。
我个人建议生产环境用RocksDB。为什么?因为风控的状态量通常很大——每个用户都要维护一个画像,几千万用户就是几千万个Key。RocksDB能把状态写到磁盘,内存只放热数据,不会OOM。
容错方面,Flink的Checkpoint机制默认是“精确一次”语义。但要注意,Checkpoint间隔不能设得太短,否则频繁做快照会影响性能。我一般设成30秒到1分钟,同时开启增量Checkpoint。
3.7 性能调优实战
最后分享几个我在项目中用过的调优技巧:
- 合理设置并行度:并行度不是越大越好。我见过有人把并行度设成100,结果每个TaskManager只处理几条数据,网络开销反而上去了。一般建议并行度等于CPU核心数的2~3倍。
- 使用ValueState代替MapState:如果每个Key只存一个值,用ValueState比MapState快得多。MapState的序列化开销更大。
- 开启对象重用:在env.getConfig().enableObjectReuse()可以减少对象创建,降低GC压力。
- 避免大Key倾斜:如果某个用户的交易量特别大(比如刷单机器人),会导致数据倾斜。可以用二次Key打散,或者单独处理热点Key。
好了,这一章的内容就到这里。实时风控是个系统工程,从数据接入到规则匹配,从模型推理到状态管理,每个环节都有坑。但只要你把架构搭稳了,后面的事情就好办多了。