Context是Esper裏一個很有意思的概念,要是理解爲上下文,我覺得有點不妥。以我的理解,Context就像一個框,把不同的事件按照框的規則框起來,並且有可能有多個框,而框與框之間不會互相影響。不知道各位在看完這篇文章後是否認同我的觀點,我願洗耳恭聽。
1.Context基本語法
語法結構如下
- create context context_name partition [by] event_property [and event_property [and ...]] from stream_def
- [, event_property [...] from stream_def] [, ...]
說明:
context_name爲context的名字,並且唯一。如果重複,會說明已存在。
event_property爲事件的屬性名,多個屬性名之間用and連接,也可以用逗號連接。
stream_def爲事件流的定義,簡單的定義可以是一個事件的名稱,比如之前定義了一個Map結構的事件爲User,那麼這裏就可以寫User。複雜的流定義後面會說到
舉個例子:
- create context NewUser partition by id and name from User
- // id和name是User的屬性
如果context包含多個流,例子如下:
- create context Person partition by sid from Student, tid from Teacher
- // sid是Student的屬性,tid是Teacher的屬性
多個流一定要注意,每個流的中用於context的屬性的數量要一樣,數據類型也要一致。比如下面這幾個就是錯誤的:
- create context Person partition by sid from Student, tname from Teacher
- // 錯誤:sid是int,tname是String,數據類型不一致
- create context Person partition by sid from Student, tid,tname from Teacher
- // 錯誤:Student有一個屬性,Teacher有兩個屬性,屬性數量不一致
- create context Person partition by sid,sname from Student, tname,tid from Teacher
- // 錯誤:sid對應tname,sname對應tid,並且sname和tname是String,sid和tid是int,屬性數量一樣,但是對應的數據類型不一致
實際上可以對進入context的事件增加過濾條件,不符合條件的就被過濾掉,就像下面這樣:
- create context Person partition by sid from Student(age > 20)
- // age大於20的Student事件才能建立或者進入context
看了這麼多,可能大家只是知道context的一些基本定義方法,但是不知道什麼意思。其實很簡單,partition by後面的屬性,就是作爲context的一個約束,比如說id,如果id相等的則進入同一個context裏,如果id不同,那就新建一個context。好比根據id分組,id相同的會被分到一個組裏,不同的會新建一個組並等待相同的進入。
如果parition by後面跟着同一個流的兩個屬性,那麼必須兩個屬性值一樣才能進入context。比如說A事件id=1,name=a,那麼會以1和a兩個值建立context,有點像數據庫裏的聯合主鍵。然後B事件id=1,name=b,則又會新建一個context。接着C事件id=1,name=a,那麼會進入A事件建立的context。
如果partition by後面跟着兩個流的一個屬性,那麼兩個屬性值一樣才能進入context。比如說Student事件sid=1,那麼會新建一個context,然後來了個Teacher事件tid=1,則會進入sid=1的那個context。多個流也一樣,不用關心是什麼事件,只用關心事件的屬性值一樣即可進入同一個context。
要是說了這麼多還是不懂,可以看看下面要講的context自帶屬性也許就能明白一些了。
2. Built-In Context Properties
Context本身自帶一些屬性,最關鍵的是可以查看所創建的context的標識,並幫助我們理解context的語法。
如上所示,name表示context的名稱,這個是不會變的。id是每個context的唯一標識,從0開始。key1和keyN表示context定義時所選擇的屬性的值,1和N表示屬性的位置。例如:
- EPL: create context Person partition by sid, sname from Student
- // key1爲sid,key2爲sname
爲了說明對這幾個屬性的應用,我舉了一個比較完整的例子。
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- class ESB
- {
- private int id;
- private int price;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- }
- class ContextPropertiesListener2 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("context.name " + event.get("name") + ", context.id " + event.get("id") + ", context.key1 " + event.get("key1")
- + ", context.key2 " + event.get("key2"));
- }
- }
- }
- public class ContextPropertiesTest2
- {
- public static void main(String[] args)
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- EPRuntime runtime = epService.getEPRuntime();
- String esb = ESB.class.getName();
- // 創建context
- String epl1 = "create context esbtest partition by id,price from " + esb;
- // context.id針對不同的esb的id,price建立一個context,如果事件的id和price相同,則context.id也相同,即表明事件進入了同一個context
- String epl2 = "context esbtest select context.id,context.name,context.key1,context.key2 from " + esb;
- admin.createEPL(epl1);
- EPStatement state = admin.createEPL(epl2);
- state.addListener(new ContextPropertiesListener2());
- ESB e1 = new ESB();
- e1.setId(1);
- e1.setPrice(20);
- System.out.println("sendEvent: id=1, price=20");
- runtime.sendEvent(e1);
- ESB e2 = new ESB();
- e2.setId(2);
- e2.setPrice(30);
- System.out.println("sendEvent: id=2, price=30");
- runtime.sendEvent(e2);
- ESB e3 = new ESB();
- e3.setId(1);
- e3.setPrice(20);
- System.out.println("sendEvent: id=1, price=20");
- runtime.sendEvent(e3);
- ESB e4 = new ESB();
- e4.setId(4);
- e4.setPrice(20);
- System.out.println("sendEvent: id=4, price=20");
- runtime.sendEvent(e4);
- }
- }
- sendEvent: id=1, price=20
- context.name esbtest, context.id 0, context.key1 1, context.key2 20
- sendEvent: id=2, price=30
- context.name esbtest, context.id 1, context.key1 2, context.key2 30
- sendEvent: id=1, price=20
- context.name esbtest, context.id 0, context.key1 1, context.key2 20
- sendEvent: id=4, price=20
- context.name esbtest, context.id 2, context.key1 4, context.key2 20
這個例子說得比較明白,針對不同的id和price,都會新建一個context,並context.id會從0開始增加作爲其標識。如果id和price一樣,事件就會進入之前已經存在的context,所以e3這個事件就會和e1一樣存在於context.id=0的context裏面。
對於epl2這個句子,意思是在esbtest這個context限制下進行事件的計算,不過這個句子很簡單,可以說沒有什麼計算,事件進入後就顯示出來了。實際上寫成什麼樣都可以,但是必須以context xxx開頭(xxx表示context定義時的名字),比如說:
- // context定義
- create context esbtest2 partition by id from ESB
- // 每當5個id相同的ESB事件進入時,統計price的總和
- context esbtest select sum(price) from ESB.win:length_batch(5)
- // 根據不同的id,統計3秒內進入的事件的平均price,且price必須大於10
- context esbtest select avg(price) from ESB(price>10).win:time(3 sec)
也許你會發現爲什麼我寫的句子都會帶有".win:length"或者".win:time",那是因爲我要計算的都是一堆事件,所以必須用一定條件才能把事件聚集起來。當然並不是一個事件沒法計算,只不過更多情況下計算都是以多個事件爲基礎的。關於這一點,學習到後面就會有更多的接觸。
3. Hash Context
前面介紹的Context語法是以事件屬性來定義的,Esper提供了以Hash值爲標準定義Context,通俗一點說就是提供事件屬性參與hash值的計算,計算的值再對某個值(這是什麼)是同餘的則進入到同一個context中。詳細語法如下:
- create context context_name coalesce [by]
- hash_func_name(hash_func_param) from stream_def
- [, hash_func_name(hash_func_param) from stream_def ]
- [, ...]
- granularity granularity_value
- [preallocate]
a). hash_func_name爲hash函數的名稱,Esper提供了CRC32或者使用Java的hashcode函數來計算hash值,分別爲consistent_hash_crc32和hash_code。你也可以自己定義hash函數,不過這需要配置。
b). hash_func_param爲參與計算的屬性列表,比如之前的sid或者tname什麼的。
c). stream_def就是事件類型,可以一個可以多個。不同於前面的Context語法要求,Hash Context不管有多個少屬性作爲基礎來計算hash值,hash值都只有一個,並且爲int型。所以就不用關心這些屬性的個數以及數據類型了。
d). granularity是必選參數,表示爲最多能創建多少個context
e). granularity_value就是那個用於取餘的“某個值”,因爲Esper爲了防止內存溢出,就想出了取餘這種辦法來限制context創建的數量。也就是說context.id=hash_func_name(hash_func_param) % granularity_value。
f). preallocate是一個可選參數,如果使用它,那麼Esper會預分配空間來創建granularity_value數量的context。比如說granularity_value爲1024,那麼Esper會預創建1024個context。內存不大的話不建議使用這個參數。
Hash Context同樣可以過濾事件,舉個完整的例子:
- // 以java的hashcode方法計算sid的值(sid必須大於5),以CRC32算法計算tid的值,然後對10取餘後的值來建立context
- create context HashPerson coalesce by hash_code(sid) from Student(sid>5), consistent_hash_crc32(tid) from Teacher granularity 10
Hash Context也有Built-In Context Properties,只不過只有context.id和context.name了。用法和前面說的一樣,這裏就不列舉了。
小貼士:
1.如果用於hash計算的屬性比較多,那麼就不建議使用CRC32算法了,因爲他會把這些屬性值先序列化字節數組以後才能計算hash值。hashcode方法相對它能快很多。
2.如果使用preallocate參數,建議granularity_value不要超過1000
3.如果granularity_value超過65536,引擎查找context會比較費勁,進而影響計算速度
4. Category Context
Category Context相對之前的兩類context要簡單許多,也更容易理解。語法說明如下:
- create context context_name
- group [by] group_expression as category_label
- [, group [by] group_expression as category_label]
- [, ...]
- from stream_def
我相信基本上不用我說,大家都能理解。group_expression表示分組策略的表達式,category_label爲策略定義一個名字,一個context可以有多個策略同時存在,但是特殊的是之能有一個stream_def。例如:
- create context CategoryByTemp
- group temp < 5 as cold, group temp between 5 and 85 as normal, group temp > 85 as large
- from Temperature
Category Context也有它自帶的屬性。
label指明進入的事件所處的group是什麼。完整例子如下:
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- class ESB3
- {
- private int id;
- private int price;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- }
- class ContextPropertiesListener4 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("context.name " + event.get("name") + ", context.id " + event.get("id") + ", context.label " + event.get("label"));
- }
- }
- }
- public class ContextPropertiesTest4
- {
- public static void main(String[] args)
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- EPRuntime runtime = epService.getEPRuntime();
- String esb = ESB3.class.getName();
- String epl1 = "create context esbtest group by id<0 as low, group by id>0 and id<10 as middle,group by id>10 as high from " + esb;
- String epl2 = "context esbtest select context.id,context.name,context.label, price from " + esb;
- admin.createEPL(epl1);
- EPStatement state = admin.createEPL(epl2);
- state.addListener(new ContextPropertiesListener4());
- ESB3 e1 = new ESB3();
- e1.setId(1);
- e1.setPrice(20);
- System.out.println("sendEvent: id=1, price=20");
- runtime.sendEvent(e1);
- ESB3 e2 = new ESB3();
- e2.setId(0);
- e2.setPrice(30);
- System.out.println("sendEvent: id=0, price=30");
- runtime.sendEvent(e2);
- ESB3 e3 = new ESB3();
- e3.setId(11);
- e3.setPrice(20);
- System.out.println("sendEvent: id=11, price=20");
- runtime.sendEvent(e3);
- ESB3 e4 = new ESB3();
- e4.setId(-1);
- e4.setPrice(40);
- System.out.println("sendEvent: id=-1, price=40");
- runtime.sendEvent(e4);
- }
- }
- sendEvent: id=1, price=20
- context.name esbtest, context.id 1, context.label middle
- sendEvent: id=0, price=30
- sendEvent: id=11, price=20
- context.name esbtest, context.id 2, context.label high
- sendEvent: id=-1, price=40
- context.name esbtest, context.id 0, context.label low
可以發現,id=0的事件,並沒有觸發監聽器,那是因爲context裏的三個category沒有包含id=0的情況,所以這個事件就被排除掉了。
5. Non-Overlapping Context
這類Context有個特點,是由開始和結束兩個條件構成context。語法如下:
- create context context_name start start_condition end end_condition
這個context有兩個條件做限制,形成一個約束範圍。當開始條件和結束條件都沒被觸發時,引擎會觀察事件的進入是否會觸發開始條件。如果開始條件被觸發了,那麼就新建一個context,並且觀察結束條件是否被觸發。如果結束條件被觸發,那麼context結束,引擎繼續觀察開始條件何時被觸發。所以說這類Context的另一個特點是,要麼context存在並且只有一個,要麼條件都沒被觸發,也就一個context都沒有了。
start_condition和end_condition可以是時間,或者是事件類型。比如說:
- create context NineToFive start (0, 9, *, *, *) end (0, 17, *, *, *)
- // 9點到17點此context纔可用(以引擎的時間爲準)。如果事件進入的事件不在此範圍內,則不受該context影響
我列了一個完整的例子,以某類事件開始,以某類事件結束
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- class StartEvent
- {
- }
- class EndEvent
- {
- }
- class OtherEvent
- {
- private int id;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- }
- class NoOverLappingContextTest3 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("Class:" + event.getUnderlying().getClass().getName() + ", id:" + event.get("id"));
- }
- }
- }
- public class NoOverLappingContextTest
- {
- public static void main(String[] args)
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- EPRuntime runtime = epService.getEPRuntime();
- String start = StartEvent.class.getName();
- String end = EndEvent.class.getName();
- String other = OtherEvent.class.getName();
- // 以StartEvent事件作爲開始條件,EndEvent事件作爲結束條件
- String epl1 = "create context NoOverLapping start " + start + " end " + end;
- String epl2 = "context NoOverLapping select * from " + other;
- admin.createEPL(epl1);
- EPStatement state = admin.createEPL(epl2);
- state.addListener(new NoOverLappingContextTest3());
- StartEvent s = new StartEvent();
- System.out.println("sendEvent: StartEvent");
- runtime.sendEvent(s);
- OtherEvent o = new OtherEvent();
- o.setId(2);
- System.out.println("sendEvent: OtherEvent");
- runtime.sendEvent(o);
- EndEvent e = new EndEvent();
- System.out.println("sendEvent: EndEvent");
- runtime.sendEvent(e);
- OtherEvent o2 = new OtherEvent();
- o2.setId(4);
- System.out.println("sendEvent: OtherEvent");
- runtime.sendEvent(o2);
- }
- }
執行結果:
- sendEvent: StartEvent
- sendEvent: OtherEvent
- Class:blog.OtherEvent, id:2
- sendEvent: EndEvent
- sendEvent: OtherEvent
由此可以看出,在NoOverLapping這個Context下監控OtherEvent,必須是在StartEvent被觸發才能監控到,所以在EndEvent發送後,再發送一個OtherEvent是不會觸發Listener的。
6. OverLapping
OverLapping和NoOverLapping一樣都有兩個條件限制,但是區別在於OverLapping的初始條件可以被觸發多次,並且只要被觸發就會新建一個context,但是當終結條件被觸發時,之前建立的所有context都會被銷燬。他的語法也很簡單:
- create context context_name initiated [by] initiating_condition terminated [by] terminating_condition
initiating_condition和terminating_condition可以爲事件類型,事件或者別的條件表達式。下面給出了一個完整的例子。
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- class InitialEvent{}
- class TerminateEvent{}
- class SomeEvent
- {
- private int id;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- }
- class OverLappingContextListener implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("context.id:" + event.get("id") + ", id:" + event.get("id"));
- }
- }
- }
- class OverLappingContextListener2 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("Class:" + event.getUnderlying().getClass().getName() + ", id:" + event.get("id"));
- }
- }
- }
- public class OverLappingContextTest
- {
- public static void main(String[] args)
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- EPRuntime runtime = epService.getEPRuntime();
- String initial = InitialEvent.class.getName();
- String terminate = TerminateEvent.class.getName();
- String some = SomeEvent.class.getName();
- // 以InitialEvent事件作爲初始事件,TerminateEvent事件作爲終結事件
- String epl1 = "create context OverLapping initiated " + initial + " terminated " + terminate;
- String epl2 = "context OverLapping select context.id from " + initial;
- String epl3 = "context OverLapping select * from " + some;
- admin.createEPL(epl1);
- EPStatement state = admin.createEPL(epl2);
- state.addListener(new OverLappingContextListener());
- EPStatement state1 = admin.createEPL(epl3);
- state1.addListener(new OverLappingContextListener2());
- InitialEvent i = new InitialEvent();
- System.out.println("sendEvent: InitialEvent");
- runtime.sendEvent(i);
- SomeEvent s = new SomeEvent();
- s.setId(2);
- System.out.println("sendEvent: SomeEvent");
- runtime.sendEvent(s);
- InitialEvent i2 = new InitialEvent();
- System.out.println("sendEvent: InitialEvent");
- runtime.sendEvent(i2);
- TerminateEvent t = new TerminateEvent();
- System.out.println("sendEvent: TerminateEvent");
- runtime.sendEvent(t);
- SomeEvent s2 = new SomeEvent();
- s2.setId(4);
- System.out.println("sendEvent: SomeEvent");
- runtime.sendEvent(s2);
- }
- }
執行結果:
- sendEvent: InitialEvent
- context.id:0, id:0
- sendEvent: SomeEvent
- Class:blog.SomeEvent, id:2
- sendEvent: InitialEvent
- context.id:1, id:1
- context.id:0, id:0
- sendEvent: TerminateEvent
- sendEvent: SomeEvent
從結果可以看得出來,每發送一個InitialEvent,都會新建一個context,以至於context.id=0和1。並且當發送TerminateEvent後,再發送SomeEvent監聽器也不會被觸發了。
另外,context.id是每一種Context都會有的自帶屬性,而且針對OverLapping,還增加了startTime和endTime兩種屬性,表明context的開始時間和結束時間。
7. Context Condition
Context Condition主要包含Filter,Pattern,Crontab以及Time Period
A). Filter主要就是對屬性值的過濾,比如:
- create context NewUser partition by id from User(id > 10)
B). Pattern是複雜事件流的代表,比如說“A事件到達後跟着B事件到達”這是一個完整的Pattern。Pattern是Esper裏面很特別的東西,並且用它描述複雜的事件流是最合適不過的了。這裏暫且不展開說,後面會有專門好幾篇來講解Pattern。
C). Crontab是定時任務,主要用於NoOverLapping,就像前面提到的(0, 9, *, *, *),括號裏的五項代表分,時,天,月,年。關於這個後面也會有講解。
D). Time Period在這裏只有一種表達式,就是after time_period_expression。例如:after 1 minute,after 5 sec。結合Context的例子如下:
- // 以0秒爲時間初始點,新建一個context,於10秒後開始,1分鐘後結束。下一個context從1分20秒開始
- create context NonOverlap10SecFor1Min start after 10 seconds end after 1 minute
8. Context Nesting
Context也可以嵌套,意義就是多個Context聯合在一起組成一個大的Context,以滿足複雜的限制需求。語法結構:
- create context context_name
- context nested_context_name [as] nested_context_definition ,
- context nested_context_name [as] nested_context_definition [, ...]
舉個例子:
- create context NineToFiveSegmented
- context NineToFive start (0, 9, *, *, *) end (0, 17, *, *, *),
- context SegmentedByUser partition by userId from User
應用和普通的Context沒區別,在此就不舉例了。另外針對嵌套Context,其自帶的屬性使用方式會有些變化。比如針對上面這個,若想查看NineToFive的startTime和SegmentedByUser的第一個屬性值,要按照下面這樣寫:
- context NineToFiveSegmented select
- context.NineToFive.startTime,
- context.SegmentedByUser.key1
- from User
9. Output When Context Partition Ends
當Context銷燬時,如果你想同時查看此時Context裏的東西,那麼Esper提供了一種辦法來輸出其內容。例如:
- create context OverLapping initiated InitialEvent terminated TerminateEvent
- context OverLapping select * from User output snapshot when terminated
那麼當終結事件發送到引擎後,會立刻輸出OverLapping的快照。
如果你想以固定的頻率查看Context的內容,Esper也支持。例如:
- context OverLapping select * from User output snapshot every 2 minute // 每兩分鐘輸出OverLapping的事件
關於output表達式,後面也會有詳解。
以上的內容算是包含了Context的所有方面,可能還有些細節需要各位自己去研讀他的手冊,並且多加練習。Esper的內容之多以至於我說了很多次“後面會專門講解”,不過也確實是因爲內容複雜,所以不得不先跳過這些。在學習到之後的內容以後,再回過頭來理解Context可能會有另一番效果。