3. 金融行业(二)

各位好,欢迎来到金融行业的第二讲。上一章我们聊了银行核心账户体系,今天咱们来点更刺激的——实时风控。

说实话,金融风控是我个人觉得最有挑战性的场景之一。你想想看,用户点一下“转账”按钮,后台要在几十毫秒内判断这笔交易是不是诈骗、有没有盗刷、会不会洗钱。慢了不行,误判了更不行。我当年在支付公司做第一个实时风控项目时,上线第一天就被业务方追着问:“你们系统到底能不能扛住双十一?”嗯,那会儿心里是真没底。

3.1 实时风控的核心挑战

先看几个数字。一个中等规模的支付平台,每天的交易量大概在几千万笔。每笔交易从进入系统到返回结果,留给风控的时间窗口通常不超过100毫秒。在这100毫秒里,你要完成数据清洗、规则匹配、模型推理、风险打分、结果回传——说白了,就是一场与时间的赛跑。

指标 传统批处理 实时流处理
延迟 分钟~小时级 毫秒~秒级
吞吐量 受限于批窗口 百万级TPS
状态管理 依赖外部DB 内置状态后端
容错机制 重跑整个批次 精确一次语义

传统做法是每天跑一次离线规则,第二天出黑名单。但现在的欺诈手段,从交易发生到资金转移,可能只需要几秒钟。所以,实时流处理成了唯一的选择。

3.2 为什么选Flink?

市面上能做实时计算的框架不少,Storm、Spark Streaming、Flink……我个人习惯用Flink,原因有三:

  • 真正的流处理:Flink是逐条处理数据,而不是微批次。延迟能压到个位数毫秒。
  • 强大的状态管理:风控需要维护用户画像、设备指纹、历史行为等状态。Flink的Keyed State和Broadcast State用起来非常顺手。
  • 精确一次语义:金融场景最怕丢数据或重复计算。Flink的Checkpoint机制能保证数据不重不漏。
一个小建议:如果你的团队刚接触实时计算,别一上来就搞复杂的CEP(复杂事件处理)。先把基础的DataStream API跑通,再逐步上高级特性。我在项目里吃过这个亏——架构设计得太花哨,结果线上出了Bug,排查了整整两天。

3.3 系统整体架构

下面这张图是我在多个项目中沉淀下来的实时风控架构,你可以把它当成一个参考模板。

实时风控系统架构图 交易数据源 用户行为日志 设备指纹 外部黑名单 Kafka / Pulsar 消息队列 Flink 实时计算引擎 数据清洗 & 特征提取 规则引擎匹配 模型推理打分 风险聚合 风控结果(通过/拒绝) 告警 & 人工审核 离线特征回放 说明:所有数据先进入消息队列解耦,Flink消费后依次执行清洗、规则、模型、聚合,最终输出决策结果

整个流程其实不复杂:数据从多个源头进入消息队列,Flink消费后依次执行清洗、规则匹配、模型推理、风险聚合,最后输出决策结果。关键点在于——每一步都不能成为瓶颈。

3.4 规则引擎集成

规则引擎是风控系统的“第一道防线”。简单规则(比如“单笔转账超过5万”或“IP地址来自高风险国家”)直接拦截,不需要走模型推理,能省下大量计算资源。

我常用的方案有两种:

  • Drools:适合复杂规则,支持DRL语言,但性能一般,单机每秒能处理几千条规则。
  • 自研轻量规则引擎:用Groovy或SpEL表达式,配合Flink的BroadcastStream动态更新规则。性能好,延迟低,但需要自己维护规则管理平台。
避坑指南:我曾经在一个项目里把规则全部写在数据库里,每次交易都查一次DB。结果压测时数据库连接池直接被打爆。后来改成规则预热到Flink的BroadcastState里,延迟从50ms降到了5ms。

下面是一个简单的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 高 高
注意:PyFlink UDF虽然延迟低,但Python的GIL会限制并行度。如果你的模型推理是CPU密集型的,建议还是用gRPC方式,把模型推理独立出去,方便单独扩缩容。

3.6 状态管理与容错

实时风控离不开状态。比如“过去5分钟内同一设备登录了多少个账号”或者“用户当前累计交易金额是否超过阈值”。这些都需要在Flink里维护状态。

Flink提供了多种状态后端:

  • MemoryStateBackend:存在TaskManager内存里,适合开发测试。
  • FsStateBackend:存在文件系统,适合生产环境。
  • RocksDBStateBackend:存在本地磁盘,支持超大状态(TB级别)。

我个人建议生产环境用RocksDB。为什么?因为风控的状态量通常很大——每个用户都要维护一个画像,几千万用户就是几千万个Key。RocksDB能把状态写到磁盘,内存只放热数据,不会OOM。

容错方面,Flink的Checkpoint机制默认是“精确一次”语义。但要注意,Checkpoint间隔不能设得太短,否则频繁做快照会影响性能。我一般设成30秒到1分钟,同时开启增量Checkpoint。

3.7 性能调优实战

最后分享几个我在项目中用过的调优技巧:

  1. 合理设置并行度:并行度不是越大越好。我见过有人把并行度设成100,结果每个TaskManager只处理几条数据,网络开销反而上去了。一般建议并行度等于CPU核心数的2~3倍。
  2. 使用ValueState代替MapState:如果每个Key只存一个值,用ValueState比MapState快得多。MapState的序列化开销更大。
  3. 开启对象重用:在env.getConfig().enableObjectReuse()可以减少对象创建,降低GC压力。
  4. 避免大Key倾斜:如果某个用户的交易量特别大(比如刷单机器人),会导致数据倾斜。可以用二次Key打散,或者单独处理热点Key。
一个小技巧:调优时别光看平均延迟,要看P99和P999。平均延迟10ms,但P99可能已经到500ms了。我习惯在Flink的Metrics里加上延迟监控,配合Grafana看板,一眼就能发现瓶颈。

好了,这一章的内容就到这里。实时风控是个系统工程,从数据接入到规则匹配,从模型推理到状态管理,每个环节都有坑。但只要你把架构搭稳了,后面的事情就好办多了。