大数据-131 Flink CEP 实战 24 小时≥5 次交易 & 10 分钟未支付检测 案例附代码

TL;DR

场景:需要在流上做累计计数(24h≥5 次)和超时未支付检测。 结论:用 CEP 的 times/timesOrMore/within 组合,配合 skipPastLastEvent 抑制重复;Watermark 用官方策略即可,代码可复制即跑。 产出:两段可运行示例(含 Watermark)、侧输出模板、易错点速查。

上节的案例: Flink CEP 详解 - 捕获超时事件提取全解析:从原理到完整实战代码教程 恶意登录案例实现

基本概念

Flink CEP(Complex Event Processing)是Apache Flink提供的一个扩展库,用于实时复杂事件处理。通过Flink CEP,开发者可以从流数据中识别出特定的事件模式。这在欺诈检测、网络安全、实时监控、物联网等场景中非常有用。

Flink CEP的核心是通过定义事件模式,从流中检测复杂事件序列。 具体来说,CEP允许用户:

  • 定义事件模式:用户可以描述感兴趣的事件组合(如连续事件、延迟事件等)。
  • 匹配模式:Flink CEP从流中搜索与定义模式相匹配的事件序列。
  • 处理匹配结果:一旦找到符合模式的事件序列,用户可以定义如何处理这些匹配。

基本组成部分

  • Pattern(模式):描述要在事件流中匹配的事件序列。可以是单个事件或多个事件的组合。常用的模式操作包括next(紧邻)、followedBy(接续)等。
  • PatternStream(模式流):通过应用模式定义,将事件流转变为模式流。
  • Select函数:用于从模式流中提取匹配的事件序列

CEP开发步骤

开发Flink CEP应用的基本步骤包括:

定义事件流:创建一个DataStream,表示原始的事件流。 定义事件模式:使用Flink CEP的API定义事件模式,例如连续事件、迟到事件等。 将模式应用到流中:将定义好的模式应用到事件流上,生成模式流PatternStream。 提取匹配事件:使用select函数提取匹配模式的事件,并定义如何处理这些事件。

使用场景

  • 欺诈检测:可以通过CEP识别连续发生的异常行为,如频繁的登录尝试等。
  • 网络监控:检测一段时间内的特定网络攻击模式。
  • 物联网:分析传感器数据,检测设备异常、温度异常等。
  • 用户行为分析:分析用户在某一时间段内的行为序列,从而作出预测或检测异常。

案例2:检测交易活跃用户

业务需求

业务上需要找出24小时内,至少5次有效交易的用户。

  • 获取数据源
  • Watermark转化
  • keyBy转化
  • 至少5次:timeOrMore(5)
  • 24小时之内:within(Time.hours(24))
  • 模式匹配
  • 提取匹配成功的数据

编写代码

java 复制代码
package icu.wzk;
/**
 * 使用 Flink CEP 识别"活跃用户"的简单示例:
 * - 输入:同一用户在 24 小时内多次产生 price>0 的事件
 * - 目标:匹配同一 key(username)下,满足 timesOrMore(5) 的事件序列
 *
 * 说明:
 * 1)这里演示了 EventTime、水位线、KeyBy、CEP Pattern、PatternStream.process 的完整链路。
 * 2)process() 里目前只打印匹配 map,没有向下游 collect 实际结果(下方保留原样)。
 */
public class FlinkCepActiveUser {

    public static void main(String[] args) throws Exception {
        // 1. 环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 老版本 API:指定使用事件时间(1.12+ 默认就是事件时间,此方法已逐步废弃)
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);

        // 2. 构造示例数据(username, price, eventTimeMillis)
        DataStreamSource<CepActiveUserBean> data = env.fromElements(
                new CepActiveUserBean("100XX", 0.0D, 1597905234000L),
                new CepActiveUserBean("100XX", 100.0D, 1597905235000L),
                new CepActiveUserBean("100XX", 200.0D, 1597905236000L),
                new CepActiveUserBean("100XX", 300.0D, 1597905237000L),
                new CepActiveUserBean("100XX", 400.0D, 1597905238000L),
                new CepActiveUserBean("100XX", 500.0D, 1597905239000L),
                new CepActiveUserBean("101XX", 0.0D, 1597905240000L),
                new CepActiveUserBean("101XX", 100.0D, 1597905241000L)
        );

        // 3. 事件时间抽取 + 自定义水位线
        //    注意:maxTimestamp 初值设为 Long.MAX_VALUE 会导致 Math.max 永远取不到较大的事件时间(逻辑上不合理),
        //    这里保持你的原始逻辑不动,仅在"修正版"里给出正确写法。
        SingleOutputStreamOperator<CepActiveUserBean> watermark = data
                .assignTimestampsAndWatermarks(new WatermarkStrategy<CepActiveUserBean>() {
                    @Override
                    public WatermarkGenerator<CepActiveUserBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                        return new WatermarkGenerator<CepActiveUserBean>() {

                            long maxTimestamp = Long.MAX_VALUE; // 原代码:应为 Long.MIN_VALUE 更合理(见修正版)
                            long maxOutOfOrderness = 500L;      // 允许 500ms 的乱序

                            @Override
                            public void onEvent(CepActiveUserBean event, long eventTimestamp, WatermarkOutput output) {
                                // 维护已观察到的最大事件时间
                                maxTimestamp = Math.max(event.getTimestamp(), maxTimestamp);
                            }

                            @Override
                            public void onPeriodicEmit(WatermarkOutput output) {
                                // 周期性发射水位线
                                output.emitWatermark(new Watermark(maxTimestamp - maxOutOfOrderness));
                            }
                        };
                    }
                }.withTimestampAssigner((element, recordTimes) -> element.getTimestamp()) // 抽取事件时间戳
                );

        // 4. 按 username 分区(CEP 必须在 keyed 流上应用)
        KeyedStream<CepActiveUserBean, String> keyed = watermark
                .keyBy(new KeySelector<CepActiveUserBean, String>() {
                    @Override
                    public String getKey(CepActiveUserBean value) {
                        return value.getUsername();
                    }
                });

        // 5. 定义 CEP 模式:
        //    - begin("start"): 起始状态
        //    - where: 条件为 price>0
        //    - timesOrMore(5): 至少匹配 5 次
        //    - within(Time.hours(24)): 整个匹配序列需在 24 小时窗口内完成
        Pattern<CepActiveUserBean, CepActiveUserBean> pattern = Pattern
                .<CepActiveUserBean>begin("start")
                .where(new SimpleCondition<CepActiveUserBean>() {
                    @Override
                    public boolean filter(CepActiveUserBean value) {
                        return value.getPrice() > 0;
                    }
                })
                .timesOrMore(5)
                .within(Time.hours(24));

        // 6. 构造 PatternStream 并处理匹配结果
        PatternStream<CepActiveUserBean> parentStream = CEP.pattern(keyed, pattern);

        // 当前仅打印匹配 map,不向下游发射元素;因此下游 print() 可能无输出
        SingleOutputStreamOperator<CepActiveUserBean> process = parentStream
                .process(new PatternProcessFunction<CepActiveUserBean, CepActiveUserBean>() {
                    @Override
                    public void processMatch(Map<String, List<CepActiveUserBean>> map,
                                             Context context,
                                             Collector<CepActiveUserBean> collector) {
                        System.out.println("map: " + map); // 调试输出:查看匹配到的各阶段事件
                        // 如果需要真正输出,可在这里基于 map 组装结果并 collector.collect(...)
                    }
                });

        // 7. Sink
        process.print();

        env.execute("FlinkCepActiveUser");
    }
}

/** 简单事件 Bean:用户名、价格、事件时间戳(毫秒) */
class CepActiveUserBean {
    private String username;
    private Double price;
    private Long timestamp;

    public CepActiveUserBean(String username, Double price, Long timestamp) {
        this.username = username;
        this.price = price;
        this.timestamp = timestamp;
    }

    // getter / setter 省略
}

运行结果

shell 复制代码
map: {start=[CepActiveUserBean{username='100XX', price=100.0, timestamp=1597905235000}...

运行结果如下图所示:

案例3:超时未支付

业务需求

找出下单后10分钟没有支付的订单

  • 获取数据源
  • 转 Watermark
  • keyBy 转化
  • 做出 Pattern (下单以后10分钟未支付)
  • 模式匹配
  • 取出匹配成功的数据

编写代码

java 复制代码
package icu.wzk;
/**
 * 订单超时支付检测(原逻辑保留):
 * - 输入事件:同一 userId 下会出现 "create" 与 "pay" 两类操作
 * - 目标:匹配 "create" 后在 10 分钟内跟随一个 "pay";否则输出到侧输出流作为"超时订单"
 *
 * 注意:
 * 1)这里仍使用了 setStreamTimeCharacteristic(EventTime) 与自定义 Watermark(与旧版一致)。
 * 2)maxTimestamp 初始为 Long.MAX_VALUE 的写法有逻辑问题(详见修正版)。
 * 3)主输出只保留了超时侧输出打印。如需同时打印成功支付匹配,可在 result.print() 处放开。
 */
public class FlinkCepTimeOutPay {

    public static void main(String[] args) throws Exception {
        // 1. 环境(旧 API,仅为保持与原逻辑一致)
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);

        // 2. 构造示例数据:userId, operation, eventTimeMillis
        DataStreamSource<TimeOutPayBean> data = env.fromElements(
                new TimeOutPayBean(1L, "create", 1597905234000L),
                new TimeOutPayBean(1L, "pay",    1597905235000L),
                new TimeOutPayBean(2L, "create", 1597905236000L),
                new TimeOutPayBean(2L, "pay",    1597905237000L),
                new TimeOutPayBean(3L, "create", 1597905239000L) // 未支付,期望走超时侧输出
        );

        // 3. 事件时间抽取 + 自定义水位线(保持原写法)
        DataStream<TimeOutPayBean> watermark = data
                .assignTimestampsAndWatermarks(new WatermarkStrategy<TimeOutPayBean>() {
                    @Override
                    public WatermarkGenerator<TimeOutPayBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                        return new WatermarkGenerator<TimeOutPayBean>() {
                            long maxTimestamp = Long.MAX_VALUE; // 原代码:不合理,见修正版
                            long maxOutOfOrderness = 500L;      // 允许 500ms 乱序

                            @Override
                            public void onEvent(TimeOutPayBean e, long ts, WatermarkOutput out) {
                                // 维护已观察到的最大事件时间
                                maxTimestamp = Math.max(maxTimestamp, e.getTimestamp());
                            }

                            @Override
                            public void onPeriodicEmit(WatermarkOutput out) {
                                out.emitWatermark(new Watermark(maxTimestamp - maxOutOfOrderness));
                            }
                        };
                    }
                }.withTimestampAssigner((element, recordTimestamp) -> element.getTimestamp())
                );

        // 4. 按 userId 分区(CEP 要求 keyed 流)
        KeyedStream<TimeOutPayBean, Long> keyedStream = watermark.keyBy(new KeySelector<TimeOutPayBean, Long>() {
            @Override
            public Long getKey(TimeOutPayBean value) { return value.getUserId(); }
        });

        // 5. 定义 CEP 模式:create -> pay 且在 10 分钟内(600s)
        OutputTag<TimeOutPayBean> orderTimeoutOutput = new OutputTag<>("orderTimeout") {};
        Pattern<TimeOutPayBean, TimeOutPayBean> pattern = Pattern
                .<TimeOutPayBean>begin("begin")
                .where(new IterativeCondition<TimeOutPayBean>() {
                    @Override
                    public boolean filter(TimeOutPayBean e, Context<TimeOutPayBean> ctx) {
                        return "create".equals(e.getOperation());
                    }
                })
                .followedBy("pay")
                .where(new IterativeCondition<TimeOutPayBean>() {
                    @Override
                    public boolean filter(TimeOutPayBean e, Context<TimeOutPayBean> ctx) {
                        return "pay".equals(e.getOperation());
                    }
                })
                .within(Time.seconds(600)); // 10 分钟

        // 6. 匹配选择:超时走侧输出,正常支付走主输出
        PatternStream<TimeOutPayBean> patternStream = CEP.pattern(keyedStream, pattern);
        SingleOutputStreamOperator<TimeOutPayBean> result = patternStream.select(
                orderTimeoutOutput,
                new PatternTimeoutFunction<TimeOutPayBean, TimeOutPayBean>() {
                    @Override
                    public TimeOutPayBean timeout(Map<String, List<TimeOutPayBean>> map, long timeoutTs) {
                        // 超时时仅有 "begin"(create)阶段
                        return map.get("begin").get(0);
                    }
                },
                new PatternSelectFunction<TimeOutPayBean, TimeOutPayBean>() {
                    @Override
                    public TimeOutPayBean select(Map<String, List<TimeOutPayBean>> map) {
                        // 正常匹配时可返回 "pay" 或汇总对象,这里保留原写法
                        return map.get("pay").get(0);
                    }
                }
        );

        // 7. 输出(原逻辑只打印侧输出)
        System.out.println("==============");
        DataStream<TimeOutPayBean> sideOutput = result.getSideOutput(orderTimeoutOutput);
        sideOutput.print();          // 超时订单
        // result.print();           // 如需同时打印"成功支付"匹配,放开此行

        env.execute("FlinkCepTimeOutPay");
    }
}

/** 输入事件:用户ID / 操作类型(create|pay) / 事件时间戳(毫秒) */
class TimeOutPayBean {
    private Long userId;
    private String operation;
    private Long timestamp;

    public TimeOutPayBean(Long userId, String operation, Long timestamp) {
        this.userId = userId;
        this.operation = operation;
        this.timestamp = timestamp;
    }
    // getter / setter 省略
}

运行结果

控制台输出为:

shell 复制代码
TimeOutPayBean{userId=1, operation='pay', timestamp=1597905235000...

对应截图如下:

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南! AI-调查研究-108-具身智能 机器人模型训练全流程详解:从预训练到强化学习与人类反馈 🔗 AI模块直达链接

💻 Java篇持续更新中(长期更新)

Java-154 深入浅出 MongoDB 用Java访问 MongoDB 数据库 从环境搭建到CRUD完整示例 MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础! 🔗 Java模块直达链接

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解 🔗 大数据模块直达链接

相关推荐
刘一说2 小时前
深入理解 Spring Boot Actuator:构建可观测性与运维友好的应用
运维·spring boot·后端
oak隔壁找我2 小时前
Spring AI 入门教程,使用Ollama本地模型集成,实现对话记忆功能。
java·人工智能·后端
todoitbo2 小时前
从大数据角度看时序数据库选型:Apache IoTDB的实战经验分享
大数据·apache·时序数据库·iotdb
郝开2 小时前
最终 2.x 系列版本)2 - 框架搭建:pom配置;多环境配置文件配置;多环境数据源配置;测试 / 生产多环境数据源配置
java·spring boot·后端
南囝coding2 小时前
100% 用 AI 做完一个新项目,从 Plan 到 Finished 我学到了这些
前端·后端
Homeey2 小时前
深入理解ThreadLocal:从原理到架构实践的全面解析
java·后端
大数据CLUB2 小时前
酒店预订数据分析及预测可视化
大数据·hadoop·分布式·数据挖掘·数据分析·spark·mapreduce
shykevin3 小时前
Rust入门
开发语言·后端·rust
Lisonseekpan3 小时前
Git 命令大全:从基础到高级操作
java·git·后端·github·团队开发
学历真的很重要3 小时前
LangChain V1.0 Messages 详细指南
开发语言·后端·语言模型·面试·langchain·职场发展·langgraph