EPL语法概述
-
- [1、EPL 的标准语法骨架](#1、EPL 的标准语法骨架)
- [2、基础过滤与事件流选择(SELECT & FROM & WHERE)](#2、基础过滤与事件流选择(SELECT & FROM & WHERE))
- [3、数据窗口语法(Data Windows)------ 流处理的物理边界](#3、数据窗口语法(Data Windows)—— 流处理的物理边界)
- [4、分组与聚合(GROUP BY & HAVING)](#4、分组与聚合(GROUP BY & HAVING))
- 5、高级时序控制:输出频率限制(OUTPUT)
- 6、核心杀手级语法:模式匹配(Pattern)
- 7、总结
在 Esper 中,EPL(Event Processing Language,事件处理语言) 是整个引擎的灵魂。它的语法虽然极度神似传统数据库的 SQL,但其底层的执行逻辑却恰恰相反。
传统 SQL 的本质是"数据静止,查询在动"(数据老老实实呆在磁盘里,你发送一条 SQL 查一次);而 EPL 的本质是"查询静止,数据在流"(EPL 规则像一张网一样死死挂在内存里,数据像瀑布一样冲过去,一旦命中立刻触发回调)。
1、EPL 的标准语法骨架
一条完整的、复杂的 EPL 语句通常包含以下结构:
sql
[ @Annotation ]
SELECT select_list
FROM event_stream_expression [ .win:window_spec ] [ AS alias ]
[ WHERE search_conditions ]
[ GROUP BY group_by_expression ]
[ HAVING having_conditions ]
[ OUTPUT output_spec ]
乍一看与 SQL 一模一样,但请注意,其中 win:window_spec(窗口) 和 OUTPUT(输出控制) 是传统 SQL 绝不可能存在的流处理核心特有语法。
2、基础过滤与事件流选择(SELECT & FROM & WHERE)
这是 EPL 最基本的形态,用于从源源不断的事件流中筛选出符合条件的数据。
示例 :简单无状态过滤
sql
@name('VIP-Order-Filter')
select orderId, price, buyerName
from OrderEvent
where price > 10000 and region = 'CN'
@name(...):注解。给这条 EPL 命名,方便在 Java 代码中通过名字精准捞出对应的 EPStatement。OrderEvent:这是你注册到引擎里的事件别名(通常对应一个 Java Bean)。执行逻辑:没有任何内存缓存。每当一条 OrderEvent 流入,只要价格大于 10000 且地区是 CN,就立刻向外发射结果。
3、数据窗口语法(Data Windows)------ 流处理的物理边界
窗口主要分为两大流派:滑动窗口(Sliding) 和 滚动/翻转窗口(Tumbling/Batch)。
1. 滑动窗口:数据有进有出,位置不断向前推移
win:time(时间大小):只保留最近一段时间内的数据。
sql
-- 统计最近 10 秒内,流入的刷卡事件的总金额
select sum(amount) from CardPayEvent.win:time(10 sec)
win:length(条数大小):只保留最近的固定 N 条数据。
sql
-- 统计最近 3 条温度数据的平均值
select avg(temperature) from TempEvent.win:length(3)
2. 滚动/翻转窗口:攒满一批,打包处理,然后清空
win:time_batch(时间周期):每隔固定时间倒出来统计一次。
sql
-- 每隔 1 分钟,统计一次这 1 分钟内所有登录失败的用户数
select count(*) from LoginFailEvent.win:time_batch(1 min)
win:length_batch(条数):凑够固定条数再触发。
sql
-- 每积攒够 100 条日志,打包输出一次日志级别分类统计
select logLevel, count(*) from LogEvent.win:length_batch(100) group by logLevel
4、分组与聚合(GROUP BY & HAVING)
当流数据配合窗口使用时,GROUP BY 的威力才真正显现。
sql
select deviceId, max(value) as maxVal
from SensorEvent.win:time(5 min)
group by deviceId
having max(value) > 100
内存运作:Esper 会在内存里为每一个不同的 deviceId 维护一个长达 5 分钟的滑动窗口。触发时机:只要任何一个设备的 5 分钟内最大值超过了 100,就会向外发射。注意:这里的 group by 会隐式占用内存。如果设备源源不断且几万个设备以后再也不发数据了,需要配合 Context(上下文)来及时释放过期设备的内存,否则会导致内存泄漏。
5、高级时序控制:输出频率限制(OUTPUT)
在传统 SQL 中,算完了直接一把梭给出来。但流处理不行。比如"最近 1 小时内",每流过一条数据,滑动窗口的值都在变,如果你绑定了监听器,监听器一秒钟会被轰炸几万次。
OUTPUT 关键字就是用来给输出结果做限流和定时定量外发的。
sql
select symbol, avg(price)
from StockTick.win:time(1 hour)
group by symbol
output last every 5 sec
output last every 5 sec:每隔 5 秒钟,才把过去 1 小时滑动窗口算出来的最后一条(最新一条)统计结果发送给 Java 监听器。中间那 5 秒内产生的无数动态变化直接在内存里悄悄更新,不轰炸外部系统。常用策略:output first(只要周期内的第一条)、output last(只要最新的)、output all(把这 5 秒内产生的所有中间结果打包一起发)。
6、核心杀手级语法:模式匹配(Pattern)
这是 EPL 最难、但也是最强悍的地方。它不用 from Event.win: 的传统格式,而是使用 from pattern [...] 语法,专门用来抓取"先发生 A,接着发生 B,中间不能发生 C"这种因果和时序关系。
核心符号:
->:跟随操作符(A 发生后,接着发生 B)。every:循环修饰符。如果没有 every,该规则触发一次后就会在内存里销毁。timer:within(时间):时间沙漏限制。
示例:电商防刷(频次因果)
用户(userId)在一分钟内连续 3 次登录失败,接着立刻发生了一笔大额消费。
sql
select a.userId, b.amount from pattern [
every (
a=LoginEvent(success=false) ->
LoginEvent(userId=a.userId, success=false) ->
LoginEvent(userId=a.userId, success=false) ->
b=OrderEvent(userId=a.userId, amount > 5000)
) where timer:within(1 min)
]
示例:物联网故障检测(状态缺失检查)
传感器启动了(Status=START),但在接下来的 10 秒钟内,没有收到任何心跳上报(Status=PING)。
- 解读:
and not配合timer:interval可以在指定时间内一旦没有等到预期事件时,精准触发超时告警。
7、总结
- Getter 强依赖:EPL 里写 select name,底层的 Java 类必须有标准的 public String getName()。
- 避免无边界的 GROUP BY:千万别在没有加 win:time 窗口的流上直接用 group by。这会导致 Esper 认为你要把从开机到关机所有的历史数据都按 Key 存在内存里,用不了几天 JVM 就会直接 OutOfMemoryError。
- Pattern 与普通 EPL 的区别:
- 普通的 EPL(
from MyEvent.win:time)侧重于数据量的统计和计算(算平均值、最大值)。 - Pattern EPL(
from pattern [...])侧重于时序的触发和警报(捕捉特定行为轨迹)。
- 普通的 EPL(