鍵值查詢是很常見的查詢場景,在數據表上建有索引後,即便表中數據記錄數巨大(幾億甚至幾十億行),用鍵值查詢出單條記錄也會很快,由於創建索引後的複雜度只有 logN(以 2 爲底)次, 10 億行數據也只要比較 30 次(10 億約等於 2^30),在現代計算機上也只須要數十毫秒而已。css
不過,若是須要查詢的鍵值不少,好比多達幾千甚至幾萬的時候,若是每次都獨立查找,那讀取和比較也會累積到幾萬甚至幾十萬次,時間延遲由此也會漲到幾十分鐘甚至小時級別,這時候簡單地使用數據庫索引對於用戶體驗必然是難以容忍的了。html
下面咱們要介紹的集算器組表功能,基於高性能索引和批量鍵值查找,能夠有效地應對這種場景。咱們會按照如下幾種順序逐步深刻討論:java
1)單字段鍵數據庫
2)多字段鍵數據結構
3)多線程查詢多線程
4)數據追加的處理app
須要說明的,本文只研討單機的狀況,後續還有文章會繼續深刻討論基於集羣的方案。函數
咱們如下表這種比較典型的數據結構爲例:工具
字段名稱性能 |
類型 |
是否主鍵 |
說明 |
id |
long |
是 |
從 1 開始自增 |
data |
string |
須要獲取的數據 |
首先咱們建立一個組表,把源數據導入組表:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create(#id,data) |
3 |
=file("single_source.txt") |
4 |
=A3.cursor@t() |
5 |
=A2.append(A4) |
A1:創建並打開指定文件對象,這裏的 single.ctx 是將要建立的組表文件,擴展名用 ctx。關於組表的詳細定義和使用方法能夠參考集算器教程。
A2:建立組表的數據結構爲(id,data)。其中,# 開頭的字段稱爲維字段,組表將假定數據按該字段有序,也就是組表 A2 將對鍵字段 id 有序。組表默認使用列存方式。
A3:假定源數據以文本方式存儲,A3 打開數據文件。這個 txt 文件的數據表頭以及前幾行部分數據以下圖所示。固然,源數據也能夠來自數據庫等其它類型的數據源。
A4:針對源數據生成遊標。其中 @t 選項指明文件中第一行記錄是字段名。
A5:將遊標 A4 中的記錄追加寫入組表。
上面的腳本假定主鍵 id 在原始數據中已是有序的了,若是實際狀況的主鍵並不是有序,那就須要先將主鍵排序後再建爲組表。這時可使用cs.sortx()函數排序,具體方法詳見函數參考。
在集算器的設計器中,經過三行代碼,能夠直觀看到其中前十條數據,代碼和截圖以下所示:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create() |
3 |
=A2.cursor().fetch(10) |
A1:打開指定文件名的組表文件對象。
A2:f.create(),函數中無參數時則直接打開組表。
A3:使用遊標訪問 A2 中的前十條數據,以下圖。
接下來,咱們爲組表文件創建索引,以提高檢索性能:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create() |
3 |
=A2.index(id_idx;id;data) |
A1:打開指定文件名的組表文件對象。
A2:使用無參數的 create 函數打開組表。
A3:創建索引。在函數 T.index() 中,參數 id_idx 是索引名稱,id 是維,data 是數據列。通常狀況下,建索引並不須要使用數據列,在用索引查找時會到原數據表中再去找數據。不過,本例在創建索引時將數據列也包含進了索引,這樣查找時就再也不引用數據列了,雖然佔用的空間會大一些,可是查找的也會更快一些。
按維字段建索引時會利用組表已經有序的特色再也不排序。若是開始建組表時沒有使用 #,那麼這時建索引時就會從新排序。
使用主、子程序調用的方式來完成查詢:
查詢子程序腳本 search.dfx:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create() |
3 |
=keys |
4 |
=A2.icursor(;A3.contain(id),id_idx) |
5 |
>file("result.txt").export@t(A4) |
A3:keys 是參數,由下面的主程序在調用時傳遞。
A4:在組表的 icursor()這個函數中,使用索引 id_idx,以條件 A3.contain(id) 來過濾組表。集算器會自動識別出 A3.contain(id) 這個條件可使用索引,並會自動將 A3 的內容排序後從前向後查找。
A5:將 A4 中查詢出的結果導出至 result.txt。這裏 @t 選項指定導出時將輸出字段名。
主程序腳本:
A |
|
1 |
=file("keys.txt").import@i() |
2 |
=call("search.dfx",A1) |
A1:從keys.txt獲取查詢鍵值序列,由於只有一列結果,可使用 @i 選項,將結果返回成序列:
這個序列就是須要進行查詢的隨機鍵值集。例子中使用 keys.txt 來預先存好隨機的鍵值,實際應用中,也能夠用其餘數據源來存儲。
A2:調用子程序 serach.dfx,把 A1 得到的鍵值集做爲參數傳遞給子程序。
下面就是結果文件 result.txt 中的部份內容:
另外,咱們還能夠將集算器嵌入到 Java 應用程序中,從而爲 Java 應用提供靈活、簡便的數據查詢能力。嵌入時能夠像用 JDBC 訪問數據庫那樣訪問集算器腳本。具體的寫法能夠參閱教程《被 JAVA 調用》一節。
本例的單字段鍵查詢示例,在數據結構上較爲簡單。其中查詢的鍵值字段爲 id,須要獲取的數據爲單列的 data,若是還有其它列,例如:
字段名稱 |
類型 |
是否主鍵 |
說明 |
id |
Long |
是 |
從 1 開始自增 |
data1 |
String |
須要獲取的數據 1 |
|
data2 |
Int |
須要獲取的數據 2 |
|
…… |
…… |
…… |
|
dataN |
…… |
須要獲取的數據 N |
那麼在創建索引步驟時,就應該包含多個數據列字段,數據列參數的寫法以下所示:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create() |
3 |
=A2.index(id_idx;id;data1,data2,…,dataN) |
在接下來要討論的多字段鍵狀況中,建索引時須要創建多個索引字段,對應參數部分也有相似的寫法:index(id_idx;id1,id2,…,idN;data1,data2,…,dataN)。
多字段健指的是聯合主鍵的狀況,例如:
字段名稱 |
類型 |
是否主鍵 |
說明 |
type |
string |
可枚舉 |
|
Id |
long |
每種枚舉類型的 id 都從 1 開始自增 |
|
data |
string |
須要獲取的數據 |
其中 type 和 id 兩個字段做爲聯合主鍵肯定一條記錄。
A |
|
1 |
=file("multi.ctx") |
2 |
=A1.create(#type,#id,data) |
3 |
=file("multi_source.txt") |
4 |
=A3.cursor@t() |
5 |
=A2.append(A4) |
本例中 A2 須要指定兩個維,type和 id,代碼其它部分與單字段鍵一致。
A |
|
1 |
=file("multi.ctx") |
2 |
=A1.create() |
3 |
=A2.index(type_id_idx;type,id;data) |
因爲本例中有兩個維,創建索引時須要包含 type 和 id 兩個維,如 A3 所示。
A |
|
1 |
=file("multi.ctx") |
2 |
=A1.create() |
3 |
=[["type_a",55],["type_b",66]] |
4 |
=A2.icursor(;A3.contain([type,id]),type_id_idx) |
5 |
>file("result.txt").export@t(A4) |
A3準備了兩條數據,是由 type 和 id 構成的二元組,做爲查找的建值集,結構以下圖所示:
A4:A3.contain([type,id]),基於二元組的序列進行數據的篩選,因此須要將 contain 函數中的參數也變爲二元組。
最終導出的結果文件內容以下:
雖然多字段鍵能夠直接使用,可是涉及到集合的存儲和比較都要慢一些。爲了獲取高性能,更經常使用的辦法是把多字段鍵拼成單字段鍵。
觀察本例數據結構,雖然 type 是個串,但倒是可枚舉的,所以能夠將 type 數字化後,與 id 合併爲一個新的主鍵字段。而 long 類型最大值爲 2^63-1,徹底能夠容納 id 和 type 數字化後的合併結果。咱們把 type 和 id 合併後的新主鍵叫作 nid,能夠按數據的規模,肯定 nid 中分別用幾位表明 type 和 id。
舉例來講,id 的範圍是 9 位數,type 的枚舉個數用 3 位數表示就夠了。所以對於 nid 而言,須要 13 位(爲了不前幾位是 0,看上去不整齊,咱們把第一位數字設爲 1)。這樣就能夠把聯合主鍵變成單字段的惟一主鍵,去掉第一位後的 12 位數,前 3 位表明數字化後的 type,後 9 位就是原來的 id。
代碼以下:
A |
|
1 |
=["type_a",……,"type_z","type_1",……,"type_9","type_0"] |
2 |
=A1.new(#:tid,~:type) |
3 |
=file("multi_source.txt") |
4 |
=A3.cursor@t() |
5 |
=A4.switch(type,A2:type) |
6 |
=A4.new(1000000000000+type.tid*long(1000000000)+id:nid,data) |
7 |
=A4.skip(99999995) |
8 |
=A4.fetch(10) |
A1:type 的枚舉值組成的序列。在實際狀況中,枚舉列表可能來自文件或者數據庫數據源。。
A2:給枚舉值序列中每一個 type 一個 tid。爲後續的數字化主鍵合併作準備。
A3~A6:從 multi_source.txt 文件中獲取數據,並按照 A2 中的對應關係,把 type 列的枚舉串變成數字,而後將 type 和 id 進行合併後,生成新的主鍵 nid。
A7~A8:查看一下合併逐漸後的數據狀況,跳過遊標 A4 的前 99999995 條記錄後,取 10 條記錄,結果以下:
這樣就獲得了新的「單字段建」的數據結構:
字段名稱 |
類型 |
是否主鍵 |
說明 |
nid |
long |
是 |
包含 type 和 id 信息的惟一主鍵 |
data |
string |
須要獲取的數據 |
接下來按照 "單字段鍵" 中的作法就能夠處理了,固然還要注意確保 nid 有序。
在上述方法的基礎上,咱們還能夠採用多線程並行方式來進一步提升性能。
所謂多線程並行,就是把數據分紅 N 份,用 N 個線程查詢。但若是隻是隨意地將數據分紅 N 份,極可能沒法真正地提升性能。由於將要查詢的鍵值集是未知的,因此理論上也沒法確保但願查找的數據可以均勻分佈在每一份組表文件中。比較好的處理方式是先觀察鍵值集的特徵,從而儘量地進行數據的均勻拆分。
好比說,繼續使用上文中多字段鍵拼成單字段鍵的例子,將合併後的主鍵 nid 對 4 取模,餘數相同的數據存在同一個組表中,最終由 4 個組表文件裝載現有所有數據。這樣的文件拆分方法,可使被查詢的數據分佈的相對更加均勻一些。
若是鍵值數據有比較明顯的業務特徵,咱們能夠考慮按照實際業務場景使用日期、部門之類的字段來處理文件拆分。如:將屬於部門 A 的 1000 條記錄均分在 10 個文件中,每一個文件就有 100 條記錄。在利用多線程查詢屬於部門 A 的記錄時,每一個線程就會從各自對應的文件中取數相應的這 100 條記錄了。
下面咱們來看個實際的例子。
A |
|
1 |
=["type_a",……,"type_z","type_1",……,"type_9","type_0"] |
2 |
=A1.new(#:tid,~:type) |
3 |
=file("multi_source.txt") |
4 |
=A3.cursor@t() |
5 |
=A4.switch(type,A2:type) |
6 |
=A4.new(1000000000000+type.tid*long(1000000000)+id:nid,data) |
7 |
=N.(file("nid_"+string(~-1)+"_T.ctx").create(#nid,data)) |
8 |
=N.(eval("channel(A4).select(nid%N=="+string(~-1)+").attach(A7("+string(~)+").append(~.cursor()))")) |
9 |
for A6,500000 |
A1~A6:與多字段鍵的方法二一致。
A7:使用循環函數,建立名爲「鍵值名 _ 鍵值取 N 的餘數 _T.ctx」的組表文件,其結構同爲 (#nid,data)。
A8:用循環函數將遊標數據分別追加到 N 個原組表上。好比當 N=1 時,拼出的 eval 函數參數爲:channel(A4).select(nid%4==0).attach(A7(1).append(~.cursor()))。意思是對遊標 A4 建立管道,將管道中記錄按鍵值 nid 取 4 的餘數,將餘數值等於 0 的記錄過濾出來。attach 是對當前管道的附加運算,表示取和當前餘數值對應的原組表,將當前管道中篩選過濾出的記錄,以遊標記錄的方式追加到 A7(1),即第 1 個組表。
A9:循環遊標 A6,每次獲取 50 萬條記錄,直至 A6 遊標中的數據取完。
執行後,產出 4(這時例子取 N=4)個獨立的組表文件:
A |
B |
|
1 |
fork directory@p("nid*T.ctx") |
=file(A1).create().index(nid_idx;nid;data) |
A1:列出知足 nid*T.ctx 的文件名(這裏 * 爲通配符),這裏 @p 選項表明須要返回帶有完整路徑信息的文件名。使用 fork 執行多線程時,須要注意環境中的並行限制數是否設置合理。這裏用了 4 個線程,設計器中對應的設置以下:
B2:每一個線程爲各個組表創建對應的索引文件,最終結果以下:
A |
B |
|
1 |
=file("keys.txt").import@i() |
|
2 |
=A1.group(~%N) |
|
3 |
fork N.(~-1),A2 |
=A3(2) |
4 |
=file("nid_"/A3(1)/"_T.ctx").create().icursor(;B3.contain(nid),nid_idx) |
|
5 |
return B4 |
|
6 |
=A3.conjx() |
|
7 |
=file("result_nid.txt").export@t(A6) |
A1:從 keys.txt 獲取查詢鍵值序列,由於只有一列結果,使用 @i 選項,將結果返回成序列:
A2:把 A1 的序列按 4 的餘數進行等值分組:
A三、B3~B5:用 fork 函數,按等值分組後的鍵值對各個組表分別並行查詢。這裏的 fork 後面分別寫了兩個參數,第一個是循環函數 N.(~-1),第二個是 A2。在接下來的 B三、B4 中分別使用 A3(2) 和 A3(1) 來獲取 fork 後面這兩個對應順序的參數,B4:對組表文件進行根據 B3 中的鍵值集進行數據篩選,B5:返回遊標。因爲 A3 中是多個線程返回的遊標序列,因此 A6 中須要使用 conjx 對多個遊標進行縱向鏈接。
A6~A7:將多個線程返回的遊標進行縱向鏈接後,導出遊標記錄至文本文件,前幾行內容以下。
前面咱們已經解決了針對大數據的批量隨機鍵值查詢問題,不過,咱們不能假定數據永遠不變。尤爲是對於大數據來講,新數據的追加是必然要面對的。在將新數據追加到原有組表文件中時,咱們須要討論三種狀況:有序鍵值追加、無序鍵值追加,以及數據量很大時的數據追加。
單個文件時,若是鍵值有序,追加的代碼以下:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create() |
3 |
=file("single_add.txt") |
4 |
=A3.cursor@t() |
5 |
=A2.append(A4) |
A1:single.ctx 是已有的組表文件,結構爲 (#id,data),其中 id 爲自增鍵值。
A3~A5:新數據文件與已有文件結構相同,其 id 加入原組表後,對於總體數據也是有序的。這種狀況能夠直接追加到原組表,組表會自動更新索引。
若是按按多線程的方法拆分爲多個文件,代碼以下:
A |
|
1 |
=file("single_add.txt") |
2 |
=A1.cursor@t() |
3 |
=directory@p("id*T.ctx").(file(~).create()) |
4 |
=N.(eval("channel(A2).select(id%N=="+string(~-1)+").attach(A3("+string(~)+").append([~.cursor()]))")) |
5 |
for A2,500000 |
A一、A2:用遊標方式獲取新增數據。
A3:知足通配符串:"id*T.ctx",現有 N 份組表文件的序列。
A四、A5:與前面方法中的代碼一致。
一樣先來看一下單個文件的追加方法,以單字段鍵爲例,代碼以下:
A |
|
1 |
=file("single.ctx") |
2 |
=A1.create().cursor() |
3 |
=file("single_add.txt") |
4 |
=A3.cursor@t() |
5 |
=file("single.ctx_temp").create(#id,data) |
6 |
=A5.append([A2,A4].mergex(id)) |
A2:遊標方式打開現有組表。
A4:遊標方式獲取新增數據。
A5:建個新的組表文件。
A6:在新的組表中存放現有組表數據和新增數據歸併後的結果。這裏要注意的是,用 cs.mergex(x) 進行歸併操做,須要 cs 序列對 x 有序,也就是要求組表文件 A1 和新增數據文件 A3 中的數據對於 id 都分別有序。若不知足 cs 對 x 有序,程序雖然不會報錯,可是歸併出來的結果也是無序的。
當這段代碼執行完後,還須要進行舊組表、舊索引的清理以及對新組表的創建索引等操做:
A |
|
1 |
=movefile(file("single.ctx")) |
2 |
=movefile(file("single.ctx__id_idx")) |
3 |
=movefile(file("single.ctx_temp"),"single.ctx")) |
4 |
=file("single.ctx").create().index(id_idx;id;data) |
前三行是文件操做,詳見函數參考:movefile。A4 爲組表創建索引,再也不詳述。
下面再看看多個文件的追加方法,以多字段鍵轉單字段鍵後的數據結構 (nid,data) 爲例,代碼以下:
A |
|
1 |
=["type_a",……,"type_z","type_1",……,"type_9","type_0"] |
2 |
=A1.new(#:tid,~:type) |
3 |
=file("multi_source_add.txt") |
4 |
=A3.cursor@t() |
5 |
=A4.switch(type,A2:type) |
6 |
=A4.new(1000000000000+type.tid*long(1000000000)+id:nid,data) |
7 |
=directory@p("nid*T.ctx").(file(~).create().cursor()) |
8 |
=directory@p("nid*T.ctx").(file(~+"_temp").create(#nid,data)) |
9 |
=N.(eval("channel(A4).select(nid%N=="+string(~-1)+").attach(A8("+string(~)+").append([~.cursor(),A7("+string(~)+")].mergex(nid)))")) |
10 |
for A4,500000 |
A3:multi_source_add.txt 是新增數據來源。
A7:假設原組表已存在,列出原組表的文件名,依次獲取組表遊標,返回成序列。
A8:創建新的組表文件,用來存放舊組表數據和新增數據,在原有文件名後加上 "_temp",以示區別。
A9:對新增數據使用管道,將管道中的 N 份遊標記錄與對應的 N 箇舊份組表中游標記錄進行歸併,追加到新 N 份組表中。上文已有詳細的解釋。
當這段代碼執行完後,還須要進行舊組表、舊索引的清理以及對新組表的索引創建等操做,以下:
A |
|
1 |
=directory@p("*T.ctx_temp") |
2 |
=A1.(left(~,len(~)-5)) |
3 |
=A2.(movefile(file(~))) |
4 |
=A1.(left(~,len(~)-5)+"__nid_idx") |
5 |
=A2.(movefile(file(~))) |
6 |
=A1.(movefile(file(~),left(~,len(~)-5))) |
7 |
=A2.(file(~).create().index(nid_idx;nid;data)) |
代碼中幾乎全是循環函數與簡單的文件操做。詳見函數參考《文件》。最後一行創建索引,前文中也已屢次解釋。
隨着新數據不斷增長,每次新追加數據與全量歷史數據歸併的時間成本將會愈來愈高。這時須要把每份組表文件分爲新、舊兩份,新的一份是最近一段時間內積累的追加數據,舊的是以前的歷史數據。每當有新數據須要追加時,仍是按 2.4.2 的處理思路操做,但只對新的那份組表文件進行處理。當新份數據文件超過必定大小閾值(如 100G),再和舊數據合併。這樣的作法不只能夠減小歸併的時間成本,另外一方面也能夠下降對磁盤的損耗。
列舉的數據結構仍是以 (nid,data) 爲例,此次咱們從頭開始完整地看一遍代碼:
首先定義新、舊組表文件,命名規則以下:
新份組表:鍵值名 _ 鍵值取 N 的餘數 _T.ctx;舊份組表:鍵值名 _ 鍵值取 N 的餘數 _H.ctx。
一、 創建新、舊組表,本例中 N=4,表明創建 4 份組表:
A |
|
1 |
=N.(file("nid_"+string(~-1)+"_H.ctx").create(#nid,data)) |
2 |
=N.(file("nid_"+string(~-1)+"_T.ctx").create(#nid,data)) |
N 取 4,生成的歷史和臨時組表文件以下:
二、 在新組表上追加新數據。
A |
|
1 |
=["type_a",……,"type_z","type_1",……,"type_9","type_0"] |
2 |
=A1.new(#:tid,~:type) |
3 |
=file("multi_source_.txt") |
4 |
=A3.cursor@t() |
5 |
=A4.switch(type,A2:type) |
6 |
=A4.new(1000000000000+type.tid*long(1000000000)+id:nid,data) |
7 |
=directory@p("nid*T.ctx").(file(~).create().cursor()) |
8 |
=directory@p("nid*T.ctx").(file(~+"_temp").create(#nid,data)) |
9 |
=N.(eval("channel(A4).select(nid%N=="+string(~-1)+").attach(A8("+string(~)+").append([~.cursor(),A7("+string(~)+")].mergex(nid)))")) |
10 |
for A4,500000 |
三、 新組表合併後,清理原來的新組表和索引,而後重建新組表索引。
A |
|
1 |
=directory@p("*T.ctx_temp") |
2 |
=A1.(left(~,len(~)-5)) |
3 |
=A2.(movefile(file(~))) |
4 |
=A1.(left(~,len(~)-5)+"__nid_idx") |
5 |
=A2.(movefile(file(~))) |
6 |
=A1.(movefile(file(~),left(~,len(~)-5))) |
7 |
=A2.(file(~).create().index(nid_idx;nid;data)) |
四、 對新數據大小進行判斷,若是超過參數 B(單位是字節數)則與舊份組表數據合併。
A |
B |
C |
|
1 |
fork directory@p("nid*T.ctx") |
=file(A1) |
|
2 |
if B1.size()>B |
=left(A1,(len(A1)-5))+"H.ctx" |
|
3 |
=B1.create().cursor() |
||
4 |
=file(C2).create().cursor() |
||
5 |
=left(A1,(len(A1)-5))+"H.ctx_temp" |
||
6 |
=file(C5).create(#nid,data).append([C3,C4].mergex(nid)) |
五、 舊組表與新組表合併後,清理原來的舊組表和索引,而後重建舊組表索引。清理已合併的新組表,並重建空的新組表。
A |
|
1 |
=directory@p("*H.ctx_temp") |
2 |
=A1.(left(~,len(~)-5)) |
3 |
=A2.(movefile(file(~))) |
4 |
=A1.(left(~,len(~)-5)+"__nid_idx") |
5 |
=A4.(movefile(file(~))) |
6 |
=A1.(movefile(file(~),left(~,len(~)-5))) |
7 |
=A2.(file(~).create().index(nid_idx;nid;data)) |
8 |
=A1.(left(~,len(~)-10)+"T.ctx") |
9 |
=A8.(movefile(file(~))) |
10 |
=A1.(left(~,len(~)-10)+"T.ctx__nid_idx") |
11 |
=A10.(movefile(file(~))) |
12 |
=A8.(file(~).create(#nid,data)) |
六、 對新、舊組表文件分別利用多線程進行查詢
A |
B |
|
1 |
=file("keys.txt").import@i() |
|
2 |
=A1.group(~%N) |
|
3 |
fork directory@p("*H.ctx"),directory@p("*T.ctx"),A2 |
=A3(3) |
4 |
=file(A3(1)).create().icursor(;B3.contain(nid),nid_idx) |
|
5 |
=file(A3(2)).create().icursor(;B3.contain(nid),nid_idx) |
|
6 |
return B4|B5 |
|
7 |
=A3.conj() |
|
8 |
=file("result.txt").export@t(A8) |
這裏須要注意 A7 中寫法,由於 B6 中返回 B4|B5,因此致使 A3 的結果爲多個遊標序列的序列,所以在對 A3 進行縱向鏈接時,應該使用序列的 conj,而不是遊標的 conjx。
至此,基於本文的 6 個集算器腳本文件,在第三方定時任務調度工具的合理調用下,能夠實現單機狀況下大數據量數據的追加,以及針對批量隨機鍵值的查詢工做。
詳情您能夠閱讀原文