輕量級內存計算引擎

內存計算指數據事先存儲於內存,各步驟中間結果不落硬盤的計算方式,適合性能要求較高,併發較大的狀況。html

HANA、TimesTen等內存數據庫可實現內存計算,但這類產品價格昂貴結構複雜實施困難,整體擁有成本較高。本文介紹的集算器一樣可實現內存計算,並且結構簡單實施方便,是一種輕量級內存計算引擎。java

下面就來介紹一下集算器實現內存計算的通常過程。程序員

1、        啓動服務器

集算器有兩種部署方式:獨立部署、內嵌部署,區別首先在於啓動方式有所不一樣。web

  • l   獨立部署

做爲獨立服務部署時,集算器與應用系統分別使用不一樣的JVM,二者能夠部署在同一臺機器上,也可分別部署。應用系統一般使用集算器驅動(ODBC或JDBC)訪問集算服務,也可經過HTTP訪問。算法

  • n  Windows下啓動獨立服務,執行「安裝目錄\esProc\bin\esprocs.exe」,而後點擊「啓動」按鈕。

  • n Linux下應執行「安裝目錄/esProc/bin/ServerConsole.sh」。

啓動服務器及配置參數的細節,請參考:http://doc.raqsoft.com.cn/esp...sql

  • l  內嵌部署

做爲內嵌服務部署時,集算器只能與JAVA應用系統集成,二者共享JVM。應用系統經過JDBC訪問內嵌的集算服務,無需特地啓動。數據庫

詳情參考http://doc.raqsoft.com.cn/esp...apache

2、        加載數據

加載數據是指經過集算器腳本,將數據庫、日誌、WebService等外部數據讀入內存的過程。json

好比Oracle中訂單表以下:服務器

訂單明細以下:

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/esp...

若是應用系統是JAVA WebServer,那麼須要編寫一個Servlet,在Servlet的init方法中經過JDBC執行initData.dfx,同時將該servlet設置爲啓動類,並在web.xml裏進行以下配置:

對於獨立部署的集算服務器,JAVA應用系統一樣要用JDBC接口執行集算器腳本,用法與內嵌服務相似。區別在於腳本存放於遠端,因此須要像下面這樣指定服務器地址和端口:

若是應用系統非JAVA架構,則應當使用ODBC執行集算器腳本,詳見http://doc.raqsoft.com.cn/esp...

對於獨立部署的服務器,也能夠脫離應用程序,在命令行手工執行initData.dfx。這種狀況下須要再寫一個腳本(如runOnServer.dfx):

而後在命令行用esprocx.exe調用runOnServer.dfx:

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

Linux下用法相似,參考http://doc.raqsoft.com.cn/esp...

3、        執行運算得到結果

數據加載到內存以後,就能夠編寫各類算法進行訪問,執行計算並得到結果,下面舉例說明:以客戶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)

爲方便調試和維護,也能夠分步驟編寫:

A1:按客戶ID過濾數據。其中,「訂單」就是加載數據時定義的全局變量,pCustID是外部參數,用於指定須要統計的客戶ID,函數select執行查詢。@m表示並行計算,可顯著提升性能。

A2:執行分組彙總,輸出計算結果。集算器默認返回有表達式的最後一個單元格,也就是A2。若是要返回指定單元的值,能夠用return語句

當pCustID=」VINET」時,計算結果以下:

須要注意的是,假如多個業務計算都要對客戶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的過程。具體腳本以下:

A1-B1:判斷是否存在全局變量「訂單明細」,若是不存在,則執行初始化數據腳本initData.dfx。

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

4、        引用思惟

       前面例子用到了select函數,這個函數的做用與SQL的where語句相似,均可進行條件查詢,但二者的底層原理大不相同。where語句每次都會複製一遍數據,生成新的結果集;而select函數只是引用原來的記錄指針,並不會複製數據。以按客戶查詢訂單爲例,引用和複製的區別以下圖所示:

能夠看到,集算器因爲採用了引用機制,因此計算結果佔用空間更小,計算性能更高(分配內存更快)。此外,對於上述計算結果還可再次進行查詢,集算器中新結果集一樣引用最初的記錄,而SQL就要複製出不少新記錄。

除了查詢以外,還有不少集算器算法都採用了引用思惟,好比排序、集合交併補、關聯、歸併。

5、        經常使用計算

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

事實上,集算器支持完善的結構化數據算法,好比:

  • l GROUP BY…HAVING

l ORDER BY…ASC/DESC

l DISTINCT

l   UNION/UNION ALL/INTERSECT/MINUS

  • 與SQL的交併補不一樣,集算器只是組合記錄指針,並不會複製記錄。
  • l SELECT … FROM (SELECT …)

l   SELECT (SELECT … FROM) FROM

l   CURSOR/FETCH

遊標有兩種用法,其一是外部JAVA程序調用集算器,集算器返回遊標,好比下面腳本:

JAVA得到遊標後可繼續處理,與JDBC訪問遊標的方法相同。

其二,在集算器內部使用遊標,遍歷並完成計算。好比下面腳本:

集算器適合解決複雜業務邏輯的計算,但考慮到簡單算法佔大多數,而不少程序員習慣使用SQL語句,因此集算器也支持所謂「簡單SQL」的語法。好比algorithm_1.dfx也可寫做:

上述腳本通用於任意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/esp..._sql_

6、        有序引用

SQL基於無序集合作運算,不能直接用序號取數,只能臨時生成序號,效率低且用法繁瑣。集算器與SQL體系不一樣,可以基於有序集合運算,能夠直接用序號取數。例如:

函數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,運貨費))

7、        關聯計算

關聯計算是關係型數據庫的核心算法,在內存計算中應用普遍,好比:統計每一年每個月的訂單數量和訂單金額。

該算法對應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)

A1:將訂單明細與訂單關聯起來,子表主表爲別名,點擊單元格可見結果以下

能夠看到,集算器join函數與SQL join語句雖然做用同樣,但結構原理大不相同。函數join關聯造成的結果,其字段值不是原子數據類型,而是記錄,後續可用「.」號表達關係引用,多層關聯很是方便。

A2:分組彙總。

計算結果以下:

關聯關係分不少類,上述訂單和訂單明細屬於其中一類:主子關聯。針對主子關聯,只需在加載數據時各自按關聯字段排序,業務算法中就可用歸併算法來提升性能。例如:

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

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

集算器的關聯計算與RDB不一樣,RDR對全部類型的關聯關係都採用相同的算法,沒法進行有針對性的優化,而集算器採起分而治之的理念,對不一樣類型的關聯關係提供了不一樣的算法,可進行有針對性的透明優化。

除了主子關聯,最經常使用的就是外鍵關聯,經常使用的外鍵表(或字典表)有分類、地區、城市、員工、客戶等。對於外鍵關聯,集算器也有相應的優化方法,即在數據加載階段事先創建關聯,如此一來業務算法就沒必要臨時關聯,性能所以提升,併發時效果尤其明顯。另外,集算器用指針創建外鍵關聯,訪問速度更快。

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

數據加載腳本(initData_3.dfx)以下:

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

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

加載數據時已經創建了外鍵指針關聯,因此A1中的「客戶ID」表示:訂單表的客戶ID字段所指向的客戶表記錄,「客戶ID.地區」即客戶表的地區字段。

腳本中多處使用「.」號表達關聯引用,語法比SQL直觀易懂,遇到多表多層關聯時尤其便捷。而在SQL中,關聯一多如同天書。

       上述計算結果以下:

8、        內外混合計算

內存計算雖然快,可是內存有限,所以一般只駐留最經常使用、併發訪問最多的數據,而內存放不下或訪問頻率低的數據,仍是要留在硬盤,用到的時候再臨時加載,並與內存數據共同參與計算。這就是所謂的內外混合計算。

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

案例描述:某零售行業系統中,訂單明細訪問頻率較低,數據量較大,不必也沒辦法常駐內存。如今要將訂單明細與內存裏的訂單關聯起來,統計出每一年每種產品的銷售數量。數據加載腳本(initData_4.dfx)以下:

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

A2:執行SQL,以遊標方式取訂單明細,以便計算遠超內存的大量數據。

A3:將訂單錶轉爲遊標模式,下一步會用到。

A4:關聯訂單明細表和訂單表。函數joinx與join@m做用相似,均可對有序數據進行歸併關聯,區別在於前者對遊標有效,後者對序表有效。

A5:執行分組彙總。

9、        數據更新

數據庫中的物理表總會變化,這種變化應當及時反映到共享的內存表中,才能保證內存計算結果的正確,這種狀況下就須要更新內存。若是物理表較小,那麼解決起來很容易,只要定時執行初始化數據腳本(initData.dfx)就能夠了。但若是物理表太大,就不能這樣作了,由於初始化腳本會進行全量加載,自己就會消耗大量時間,並且加載時沒法進行內存計算。例如:某零售巨頭訂單數據量較大,從數據庫全量加載到內存一般超過5分鐘,但爲保證必定的實時性,內存數據又須要5分鐘更新一次,顯然,二者存在明顯的矛盾。

解決思路其實很天然,物理表太大的時候,應該進行增量更新,5分鐘的增量業務數據一般很小,增量不會影響更新內存的效率。

要實現增量更新,就須要知道哪些是增量數據,不外乎如下三種方法:

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

方法B:在原庫建立一張「變動表」,將變動的數據記錄在內。好處是不動原表,缺點是仍然要動數據庫。

方法C:將變動表記錄在另外一個數據庫,或文本文件Excel中。好處是對原數據庫不作任何改動,缺點是增長了維護工做量。

集算器支持多數據源計算,因此方法B、C沒本質區別,下面就以B爲例更新訂單表。 

第一步,在數據庫中創建「訂單變動表」,繼承原表字段,新加一個「變動標記」字段,當用戶修改原始表時,須要在變動表同步記錄。以下所示的訂單變動表,表示新增1條修改2條刪除1條。

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

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.  }      

10、        綜合示例

下面,經過一個綜合示例來看一下在數據源多樣、算法複雜的狀況下,集算器如何很好地實現內存計算:

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

其中訂單表已加載到內存,以下:

郵費規則每次試算時都不一樣,所以由參數「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值是否知足免費標準,不知足則根據重量判斷郵費檔次,以後計算郵費。算完各訂單郵費後再計算總郵費,並將彙總結果附加爲訂單表的最後一條記錄。

數據加載過程很簡單,這裏再也不贅述,即:讀數據庫表,並命名爲「訂單表」。

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

A1:解析json,將其轉爲二維表。集算器支持多數據源,不只支持RDB,也支持NOSQL、文件、webService。

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

A4:新增空字段postage。

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

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

計算結果以下:

至此,本文詳細介紹了集算器用做內存計算引擎的完整過程,同時包括了經常使用計算方法和高級運算技巧。能夠看到,集算器具備如下顯著優勢:

  • l  結構簡單實施方便,可快速實現內存計算;
  • l   支持多種調用接口,應用集成沒有障礙;
  • l   支持透明優化,可顯著提高計算性能;
  • l   支持多種數據源,便於實現混合計算;
  • l   語法敏捷精妙,可輕鬆實現複雜業務邏輯。

       關於內存計算,還有個多機分佈式計算的話題,將在後續文章中進行介紹。

相關文章
相關標籤/搜索