天天看點

輕量級記憶體計算引擎

記憶體計算指資料事先存儲于記憶體,各步驟中間結果不落硬碟的計算方式,适合性能要求較高,并發較大的情況。

HANA、TimesTen等記憶體資料庫可實作記憶體計算,但這類産品價格昂貴結構複雜實施困難,總體擁有成本較高。本文介紹的集算器同樣可實作記憶體計算,而且結構簡單實施友善,是一種輕量級記憶體計算引擎。

下面就來介紹一下集算器實作記憶體計算的一般過程。

一、        啟動伺服器

集算器有兩種部署方式:獨立部署、内嵌部署,差別首先在于啟動方式有所不同。

l   獨立部署

作為獨立服務部署時,集算器與應用系統分别使用不同的JVM,兩者可以部署在同一台機器上,也可分别部署。應用系統通常使用集算器驅動(ODBC或JDBC)通路集算服務,也可通過HTTP通路。

n   Windows下啟動獨立服務,執行“安裝目錄\esProc\bin\esprocs.exe”,然後點選“啟動”按鈕。

輕量級記憶體計算引擎

n   Linux下應執行“安裝目錄/esProc/bin/ServerConsole.sh”。

啟動伺服器及配置參數的細節,請參考:http://doc.raqsoft.com.cn/esproc/tutorial/fuwuqi.html。

l   内嵌部署

作為内嵌服務部署時,集算器隻能與JAVA應用系統內建,兩者共享JVM。應用系統通過JDBC通路内嵌的集算服務,無需特意啟動。

詳情參考http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html。

二、        加載資料

加載資料是指通過集算器腳本,将資料庫、日志、WebService等外部資料讀入記憶體的過程。

比如Oracle中訂單表如下:

訂單ID(key) 客戶ID 訂單日期 運貨費
10248 VINET 2012-07-04 32.38
10249 TOMSP 2012-07-05 11.61
10250 HANAR 2012-07-08 65.83
10251 VICTE 41.34
10252 SUPRD 2012-07-09 51.3

訂單明細如下:

訂單ID(key)(fk) 産品ID(key) 單價 數量
17 14 12
42 9 10
72 34 5
18
51 40

将上述兩張表加載到記憶體,可以使用下面的集算器腳本(initData.dfx):

A
1 =connect("orcl")
2 =A1.query("select   訂單ID,客戶ID,訂單日期,運貨費 from 訂單").keys(訂單ID)
3 =A1.query@x("select   訂單ID,産品ID,單價,數量 from 訂單明細") .keys(訂單ID,産品ID)
4 =env(訂單,A2)
=env(訂單明細,A3)

A1:連接配接Oracle資料庫。

A2-A3:執行SQL查詢,分别取出訂單表和訂單明細表。query@x表示執行SQL後關閉連接配接。函數keys可建立主鍵,如果資料庫已定義主鍵,則無需使用該函數。

A4-A5:将兩張表常駐記憶體,分别命名為訂單和訂單明細,以便将來在業務計算時引用。函數env的作用是設定/釋放全局共享變量,以便在同一個JVM下被其他算法引用,這裡将記憶體表設為全局變量,也就是将全表資料儲存在記憶體中,供其他算法使用,也就實作了記憶體計算。事實上,對于外存表、檔案句柄等資源也可以用這個辦法設為全局變量,使變量駐留在記憶體中。

腳本需要執行才能生效。

對于内嵌部署的集算服務,通常在應用系統啟動時執行腳本。如果應用系統是JAVA程式,可以在程式中通過JDBC執行initData.dfx,關鍵代碼如下:

1.       com.esproc.jdbc.InternalConnection con=null;

2.       try {

3.           Class.forName("com.esproc.jdbc.InternalDriver");

4.             con   =(com.esproc.jdbc.InternalConnection)DriverManager.getConnection("jdbc:esproc:local://");

5.             ResultSet rs = con.executeQuery("call   initData()");

6.       } catch (SQLException e){

7.           out.println(e);

8.       }finally{

9.           if   (con!=null) con.close();

10.   }

這篇文章詳細介紹了JAVA調用集算器的過程http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html

如果應用系統是JAVA WebServer,那麼需要編寫一個Servlet,在Servlet的init方法中通過JDBC執行initData.dfx,同時将該servlet設定為啟動類,并在web.xml裡進行如下配置:

 <servlet>

     <servlet-name>myServlet</servlet-name>

     <servlet-class>com.myCom.myProject.myServlet   </servlet-class>

     <load-on-startup>3</load-on-startup>

    </servlet>

對于獨立部署的集算伺服器,JAVA應用系統同樣要用JDBC接口執行集算器腳本,用法與内嵌服務類似。差別在于腳本存放于遠端,是以需要像下面這樣指定伺服器位址和端口:

st = con.createStatement();

st.executeQuery("=callx(\“initData.dfx\”;[\“127.0.0.1:8281\”])");

如果應用系統非JAVA架構,則應當使用ODBC執行集算器腳本,詳見http://doc.raqsoft.com.cn/esproc/tutorial/odbcbushu.html

對于獨立部署的伺服器,也可以脫離應用程式,在指令行手工執行initData.dfx。這種情況下需要再寫一個腳本(如runOnServer.dfx):

=callx(“initData.dfx”;[“127.0.0.1:8281”])

然後在指令行用esprocx.exe調用runOnServer.dfx:

D:\raqsoft64\esProc\bin>esprocx   runOnServer.dfx

Linux下用法類似,參考http://doc.raqsoft.com.cn/esproc/tutorial/minglinghang.html

三、        執行運算獲得結果

資料加載到記憶體之後,就可以編寫各種算法進行通路,執行計算并獲得結果,下面舉例說明:以客戶ID為參數,統計該客戶每年每月的訂單數量。

該算法對應的Oracle中的SQL語句如下:

select   to_char(訂單日期,'yyyy') AS 年份,to_char(訂單日期,'MM') AS 月份, count(1) AS 訂單數量

from   訂單  

where客戶ID=?

group   by to_char(訂單日期,'yyyy'),to_char(訂單日期,'MM')

在集算器中,應當編寫如下業務算法(algorithm_1.dfx)

=訂單.select@m(客戶ID==pCustID).groups(year(訂單日期):年份, month(訂單日期):月份;count(1):訂單數量)

為友善調試和維護,也可以分步驟編寫:

=訂單.select@m(客戶ID==pCustID)

=A1.groups(year(訂單日期):年份, month(訂單日期):月份;

count(1):訂單數量)

A1:按客戶ID過濾資料。其中,“訂單”就是加載資料時定義的全局變量,pCustID是外部參數,用于指定需要統計的客戶ID,函數select執行查詢。@m表示并行計算,可顯著提高性能。

A2:執行分組彙總,輸出計算結果。集算器預設傳回有表達式的最後一個單元格,也就是A2。如果要傳回指定單元的值,可以用return語句

當pCustID=”VINET”時,計算結果如下:

年份 月份 訂單數量
2012 7
8
2013 11

需要注意的是,假如多個業務計算都要對客戶ID進行查詢,那不妨在加載資料時把訂單按客戶ID排序,這樣後續業務算法中就可以使用二分法進行快速查詢,也就是使用select@b函數。具體實作上,initData.dfx中SQL應當改成:

=A1.query("select   訂單ID,客戶ID,訂單日期,運貨費 from 訂單 order by 客戶ID")

相應的,algorithm_1.dfx中的查詢應當改成:

=訂單.select@b(客戶ID==pCustID)

執行腳本獲得結果的方法,前面已經提過,下面重點說說報表,這類最常用的應用程式。

由于報表工具都有可視化設計界面,是以無需用JAVA代碼調用集算器,隻需将資料源配置為指向集算服務,在報表工具中以存儲過程的形式調用集算器腳本。

對于内嵌部署的集算伺服器,調用語句如下:

call algorithm_1(”VINET”)

由于本例中算法非常簡單,是以事實上可以不用編寫獨立的dfx腳本,而是在報表中直接以SQL方式書寫表達式:

=訂單.select@m(客戶ID==”VINET”).groups(year(訂單日期):年份, month(訂單日期):月份;count(1):訂單數量)

對于獨立部署的集算伺服器,遠端調用語句如下:

=callx(“algorithm_1.dfx”,”VINET”;[“127.0.0.1:8281”])

有時,需要在記憶體進行的業務算法較少,而web.xml不友善添加啟動類,這時可以在業務算法中調用初始化腳本,達到自動初始化的效果,同時也省去編寫servlet的過程。具體腳本如下:

B
if   !ifv(訂單) =call("initData.dfx")
=A2.groups(year(訂單日期):年份, month(訂單日期):月份;

A1-B1:判斷是否存在全局變量“訂單明細”,如果不存在,則執行初始化資料腳本initData.dfx。

A2-A3:繼續執行原算法。

四、        引用思維

       前面例子用到了select函數,這個函數的作用與SQL的where語句類似,都可進行條件查詢,但兩者的底層原理大不相同。where語句每次都會複制一遍資料,生成新的結果集;而select函數隻是引用原來的記錄指針,并不會複制資料。以按客戶查詢訂單為例,引用和複制的差別如下圖所示:

輕量級記憶體計算引擎

可以看到,集算器由于采用了引用機制,是以計算結果占用空間更小,計算性能更高(配置設定記憶體更快)。此外,對于上述計算結果還可再次進行查詢,集算器中新結果集同樣引用最初的記錄,而SQL就要複制出很多新記錄。

除了查詢之外,還有很多集算器算法都采用了引用思維,比如排序、集合交并補、關聯、歸并。

五、        常用計算

回顧前面案例,可以看到集算器語句和SQL語句存在如下的對應關系:

計算 SQL 集算器
查詢 select
條件 Where….訂單.客戶ID=? 訂單ID.客戶ID==pCustID
分組彙總 group   by groups
日期函數 to_char(訂單日期,'yyyy') year(訂單日期)
别名 AS   年份 :年份

事實上,集算器支援完善的結構化資料算法,比如:

l   GROUP BY…HAVING

=訂單.groups(year(訂單日期):年份;count(1):訂單數量).select(訂單數量>300)

l   ORDER BY…ASC/DESC

=訂單.sort(客戶ID,-訂單日期) /排序隻是變換了記錄指針的次序,并沒有複制記錄

l   DISTINCT

=訂單.id(year(訂單日期)) /取唯一值
=A1.(客戶ID) /所有出現值
=訂單.([ year(訂單日期),客戶ID]) /組合的所有出現值

l    UNION/UNION ALL/INTERSECT/MINUS

=訂單.select(運貨費>100)
=訂單.select([2011,2012].pos(year(訂單日期))
=A2|A3 /UNION   ALL
=A2&A3 /UNION
=A2^A3 /INTERSECTION
6 =A2\A3 /DIFFERENCE

與SQL的交并補不同,集算器隻是組合記錄指針,并不會複制記錄。

l   SELECT … FROM (SELECT …)

=訂單.select(訂單日期>date("2010-01-01")) /執行查詢
=A1.count() /對結果集再統計

l    SELECT (SELECT … FROM) FROM

=訂單.new(訂單ID,客戶.select(客戶ID==訂單.客戶ID).客戶名) /客戶表和訂單表都是全局變量

l    CURSOR/FETCH

遊标有兩種用法,其一是外部JAVA程式調用集算器,集算器傳回遊标,比如下面腳本:

=訂單.select(訂單日期>=date("2010-01-01")).cursor()

JAVA獲得遊标後可繼續處理,與JDBC通路遊标的方法相同。

其二,在集算器内部使用遊标,周遊并完成計算。比如下面腳本:

=訂單.cursor()
for A1,100 =A2.select(訂單日期>=date("2010-01-01"))  /每次取100條運算

集算器适合解決複雜業務邏輯的計算,但考慮到簡單算法占大多數,而很多程式員習慣使用SQL語句,是以集算器也支援所謂“簡單SQL”的文法。比如algorithm_1.dfx也可寫作:

$()   select year(訂單日期) AS 年份,month(訂單日期) AS 月份,count(1) AS 訂單數量

From   {訂單} 

where訂單.客戶ID='VINET'

group   by year(訂單日期),month(訂單日期)

上述腳本通用于任意SQL,$()表示執行預設資料源(集算器)的SQL語句,如果指定資料源名稱比如$(orcl),則可以執行相應資料庫(資料源名稱是orcl的Oracle資料庫)的SQL語句。

from {}語句可從任意集算器表達式取數,比如:from {訂單.groups(year(訂單日期):年份;count(1):訂單數量)}

from 也可從檔案或excel取數,比如:from d:/emp.xlsx

簡單SQL同樣支援join…on…語句,但由于SQL語句(指任意RDB)在關聯算法上性能較差,是以不建議輕易使用。對于關聯運算,集算器有專門的高性能實作方法,後續章節會有介紹。

簡單SQL的詳情可以參考:http://doc.raqsoft.com.cn/esproc/func/dbquerysql.html#db_sql_

六、        有序引用

SQL基于無序集合做運算,不能直接用序号取數,隻能臨時生成序号,效率低且用法繁瑣。集算器與SQL體系不同,能夠基于有序集合運算,可以直接用序号取數。例如:

=訂單.sort(訂單日期) /如果加載時已排序,這步可省略
=A1.m(1).訂單ID /第一條
=A1.m(-1).訂單ID /最後一條
=A1.m(to(3,5)) /第3-5條

函數m()可按指定序号擷取成員,參數為負表示倒序。參數也可以是集合,比如m([3,4,5])。而利用函數to()可按起止序号生成集合,to(3,5)=[3,4,5]。

前面例子提到過二分法查詢select@b,其實已經利用了集算器有序通路的特點。

有時候我們想取前 N名,正常的思路就是先排序,再按位置取前N個成員,集算器腳本如下:

=訂單.sort(訂單日期).m(to(100))

對應SQL寫法如下:

select   top(100) * from 訂單 order by 訂單日期   --MSSQL

select   * from (select * from 訂單 order by 訂單日期) where rownum<=100    --Oracle

但上述正常思路要對資料集大排序,運算效率很低。除了正常思路,集算器還有更高效的實作方法:使用函數top。

=訂單.top(100;訂單日期)

函數top隻排序出訂單日期最早的N條記錄,然後中斷排序立刻傳回,而不是正常思路那樣進行全量排序。由于底層模型的限制,SQL不支援這種高性能算法。

函數top還可應用于計算列,比如拟對訂單采取新的運貨費規則,求新規則下運貨費最大的前100條訂單,而新規則是:如果原運貨費大于等于1000,則運貨費打八折。

集算器腳本為:

=訂單.top(-100;if(運貨費>=1000,運貨費*0.8,運貨費))

七、        關聯計算

關聯計算是關系型資料庫的核心算法,在記憶體計算中應用廣泛,比如:統計每年每月的訂單數量和訂單金額。

該算法對應Oracle的SQL語句為:

select   to_char(訂單.訂單日期,'yyyy') AS 年份,to_char(訂單.訂單日期,'MM') AS 月份,sum(訂單明細.單價*訂單明細.數量) AS 銷售金額,count(1) AS 訂單數量

from   訂單明細 left join 訂單 on 訂單明細.訂單ID=訂單.訂單ID

group   by to_char(訂單.訂單日期,'yyyy'),to_char(訂單.訂單日期,'MM')

用集算器實作上述算法時,加載資料的腳本不變,業務算法如下(algorithm_2.dfx)

=join(訂單明細:子表,訂單ID;訂單:主表,訂單ID)
=A1.groups(year(主表.訂單日期):年份, month(主表.訂單日期):月份; sum(子表.單價*子表.數量):銷售金額, count(1):訂單數量)

A1:将訂單明細與訂單關聯起來,子表主表為别名,點選單元格可見結果如下

輕量級記憶體計算引擎

可以看到,集算器join函數與SQL join語句雖然作用一樣,但結構原理大不相同。函數join關聯形成的結果,其字段值不是原子資料類型,而是記錄,後續可用“.”号表達關系引用,多層關聯非常友善。

A2:分組彙總。

計算結果如下:

銷售金額
28988 57
26799 71
27201
37793.7 69
49704 66

關聯關系分很多類,上述訂單和訂單明細屬于其中一類:主子關聯。針對主子關聯,隻需在加載資料時各自按關聯字段排序,業務算法中就可用歸并算法來提高性能。例如:

=join@m(訂單明細:子表,訂單ID;訂單:主表,訂單ID)

函數join@m表示歸并關聯,隻對同序的兩個或多個表有效。

集算器的關聯計算與RDB不同,RDR對所有類型的關聯關系都采用相同的算法,無法進行有針對性的優化,而集算器采取分而治之的理念,對不同類型的關聯關系提供了不同的算法,可進行有針對性的透明優化。

除了主子關聯,最常用的就是外鍵關聯,常用的外鍵表(或字典表)有分類、地區、城市、員工、客戶等。對于外鍵關聯,集算器也有相應的優化方法,即在資料加載階段事先建立關聯,如此一來業務算法就不必臨時關聯,性能是以提高,并發時效果尤為明顯。另外,集算器用指針建立外鍵關聯,通路速度更快。

比如這個案例:訂單表的客戶ID字段是外鍵,對應客戶表(客戶ID、客戶名稱、地區、城市),需要統計出每個地區每個城市的訂單數量。

資料加載腳本(initData_3.dfx)如下:

=A1.query@x(“select   客戶ID,地區,城市 from 客戶”).keys(客戶ID)
=A2.switch(客戶ID,A3:客戶ID)
=env(客戶,A3)

A4:用函數switch建立外鍵關聯,将訂單表的客戶ID字段,替換為客戶表相應記錄的指針。

業務算法腳本如下(algorithm_3.dfx)如下

=訂單.groups(客戶ID.地區:地區 ,客戶ID.城市:城市;count(1):訂單數量)

加載資料時已經建立了外鍵指針關聯,是以A1中的“客戶ID”表示:訂單表的客戶ID字段所指向的客戶表記錄,“客戶ID.地區”即客戶表的地區字段。

腳本中多處使用“.”号表達關聯引用,文法比SQL直覺易懂,遇到多表多層關聯時尤為便捷。而在SQL中,關聯一多如同天書。

       上述計算結果如下:

地區 城市
東北 大連
華東 南京 89
南昌 15
常州 35
溫州

八、        内外混合計算

記憶體計算雖然快,但是記憶體有限,是以通常隻駐留最常用、并發通路最多的資料,而記憶體放不下或通路頻率低的資料,還是要留在硬碟,用到的時候再臨時加載,并與記憶體資料共同參與計算。這就是所謂的内外混合計算。

下面舉例說明集算器中的内外混合計算。

案例描述:某零售行業系統中,訂單明細通路頻率較低,資料量較大,沒必要也沒辦法常駐記憶體。現在要将訂單明細與記憶體裡的訂單關聯起來,統計出每年每種産品的銷售數量。

資料加載腳本(initData_4.dfx)如下:

=A1.query@x("select   訂單ID,客戶ID,訂單日期,運貨費 from 訂單 order by 訂單ID").keys(訂單ID)

業務算法腳本(algorithm_4.dfx)如下:

=A1.cursor@x("select   訂單ID,産品ID,數量 from 訂單明細order by 訂單ID")
=joinx(A2:子表,訂單ID; A3:主表,訂單ID)
=A4.groups(year(主表.訂單日期):年份,子表.産品ID:産品 ;sum(子表.數量):銷售數量)

A2:執行SQL,以遊标方式取訂單明細,以便計算遠超記憶體的大量資料。

A3:将訂單表轉為遊标模式,下一步會用到。

A4:關聯訂單明細表和訂單表。函數joinx與join@m作用類似,都可對有序資料進行歸并關聯,差別在于前者對遊标有效,後者對序表有效。

A5:執行分組彙總。

九、        資料更新

資料庫中的實體表總會變化,這種變化應當及時反映到共享的記憶體表中,才能保證記憶體計算結果的正确,這種情況下就需要更新記憶體。如果實體表較小,那麼解決起來很容易,隻要定時執行初始化資料腳本(initData.dfx)就可以了。但如果實體表太大,就不能這樣做了,因為初始化腳本會進行全量加載,本身就會消耗大量時間,而且加載時無法進行記憶體計算。例如:某零售巨頭訂單資料量較大,從資料庫全量加載到記憶體通常超過5分鐘,但為保證一定的實時性,記憶體資料又需要5分鐘更新一次,顯然,兩者存在明顯的沖突。

解決思路其實很自然,實體表太大的時候,應該進行增量更新,5分鐘的增量業務資料通常很小,增量不會影響更新記憶體的效率。

要實作增量更新,就需要知道哪些是增量資料,不外乎以下三種方法:

方法A:在原表加标記字段以識别。缺點是會改動原表。

方法B:在原庫建立一張“變更表”,将變更的資料記錄在内。好處是不動原表,缺點是仍然要動資料庫。

方法C:将變更表記錄在另一個資料庫,或文本檔案Excel中。好處是對原資料庫不做任何改動,缺點是增加了維護工作量。

集算器支援多資料源計算,是以方法B、C沒本質差別,下面就以B為例更新訂單表。

第一步,在資料庫中建立“訂單變更表”,繼承原表字段,新加一個“變更标記”字段,當使用者修改原始表時,需要在變更表同步記錄。如下所示的訂單變更表,表示新增1條修改2條删除1條。

變更标記
10247 101 新增
102 修改
103
104 删除

第二步,編寫集算器腳本updatemem_4.dfx,進行資料更新。

=訂單cp=訂單.derive()
=A1.query("select 訂單ID,客戶ID,訂購日期 訂單日期,運貨費,變更标記 from 訂單變更")
=訂單删除=A3.select(變更标記=="删除") =訂單cp.select(訂單删除.(訂單ID).contain(訂單ID))
=訂單cp.delete(B4)
=訂單新增=A3.select(變更标記=="新增") =訂單cp.insert@f(0:訂單新增)
=訂單修改=A3.select(變更标記=="修改") =訂單cp.select(訂單修改.(訂單ID).pos(訂單ID))
=訂單cp.delete(B7)
=訂單cp.insert@f(0:訂單修改)
=env(訂單,訂單cp)
=A1.execute("delete from 訂單變更")
=A1.close()

A1:建立資料庫連接配接。

A2:将記憶體中的訂單複制一份,命名為訂單cp。下面過程隻針對訂單cp進行修改,修改完畢再替代記憶體中的訂單,期間訂單仍可正常進行業務計算。

A3:取資料庫訂單變更表。

A4-B5:取出訂單變更表中需删除的記錄,在訂單cp中找到這些記錄,并删除。

A6-B6:取出訂單變更表中需新增的記錄,在訂單cp中追加。

A7-B9:這一步是修改訂單cp,相當于先删除再追加。也可用modify函數實作修改。

A10:将修改後的訂單cp常駐記憶體,命名為訂單。

A11-A12:清空“變更表”,以便下次取新的變更記錄。

上述腳本實作了完整的資料更新,而實際上很多情況下隻需要追加資料,這樣腳本還會簡單很多。

       腳本編寫完成後,還需第三步:定時5分鐘執行該腳本。   

       定時執行的方法有很多。如果集算器部署為獨立服務,與Web應用沒有共用JVM,那麼可以使用作業系統自帶的定時工具(計劃任務或crontab),使其定時執行集算器指令(esprocx.exe或esprocx.sh)。

有些web應用有自己的定時任務管理工具,可定時執行某個JAVA類,這時可以編寫JAVA類,用JDBC調用集算器腳本。

       如果web應用沒有定時任務管理工具,那就需要手工實作定時任務,即編寫JAVA類,繼承java内置的定時類TimerTask,在其中調用集算器腳本,再在啟動類中調用定時任務類。

       其中啟動類myServle4為:

1.     import java.io.IOException;      

2.     import java.util.Timer;      

3.     import javax.servlet.RequestDispatcher;      

4.     import javax.servlet.ServletContext;      

5.     import javax.servlet.ServletException;      

6.     import javax.servlet.http.HttpServlet;      

7.     import javax.servlet.http.HttpServletRequest;      

8.     import javax.servlet.http.HttpServletResponse;      

9.     import org.apache.commons.lang.StringUtils;      

10.   public class myServlet4 extends HttpServlet {      

11.       private static final long serialVersionUID = 1L;      

12.       private Timer timer1 = null;      

13.       private Task task1;                

14.       public ConvergeDataServlet() {      

15.           super();      

16.       }      

17.       public void destroy() {      

18.           super.destroy();       

19.           if(timer1!=null){      

20.               timer1.cancel();      

21.           }      

22.       }      

23.       public void doGet(HttpServletRequest request, HttpServletResponse response)      

24.               throws ServletException, IOException {      

25.       }      

26.       public void doPost(HttpServletRequest request, HttpServletResponse response)      

27.               throws ServletException, IOException {      

28.           doGet(request, response);             

29.       }      

30.       public void init() throws ServletException {      

31.           ServletContext context = getServletContext();      

32.           // 定時重新整理時間(5分鐘)      

33.           Long delay = new Long(5);      

34.           // 啟動定時器      

35.           timer1 = new Timer(true);      

36.           task1 = new Task(context);      

37.           timer1.schedule(task1, delay * 60 * 1000, delay * 60 * 1000);      

38.       }      

39.   } 

定時任務類Task為:

11.  import java.util.TimerTask;      

12.  import javax.servlet.ServletContext;  

13.   import java.sql.*;

14.   import com.esproc.jdbc.*;    

15.  public class Task extends TimerTask{      

16.      private ServletContext context;      

17.      private static boolean isRunning = true;      

18.      public Task(ServletContext context){      

19.          this.context = context;      

20.      }      

21.      @Override     

22.      public void run() {      

23.          if(!isRunning){      

24.                com.esproc.jdbc.InternalConnection con=null;

25.                try {

26.                      Class.forName("com.esproc.jdbc.InternalDriver");

27.                      con =(com.esproc.jdbc.InternalConnection)DriverManager.getConnection("jdbc:esproc:local://");

28.                      ResultSet rs = con.executeQuery("call updatemem_4()");

29.                }

30.                catch (SQLException e){

31.                      out.println(e);

32.                }finally{

33.                    //關閉資料集

34.                          if (con!=null) con.close();

35.                }

36.          }      

37.      }      

38.  }      

十、        綜合示例

下面,通過一個綜合示例來看一下在資料源多樣、算法複雜的情況下,集算器如何很好地實作記憶體計算:

案例描述:某B2C網站需要試算訂單的郵寄總費用,以便在一定成本下挑選合适的郵費規則。大部分情況下,郵費由包裹的總重量決定,但當訂單的價格超過指定值時(比如300美元),則提供免費付運。結果需輸出各訂單郵寄費用以及總費用。

其中訂單表已加載到記憶體,如下:

Id cost weight
Josh1 150
Drake 100
Megan
Josh2 200
Josh3 500

郵費規則每次試算時都不同,是以由參數“pRule”臨時傳入,格式為json字元串,某次規則如下:

[{"field":"cost","minVal":300,"maxVal":1000000,"Charge":0},

{"field":"weight","minVal":0,"maxVal":1,"Charge":10},

{"field":"weight","minVal":1,"maxVal":5,"Charge":20},

{"field":"weight","minVal":5,"maxVal":10,"Charge":25},

{"field":"weight","minVal":10,"maxVal":1000000,"Charge":40}]

上述json串表示各字段在各種取值範圍内時的郵費。第一條記錄表示,cost字段取值在300與1000000之間的時候,郵費為0(免費付運);第二條記錄表示,weight字段取值在0到1(kg)之間時,郵費為10(美元)。

思路:将json串轉為二維表,分别找出filed字段為cost和weight的記錄,再對整個訂單表進行循環。循環中先判斷訂單記錄中的cost值是否滿足免費标準,不滿足則根據重量判斷郵費檔次,之後計算郵費。算完各訂單郵費後再計算總郵費,并将彙總結果附加為訂單表的最後一條記錄。

資料加載過程很簡單,這裡不再贅述,即:讀資料庫表,并命名為“訂單表”。

業務算法相對複雜,具體如下:

C D
= pRule.export@j() /解析json,轉二維表
=免費=A1.select(field=="cost") /取免費标準,單條
=收費=A1.select(field=="weight").sort(-minVal) /取收費階梯,多條
=訂單表.derive(postage) /複制并新增字段
for A4 if 免費.minVal < A5.cost >A5. postage= 免費.Charge
next
for 收費 if A5.weight > B7.minVal >A5.postage=B7.Charge
next A5
=A4.record(["sum",,,A4.sum(postage)])

A1:解析json,将其轉為二維表。集算器支援多資料源,不僅支援RDB,也支援NOSQL、檔案、webService。

A2-A3:查詢郵費規則,分為免費和收費兩種。

A4:新增空字段postage。

A5-D8:按兩種規則循環訂單表,計算相應的郵費,并填入postage字段。這裡多處用到流程控制,集算器用縮進表示,其中A5、B7為循環語句,C6、D8跳入下一輪循環,B5、C7為判斷語句

A9:在訂單表追加新紀錄,填入彙總值。

postage
25
20
sum 75

       至此,本文詳細介紹了集算器用作記憶體計算引擎的完整過程,同時包括了常用計算方法和進階運算技巧。可以看到,集算器具有以下顯著優點:

l   結構簡單實施友善,可快速實作記憶體計算;

l   支援多種調用接口,應用內建沒有障礙;

l   支援透明優化,可顯著提升計算性能;

l   支援多種資料源,便于實作混合計算;

繼續閱讀