JAVA 應用必須經過 JDBC 從數據庫中取數,有時候咱們會發現,數據庫的負擔一點也不重並且 SQL 很簡單,但取數的速度仍然很慢。仔細測試會發現,性能瓶頸主要在 JDBC 上,好比 MySQL 的 JDBC 性能就很是差,Oracle 也很差。可是,JDBC 是數據庫廠商提供的包,咱們在外部沒辦法提升性能。數據庫
能夠想到的辦法是利用多 CPU 手段採用並行方案來提速,但 Java 的並行程序很是難寫,要考慮資源共享衝突等麻煩事務。編程
下面介紹使用集算器的並行技術來提高數據庫 JDBC 取數性能,能夠避免 JAVA 硬編碼的複雜性,還可以方便實現多線程結果集的合併。適用於:多線程
經過集算器進行並行取數前須要配置集算器的並行屬性。IDE 中經過菜單「工具 - 選項」設置 IDE 支持的最大並行數量,通常建議最大並行數不要超過 CPU 核數。
集算器服務端則須要修改 raqsoftConfig.xml 配置:
函數
有時咱們查詢的某個表數據量較大、時間較長,這時就能夠經過集算器針對單表並行取數提高性能。這裏所謂的單表是指經過條件並行讀取一份(單表)數據。工具
全內存性能
假設內存能夠容納所有要讀取的數據,並行取數後再進行下一步運算(全內存的計算速度最快)。
舉例測試
訂單(Orders)有訂單 ID,訂購日期,訂單金額等字段,其中訂單 ID 是遞增的整數邏輯主鍵。編碼
【計算目標】 並行讀取某時間段內訂單數spa
面向單表(單條 SQL)並行取數須要經過參數將源數據拆分多段,創建多個數據庫鏈接並行查詢。每每須要將數據儘量平均拆分以免查詢時間不均致使任務等待,同時分段參數儘量創建在索引字段上以保證分段效率。
集算器實現線程
集算器參數
根據查詢時間段創建腳本參數,查詢起止日期
集算器實現
分段策略(一)基於索引字段分段
基於單表(單 SQL)並行取數前須要進行數據分段,儘可能保證每一個分段的數據平均。而分段參數儘可能基於創建索引的字段(如訂單編號)。之因此要使用索引字段來分段,是由於使用索引並不會真地遍歷整個表,而是直接定位,當數據量較大時優點明顯。
集算器腳本
編寫並行取數腳本,這裏按照創建索引的訂單編號進行分段:
A | B | C | |
---|---|---|---|
1 | =connect(「db」) | ||
2 | =A1.query(「select min( 訂單 ID) 最小 ID,max(訂單 ID) 最大 ID from 訂單 where 訂購日期 >=? and 訂購日期 <=?」,begin,end) | =b=A2. 最小 ID | =e=A2. 最大 ID |
3 | =p=4 | / 並行數 | |
4 | =p.(b+(e-b)*~\p) | / 分段參數終值 | |
5 | =b | A4.to(,p-1).(~+1) | / 分段參數初值 |
6 | fork A5,A4 | ||
7 | =connect(「db」) | ||
8 | =B7.query@x(「select * from 訂單 where 訂單 ID>=? and 訂單 ID<=? and 訂購日期 >=? and 訂購日期 <=?」,A6(1),A6(2),begin,end) | ||
9 | =A6.conj() | / 合併查詢結果 |
腳本解析:
一、A2 根據查詢起止日期得到最大訂單編號和最小訂單編號,用於後面分段
二、B2-C2 將最小訂單號和最大訂單號分別賦值給變量 b 和 e
三、A3 設置並行數,使用並行取數前應檢查集算器的並行數配置以及受權中對並行數量的許可
四、A4-A5 根據起止訂單編號和並行數計算每一個並行任務的起止分段參數(序列)
五、經過 fork 啓動多個(4 個)線程,參數爲分段起止參數序列,這裏能夠看到 fork 啓動的線程數與參數序列成員數相同。在集算器中,常常將序表、序列做爲參數值參與運算,很是方便
六、B7 爲每一個線程(子任務)創建數據庫鏈接,須要注意鏈接必須在 fork 子句中創建,以便爲多線程分別使用,若共用一個鏈接沒法起到加速取數的效果,數據庫會自動把同一鏈接上的多個請求改成串行執行。所以只有當數據庫負擔不重,有足夠多鏈接可用時纔可使用並行取數提高性能
七、B8 分別查詢每一個分段數據,查詢結果返回到 A6 格。這裏 fork 子句直接返回查詢結果(子句最後一行),若是想返回其中某個或某幾個計算值能夠顯示使用 return 關鍵字返回子線程計算結果
八、返回結果的 A6 格結果,4 個線程返回 4 個結果集
九、A9 合併全部子線程查詢結果,以便進行下一步計算
基於索引字段進行數據分段,而且數據分段比較平均時,使用多線程並行查詢數據庫幾乎能夠得到線程數倍(線性)的性能提高。
分段策略(二)基於非索引字段分段
若是數據庫負擔不重時,也能夠基於非索引字段進行分段(如日期),相對 JDBC 取數時間,屢次遍歷庫表時間也並非很大,而這樣作的好處是不須要事先查詢數據庫以肯定起止段界(如最大最小訂單編號)。
集算器腳本
A | B | C | ||
---|---|---|---|---|
1 | =connect(「db」) | |||
2 | =n=interval(begin,end) | |||
3 | =p=4 | / 並行數 | ||
4 | =p.(n*~ \p).(elapse(begin,~)) |
/ 分段參數終值 | ||
5 | =begin | A4.to(,p-1).(after(~,-1)) | / 分段參數初值 | |
6 | fork A5,A4 | |||
7 | =connect(「db」) | |||
8 | =B7.query@x(「select * from 訂單 where 訂購日期 >=? and 訂購日期 <=?」,A6(1),A6(2)) | |||
9 | =A6.conj() | / 合併查詢結果 |
腳本解析:
與上述使用索引字段訂單 ID 分段不一樣,這裏使用非索引字段訂購日期進行分段。設起止日期爲:2012-01-01 和 2015-12-31。
一、A2 計算起止日期查詢參數間隔天數,用於分段;interval 函數還能夠計算年、季度、月、時、分、秒等間隔,用於日期時間處理很方便
二、A3-A5 根據並行數和日期間隔計算分段起止參數序列
三、A6 根據參數序列啓動多線程,B8 完成查詢並將結果返回到 A6 格,A9 合併查詢結果
分段策略(三)並行線程數多於 CPU 核數
前面咱們提到:建議並行任務數不要超過 CPU 核數,由於更多的任務數並不會增長並行度,並且還能夠避免 CPU 進行線程切換帶來的額外時間開銷。但有時也能夠將任務數設置到遠大於 CPU 核數,能夠設置爲 CPU 核數的倍數個,這樣多 CPU 負載也能夠達到動態平衡,並且某些計算還能夠簡化分段。如上述例子中,若只查詢某一年數據就能夠把線程數設置爲 12(月),從而簡化分段。
集算器提供了多線程任務動態平衡機制,當任務數大於並行數配置時,集算器會自動爲計算結束的線程分配下一個任務,這時能夠保證某個線程會多跑幾個小任務,另外一個線程只跑少許大任務,達到整體平衡,而沒必要拘泥於必須把數據量平均分配。
集算器腳本
A | B | C | |
---|---|---|---|
1 | fork to(1,12) | ||
2 | =connect(「demo」) | ||
3 | =B2.query@x(「select * from 訂單 wheremonth( 訂購日期)=? and 訂購日期 >=? and 訂購日期 <=?」,A1,begin,end) | ||
4 | =A1.conj() | / 合併查詢結果 |
腳本解析:
一、A1 根據 1 到 12 的序列啓動 12 個線程
二、B3 每一個線程查詢一個月的數據並返回結果到 A1
關於 fork 語句
在集算器中,經過 fork 語句能夠啓動多個線程實施並行計算,並且集算器還提供了多種 merge 函數能夠很方便合併並行結果,十分方便。
外存
有時某一條語句(一個表)的數據量較大,分段後並行子任務仍然沒法所有加載到內存中,這時須要使用集算器提供的外存計算機制,基於遊標查詢數據。
舉例
沿用上面的例子,假設分段後的數據量很大須要使用遊標分批讀取處理數據。
集算器實現
A | B | C | ||
---|---|---|---|---|
1 | =connect(「db」) | |||
2 | =A1.query(「select min( 訂單 ID) 最小 ID,max(訂單 ID) 最大 ID from 訂單 where 訂購日期 >=? and 訂購日期 <=?」,begin,end) | =b=A2. 最小 ID | =e=A2. 最大 ID | |
3 | =p=4 | / 並行數 | ||
4 | =p.(b+(e-b)*~\p) | / 分段參數終值 | ||
5 | =b` | `A4.to(,p-1).(~+1) | / 分段參數初值 | |
6 | fork A5,A4 | |||
7 | =connect(「db」) | |||
8 | =B7.cursor@x(「select * from 訂單 where 訂單 ID>=? and 訂單 ID<=? and 訂購日期 >=? and 訂購日期 <=?」,A6(1),A6(2),begin,end) | |||
9 | =A6.mcursor() | / 合併查詢結果 | ||
10 | =file(「D:\ 訂單.txt」).export@t(A9) | / 基於遊標寫入文件 |
腳本解析:
一、B8 創建數據庫遊標,查詢並不真正取數,多個遊標返回到 A6 格
二、A9 合併多路遊標,接下來就能夠當作一個遊標繼續使用
三、A10 基於遊標,將查詢數據分批寫入文件中。由於各個線程的運行速度沒法保證規律性,因此基於多線程導出數據時次序不可控,對數據順序有要求時不能使用這個方法。
基於外存遊標並行查詢與全內存方式很是相似,當內存資源較緊張時能夠經過外存計算的方式減小內存佔用。
除了經過條件針對單條 SQL(單表)進行並行取數外,在一些多 SQL 查詢場景(如報表多數據集)下仍然能夠經過並行同時執行多條語句進行取數。
舉例
有多個查詢 SQL 基於多個表查詢數據,須要提高查詢性能。
【計算目標】 並行讀取 5 個表數據,並完成關聯
這裏咱們使用 5 條很是簡單(基於單表)的查詢 SQL,實際業務中多條SQL 能夠任意複雜。
集算器實現
A | B | C | |
---|---|---|---|
1 | =connect(「db」) | ||
2 | =」select * from 訂單 where 訂購日期 >=date(‘」/begin/」‘) and 訂購日期 <=date(‘」/end/」‘)」 | ||
3 | select 訂單 ID, 產品 ID, 單價, 數量 from 訂單明細 | ||
4 | select 客戶 ID, 公司名稱 from 客戶 | ||
5 | select 僱員 ID, 姓名 from 僱員 | ||
6 | select 產品 ID, 產品名稱 from 產品 | ||
7 | fork [A2:A6] | ||
8 | =connect(「db」) | ||
9 | =B8.query@x(A7) | ||
10 | = 訂單 =A7(1) | = 明細 =A7(2) | |
11 | = 客戶 =A7(3) | = 僱員 =A7(4) | = 產品 =A7(5) |
12 | > 訂單.switch(客戶 ID, 客戶: 客戶 ID; 僱員 ID, 僱員: 僱員 ID) | ||
13 | = 明細.switch(訂單 ID, 訂單: 訂單 ID; 產品 ID, 產品: 產品 ID) | ||
14 | =A13.new(訂單 ID. 客戶 ID. 公司名稱: 客戶名稱, 訂單 ID. 訂單 ID: 訂單編號, 訂單 ID. 僱員 ID. 姓名: 銷售, 產品 ID. 產品名稱: 產品, 單價: 價格, 數量) |
腳本解析:
一、A2-A6 爲查詢用 SQL 語句串
二、A7 根據多條 SQL 組成序列啓動多線程(5 個)
三、B9 每一個線程執行 SQL 查詢數據將結果返回到 A7 格(5 個結果集組成的序列)
四、A10-C11 經過序號分別獲取 5 個結果集
五、爲了保證完整性,A12-A14 對 5 個結果集進行關聯並經過外鍵屬性化的方式建立結果序表
以上是集算器並行取數的部分示例,事實上集算器還能夠作更復雜的並行計算和結果歸併。集算器多線程並行的意義在於使用簡單、成本低,相對 JAVA 複雜的多線程編程集算器能夠簡單到幾行腳本,相對數據庫集羣方案集算器的成本更加可控,並且即便部署數據庫集羣仍然可使用集算器加速集羣單個數據庫節點的取數速度。