Esper复杂事务处理一小时入门
来自小韩
什么是Esper
想要认识Esper,先要了解CEP(Complex Event Processing),到处都有,并且各方理解也有偏差,我就不赘述了。
Esper就是CEP的一个java的开源实现。
Esper官方网址:http://www.espertech.com/
Esper的特性
在探究Esper特性之前,我们先总结一下复杂事件的特性:
- 类型多样,不易建模
- 场景不可控,随时可能新增场景
- 逻辑复杂,难于描述
- 既需要汇总规律,又不准数据落地。
场景举例:
股市实时K线图、网站恶意操作监测、用户登入控制、实时数据统计等
针对复杂事件的这些特性,Esper具有相应的性质:
- 实时响应
- 极速扩展新事件
- 语法描述能力强大、类sql
事件和处理流程
一个基本的例子
引用Esper就不说了,直接搜maven esper,添加以来就好。
/*定义事件模型*/
public class Coder {
private String Name;
private int age;
private double Salary;
//省略getter/setter
}
//
public class HelloEsperApp {
public static void main(String[] args) throws InterruptedException {
EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
EPAdministrator admin = epService.getEPAdministrator();
//指定事件模型
String coderModel = Coder.class.getName();
//描述复杂事件
String epl = "select name,salary,age from " + coderModel;
EPStatement state = admin.createEPL(epl);
//添加事后处理
state.addListener(new HelloEsperListener());
EPRuntime runtime = epService.getEPRuntime();
//模拟事件发生
for (int i = 0; i < 10; i++) {
Coder coder = new Coder();
coder.setName("coder"+i);
coder.setAge(20+i);
runtime.sendEvent(coder);
}
}
}
import com.espertech.esper.client.UpdateListener;
/** 实现UpdateListener接口,来定义事件的后置处理过程 **/
public class HelloEsperListener implements UpdateListener {
public void update(EventBean[] arg0, EventBean[] arg1) {
try {
System.out.println("coder: name-"+arg0[0].get("name") + " age-"+arg0[0].get("age"));
}catch(Exception e) {
e.printStackTrace();
}
}
小心的分享:笔者认为任何一个项目的学习都可以分为两步,1.看他暴露了多少给你(用起来是否清晰);2.看他隐藏了多少细节(实现得是否精致)
一张图简单描述一下Esper给我们暴露了什么:
其中,后置处理方式的subscribe本文暂不提及。
Esper支持的模型类型
前面的例子中,我们使用一个bean作为事件模型。这样做很多时候都存在局限性,当有新的事件模型需要被加入时,我们必须要定义新的类,实际上,在实际应用中,bean很少被使用。
Esper支持丰富的事件模型定义方式,包括java类、Map、List、XML。
例如使用Map来定义事件模型:
Map<String, Object> blog = new HashMap<String, Object>();
blog.put("url", String.class);
blog.put("title", String.class);
Map<String, Object> coder = new HashMap<String, Object>();
coder.put("name", String.class);
coder.put("salary", int.class);
coder.put("friends", List.class);
coder.put("blogKey", "Blog");
// 注册blog模型到esper
admin.getConfiguration().addEventType("Blog", blog);
// 注册coder到Esper
admin.getConfiguration().addEventType("Coder", coder);
这之后,我们就可以对Coder事件模型进行EPL事件描述了。
EPL语法
关于语法,我们大致讲一下,到能帮助大家整体理解Esper的程度,之后会单独写文章来分享语法技巧。
基本语法
以上面的Coder模型为例,类SQL部分,我们快速带过:
#实时计算出当前进入引擎的所有程序员的平均工资、最高工资。
select name,Blog.title,salary,avg(salary) as avgSalary,max(salary) as maxSalary from Coder where salary>1000;
#其他的例如 and、or、not、in、order by、group by、having、join等基本都与SQL标准一致
与SQL不同的地方,主要体现了实时性,与sql的持久数据形成对比我们列四点最重要的学习一下:insert、窗格、context、pattern。
insert
首先用SQL的视角去想象,好像把数据保存起来这样的动作在Esper这样实时处理的工具中好像确实没有场景。
实际上insert在Esper中做的是转发的角色。即把某事件模型类的事件经过EPL运算后,insert成另外一种事件,去触发另外事件的处理流程。看例子:
//在前文Coder的基础上,给Coder加上团队标志,再定义一个Team事件模型
Map<String, Object> coder = new HashMap<String, Object>();
coder.put("name", String.class);
coder.put("salary", int.class);
coder.put("teamName", String.class);
Map<String, Object> team = new HashMap<String, Object>();
team.put("teamName", String.class);
team.put("totalSalary", int.class);
admin.getConfiguration().addEventType("Coder", coder);
admin.getConfiguration().addEventType("Team", team);
String epl4CoderIn = "insert Team(teamName, totalSalary) select teanName,sum(salary) as totalSalary from Coder groupby teamName";
String epl4TeamIn = "select teamName, totalSalary from Team";
EPStatement state = admin.createEPL(epl4CoderIn);
EPStatement state = admin.createEPL(epl4TeamIn);
state.addListener(new TeamEventListener());
这时,每当有Coder类型的事件流入,就转化为新的Team事件去触发Team的流程。Esper可以基于此实现事件流。
窗格
窗格应该是最有别于SQL的特性了,Esper的事件收集器支持事件积攒,分为两种积攒方式,时间和数量。
-
时间窗格
select name, avg(salary) from Coder.win:time(5 sec);
计算5秒内进入引擎的Coder的平均工资,看图理解:
每有新的事件进入,就会触发事件收集器回溯5秒,找到这五秒内收到的所有事件,并一同发给后置处理器。 -
时间批量窗格
select name, avg(salary) from Coder.win:time_batch(5 sec);
看图理解:
在事件批量窗格中,事件的进入不触发后置处理,只有满了一个批量单位才会进行提交。例如在股市K线图的实时绘制中就采用这种机制。
事件窗格支持的时间定义,小到毫秒大到年,包括msec、sec、min、hour、day、week、month、year。例如我们要定义一个周期为一天5小时20分的窗格,可以写作:
1 day 5 hour 20 min -
数量窗格
数量窗格跟事件窗格一样也分普通和批量,我们先来看一下语法例子:
select name, avg(salary) from Coder.win:length(5);
select name, avg(salary) from Coder.win:length_batch(5);
非批量情况下,事件进入就会触发收集引擎回溯最近的五个事件,批量提交给后置处理器;批量情况下则是,每满5个事件才提交一次,不满则等待。
context
即使是同样的事件,也有很多情况下需要分组处理,context为Esper提供一种组别定义的能力,从而让事件流入不同的处理组中我们结合前面的窗格举一个实际的例子。
场景:游戏对局匹配
规则:十个人一组,进行5V5游戏,玩家按等级分为青铜、白银、黄金、白金、钻石、王者六种,为了保证游戏竞技性,只有级别相同的玩家才能共同进行一局游戏。
如果只是十个人对战,那个很简单,我们只需要按照前文介绍的,数量批量提交窗口,定义一个容量为10的窗格,每满十个,就交给后置处理器去创建对局。但是现在的场景又加入了玩家水平等级制,这时,就需要Context帮忙了。
// 省略Player模型的创建过程
// 创建context以Player的level属性作为组分割条件
String createContext = "create context forLevel partition by level from Player";
// 带上context前缀
String match = "context forLevel select * from Player.win:length_batch(10)";
admin.createEPL(createContext);
EPStatement state = admin.createEPL(match);
state.addListener(new StartGameListener());
pattern
pattern具有极强大的逻辑描述能力,本文暂不介绍,后续独立发文。
进程模型分析
其实看过了前面的内容这一部分,我们先搬官网的图出来看看:
不知道大家什么感觉,总之我是不愿意看的...我们来翻译一下第一张图:
官网给出的第二张事件流收集原理实际上可以看做是引擎入口处的EPL语句的解析过程。引擎中将事件分了三级缓存:
入门先到这里!
许多年前 你有一双清澈的双眼
想看遍这世界 去最遥远的远方
感觉有双翅膀 能飞越高山和海洋