時序數據庫丨DolphinDB內存表詳解

內存表是DolphinDB數據庫的重要組成部分。內存表不只能夠直接用於存儲數據,實現高速數據讀寫,並且能夠緩存計算引擎的中間結果,加速計算過程。本教程主要介紹DolphinDB內存表的分類、使用場景以及各類內存表在數據操做以及表結構(schema)操做上的異同。redis


1. 內存表類別

根據不一樣的使用場景以及功能特色,DolphinDB內存表能夠分爲如下四種:數據庫

  • 常規內存表
  • 鍵值內存表
  • 流數據表
  • MVCC內存表


1.1 常規內存表數組

常規內存表是DolphinDB中最基礎的表結構,支持增刪改查等操做。SQL查詢返回的結果一般存儲在常規內存表中,等待進一步處理。緩存

  • 建立

使用table函數可建立常規內存表。table函數有兩種用法:第一種用法是根據指定的schema(字段類型和字段名稱)以及表容量(capacity)和初始行數(size)來生成;第二種用法是經過已有數據(矩陣,表,數組和元組)來生成一個表。安全

使用第一種方法的好處是能夠預先爲表分配內存。當表中的記錄數超過容量時,系統會自動擴充表的容量。擴充時系統首先會分配更大的內存空間(增長20%到100%不等),而後複製舊錶到新的表,最後釋放原來的內存。對於規模較大的表,擴容的成本會比較高。所以,若是咱們能夠事先預計表的行數,建議建立內存表時預先分配一個合理的容量。若是表的初始行數爲0,系統會生成空表。若是初始行數不爲0,系統會生成一個指定行數的表,表中各列的值都爲默認值。例如:服務器

//建立一個空的常規內存表
t=table(100:0,`sym`id`val,[SYMBOL,INT,INT])

//建立一個10行的常規內存表
t=table(100:10,`sym`id`val,[SYMBOL,INT,INT])
select * from t

sym id val
--- -- ---
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0

table函數也容許經過已有的數據來建立一個常規內存表。下例是經過多個數組來建立。數據結構

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=table(sym,id,val)
  • 應用

常規內存表是DolphinDB中應用最頻繁的數據結構之一,僅次於數組。SQL語句的查詢結果,分佈式查詢的中間結果都存儲在常規內存表中。當系統內存不足時,該表並不會自動將數據溢出到磁盤,而是Out Of Memory異常。所以咱們進行各類查詢和計算時,要注意中間結果和最終結果的size。當某些中間結果再也不須要時,請及時釋放。關於常規內存表增刪改查的各類用法,能夠參考另外一份教程內存分區表加載和操做多線程


1.2 鍵值內存表併發

鍵值內存表是DolphinDB中支持主鍵的內存表。經過指定表中的一個或多個字段做爲主鍵,能夠惟一肯定表中的記錄。鍵值內存表支持增刪改查等操做,可是主鍵值不容許更新。鍵值內存表經過哈希表來記錄每個鍵值對應的行號,所以對於基於鍵值的查找和更新具備很是高的效率。mvc

  • 建立

使用keyedTable函數可建立鍵值內存表。該函數與table函數很是相似,惟一不一樣之處是增長了一個參數指明鍵值列的名稱。

//建立空的鍵值內存表,主鍵由sym和id字段組成
t=keyedTable(`sym`id,1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立鍵值內存表,主鍵由sym和id字段組成
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=keyedTable(`sym`id,sym,id,val)
注意:指定容量和初始大小建立鍵值內存表時,初始大小必須爲0。

咱們也能夠經過keyedTable函數將常規內存錶轉換爲鍵值內存表。例如:

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=keyedTable(`sym`id, tmp)
  • 數據插入和更新的特色

往鍵值內存表中添加新紀錄時,系統會自動檢查新記錄的主鍵值。若是新記錄中的主鍵值不存在於表中,那麼往表中添加新的記錄;若是新記錄的主鍵值與已有記錄的主鍵值重複時,會更新表中該主鍵值對應的記錄。請看下面的例子。

首先,往空的鍵值內存表中插入新記錄,新記錄中的主鍵值爲AAPL, IBM和GOOG。

t=keyedTable(`sym,1:0,`sym`datetime`price`qty,[SYMBOL,DATETIME,DOUBLE,DOUBLE]);
insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:00 2018.06.08T12:30:00 2018.06.08T12:30:00,50.3 45.6 58.0,5200 4800 7800);
t;

sym  datetime            price qty 
---- ------------------- ----- ----
APPL 2018.06.08T12:30:00 50.3  5200
IBM  2018.06.08T12:30:00 45.6  4800
GOOG 2018.06.08T12:30:00 58    7800

再次往表中插入一批主鍵值爲AAPL, IBM和GOOG的新記錄。

insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:01 2018.06.08T12:30:01 2018.06.08T12:30:01,65.8 45.2 78.6,5800 8700 4600);
t;

sym  datetime            price qty 
---- ------------------- ----- ----
APPL 2018.06.08T12:30:01 65.8  5800
IBM  2018.06.08T12:30:01 45.2  8700
GOOG 2018.06.08T12:30:01 78.6  4600

能夠看到,表中記錄條數沒有增長,可是主鍵對應的記錄已經更新。

繼續往表中插入一批新記錄,新記錄自己包含了重複的主鍵值MSFT。

能夠看到,表中有且僅有一條主鍵值爲MSFT的記錄。

  • 應用場景

(1)鍵值表對單行的更新和查詢有很是高的效率,是數據緩存的理想選擇。與redis相比,DolphinDB中的鍵值內存表兼容SQL的全部操做,能夠完成根據鍵值更新和查詢之外的更爲複雜的計算。

(2)做爲時間序列聚合引擎的輸出表,實時更新輸出表的結果。具體請參考教程使用DolphinDB計算K線



1.3 流數據表

流數據表顧名思義是爲流數據設計的內存表,是流數據發佈和訂閱的媒介。流數據表具備自然的流表對偶性(Stream Table Duality),發佈一條消息等價於往流數據表中插入一條記錄,訂閱消息等價於將流數據表中新到達的數據推向客戶端應用。對流數據的查詢和計算均可以經過SQL語句來完成。

  • 建立

使用streamTable函數可建立流數據表。streamTable的用法和table函數徹底相同。

//建立空的流數據表
t=streamTable(1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立流數據表
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=streamTable(sym,id,val)

咱們也可使用streamTable函數將常規內存錶轉換爲流數據表。例如:

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=streamTable(tmp)

流數據表也支持建立單個鍵值列,能夠經過函數keyedStreamTable來建立。但與keyed table的設計目的不一樣,keyedstreamtable的目的是爲了在高可用場景(多個發佈端同時寫入)下,避免重複消息。一般key就是消息的ID。

  • 數據操做特色

因爲流數據具備一旦生成就不會發生變化的特色,所以流數據表不支持更新和刪除記錄,只支持查詢和添加記錄。流數據一般具備連續性,而內存是有限的。爲解決這個矛盾,流數據表引入了持久化機制,在內存中保留最新的一部分數據,更舊的數據持久化在磁盤上。當用戶訂閱舊的數據時,直接從磁盤上讀取。啓用持久化,使用函數enableTableShareAndPersistence,具體參考流數據教程

  • 應用場景

共享的流數據表在流計算中發佈數據。訂閱端經過subscribeTable函數來訂閱和消費流數據。


1.4 MVCC內存表

MVCC內存表存儲了多個版本的數據,當多個用戶同時對MVCC內存表進行讀寫操做時,互不阻塞。MVCC內存表的數據隔離採用了快照隔離模型,用戶讀取到的是在他讀以前就已經存在的數據,即便這些數據在讀取的過程當中被修改或刪除了,也對以前正在讀的用戶沒有影響。這種多版本的方式可以支持用戶對內存表的併發訪問。須要說明的是,當前的MVCC內存表實現比較簡單,更新和刪除數據時鎖定整個表,並使用copy-on-write技術複製一份數據,所以對數據刪除和更新操做的效率不高。在後續的版本中,咱們將實現行級的MVCC內存表。

  • 建立

使用mvccTable函數建立MVCC內存表。例如:

//建立空的流數據表
t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立流數據表
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=mvccTable(sym,id,val)

咱們能夠將MVCC內存表的數據持久化到磁盤,只需建立時指定持久化的目錄和表名便可。例如,

t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT],"/home/user1/DolphinDB/mvcc","test")

系統重啓後,咱們能夠經過loadMvccTable函數將磁盤中的數據加載到內存中。

loadMvccTable("/home/user1/DolphinDB/mvcc","test")

咱們也可使用mvccTable函數將常規內存錶轉換爲MVCC內存表。

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=mvccTable(tmp)
  • 應用場景

當前的MVCC內存表適用於讀多寫少,並有持久化須要的場景。譬如動態的配置系統,須要持久化配置項,配置項的改動不頻繁,已新增和查詢操做爲主,很是適合MVCC表。


2. 共享內存表

DolphinDB中的內存表默認只在建立內存表的會話中使用,不支持多用戶多會話的併發操做,固然對別的會話也不可見。若是但願建立的內存表能被別的用戶使用,保證多用戶併發操做的安全,必須共享內存表。4種類型的內存表都可共享。在DolphinDB中,咱們使用share命令將內存表共享。

t=table(1..10 as id,rand(100,10) as val)
share t as st
//或者share(t,`st)

上面的代碼將表t共享爲表st。

使用undef函數能夠刪除共享表。

undef(`st,SHARED)

2.1 保證對全部會話可見

內存表僅在當前會話可見,在其餘會話中不可見。共享以後,其餘會話能夠經過訪問共享變量來訪問內存表。例如,咱們在當前會話中把表t共享爲表st。

t=table(1..10 as id,rand(100,10) as val)
share t as st

咱們能夠在其餘會話中訪問變量st。例如,往共享表st插入一條數據。

insert into st values(11,200)
select * from st

id val
-- ---
1  1  
2  53 
3  13 
4  40 
5  61 
6  92 
7  36 
8  33 
9  46 
10 26 
11 200

切換到原來的會話,咱們能夠發現,表t中也增長了一條記錄。

select * from t

id val
-- ---
1  1  
2  53 
3  13 
4  40 
5  61 
6  92 
7  36 
8  33 
9  46 
10 26 
11 200


2.2 保證線程安全

在多線程的狀況下,內存表中的數據很容易被破壞。共享則提供了一種保護機制,可以保證數據安全,但同時也會影響系統的性能。

常規內存表、流數據表和MVCC內存表都支持多版本模型,容許多讀一寫。具體說,讀寫互不阻塞,寫的時候能夠讀,讀的時候能夠寫。讀數據時不上鎖,容許多個線程同時讀取數據,讀數據時採用快照隔離(snapshot isolation)。寫數據時必須加鎖,同時只容許一個線程修改內存表。寫操做包括添加,刪除或更新。添加記錄一概在內存表的末尾追加,不管內存使用仍是CPU使用均很是高效。常規內存表和MVCC內存表支持更新和刪除,且採用了copy-on-write技術,也就是先複製一份數據(構成一個新的版本),而後在新版本上進行刪除和修改。因而可知刪除和更新操做不管內存和CPU消耗都比較高。當刪除和更新操做很頻繁,讀操做又比較耗時(不能快速釋放舊的版本),容易致使OOM異常。

鍵值內存表寫入時需維護內部索引,讀取時也須要根據索引獲取數據。所以鍵值內存表共享採用了不一樣的方法,不管讀寫都必須加鎖。寫線程和讀線程,多個寫線程之間,多個讀線程之間都是互斥的。對鍵值內存表儘可能避免耗時的查詢或計算,不然會使其它線程長時間處於等待狀態。


3. 分區內存表

當內存表數據量較大時,咱們能夠對內存表進行分區。分區後一個大表有多個子表(tablet)構成,大表不使用全局鎖,鎖由每一個子表獨立管理,這樣能夠大大增長讀寫併發能力。DolphinDB支持對內存表進行值分區、範圍分區、哈希分區和列表分區,不支持組合分區。在DolphinDB中,咱們使用函數createPartitionedTable建立內存分區表。

  • 建立分區常規內存表
t=table(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,0 101 201 301) 
pt=db.createPartitionedTable(t,`pt,`id)
  • 建立分區鍵值內存表
kt=keyedTable(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,0 101 201 301) 
pkt=db.createPartitionedTable(t,`pkt,`id)
  • 建立分區流數據表

建立分區流數據表時,須要傳入多個流數據表做爲模板,每一個流數據表對應一個分區。寫入數據時,直接往這些流表中寫入;而查詢數據時,須要查詢分區表。

st1=streamTable(1:0,`id`val,[INT,INT]) 
st2=streamTable(1:0,`id`val,[INT,INT]) 
st3=streamTable(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,1 101 201 301) pst=db.createPartitionedTable([st1,st2,st3],`pst,`id)  
st1.append!(table(1..100 as id,rand(100,100) as val)) 
st2.append!(table(101..200 as id,rand(100,100) as val)) 
st3.append!(table(201..300 as id,rand(100,100) as val))  
select * from pst
  • 建立分區MVCC內存表

與建立分區流數據表同樣,建立分區MVCC內存表,須要傳入多個MVCC內存表做爲模板。每一個表對應一個分區。寫入數據時,直接往這些表中寫入;而查詢數據時,須要查詢分區表。

mt1=mvccTable(1:0,`id`val,[INT,INT])
mt2=mvccTable(1:0,`id`val,[INT,INT])
mt3=mvccTable(1:0,`id`val,[INT,INT])
db=database("",RANGE,1 101 201 301)
pmt=db.createPartitionedTable([mt1,mt2,mt3],`pst,`id)

mt1.append!(table(1..100 as id,rand(100,100) as val))
mt2.append!(table(101..200 as id,rand(100,100) as val))
mt3.append!(table(201..300 as id,rand(100,100) as val))

select * from pmt

因爲分區內存表不使用全局鎖,建立之後不能再動態增刪子表。


3.1 增長查詢的併發性

分區表增長查詢的併發性有三層含義:(1)鍵值表在查詢時也須要加鎖,分區表由子表獨立管理鎖,至關於把鎖的粒度變細了,所以能夠增長讀的併發性;(2)批量計算時分區表能夠並行處理每一個子表;(3)若是SQL查詢的過濾指定了分區字段,那麼能夠縮小分區範圍,避免全表掃描。

以鍵值內存表爲例,咱們對比在分區和不分區的狀況下,併發查詢的性能。首先,建立模擬數據集,一共包含500萬行數據。

n=5000000
id=shuffle(1..n)
qty=rand(1000,n)
price=rand(1000.0,n)
kt=keyedTable(`id,id,qty,price)
share kt as skt

id_range=cutPoints(1..n,20)
db=database("",RANGE,id_range)
pkt=db.createPartitionedTable(kt,`pkt,`id).append!(kt)
share pkt as spkt

咱們在另一臺服務器上模擬10個客戶端同時查詢鍵值內存表。每一個客戶端查詢10萬次,每次查詢一條數據,統計每一個客戶端查詢10萬次的總耗時。

def queryKeyedTable(tableName,id){
	for(i in id){
		select * from objByName(tableName) where id=i
	}
}
conn=xdb("192.168.1.135",18102,"admin","123456")
n=5000000

jobid1=array(STRING,0)
for(i in 1..10){
	rid=rand(1..n,100000)
	s=conn(submitJob,"evalQueryUnPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`skt,rid})
	jobid1.append!(s)
}
time1=array(DOUBLE,0)
for(j in jobid1){
	time1.append!(conn(getJobReturn,j,true))
}

jobid2=array(STRING,0)
for(i in 1..10){
	rid=rand(1..n,100000)
	s=conn(submitJob,"evalQueryPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`spkt,rid})
	jobid2.append!(s)
}
time2=array(DOUBLE,0)
for(j in jobid2){
	time2.append!(conn(getJobReturn,j,true))
}

time1是10個客戶端查詢未分區鍵值內存表的耗時,time2是10個客戶端查詢分區鍵值內存表的耗時,單位是毫秒。

time1
[6719.266848,7160.349678,7271.465094,7346.452625,7371.821485,7363.87979,7357.024299,7332.747157,7298.920972,7255.876976]

time2
[2382.154581,2456.586709,2560.380315,2577.602019,2599.724927,2611.944367,2590.131679,2587.706832,2564.305815,2498.027042]

能夠看到,每一個客戶端查詢分區鍵值內存表的耗時要低於查詢未分區內存表的耗時。

查詢未分區的內存表,能夠保證快照隔離。但查詢一個分區內存表,再也不保證快照隔離。如前面所說分區內存表的讀寫不使用全局鎖,一個線程在查詢時,可能另外一個線程正在寫入並且涉及多個子表,從而可能讀到一部分寫入的數據。


3.2 增長寫入的併發性

以分區的常規內存表爲例,咱們能夠同時往不一樣的分區寫入數據。

t=table(1:0,`id`val,[INT,INT])
db=database("",RANGE,1 101 201 301)
pt=db.createPartitionedTable(t,`pt,`id)

def writeData(mutable t,id,batchSize,n){
	for(i in 1..n){
		idv=take(id,batchSize)
		valv=rand(100,batchSize)
		tmp=table(idv,valv)
		t.append!(tmp)
	}
}

job1=submitJob("write1","",writeData,pt,1..100,1000,1000)
job2=submitJob("write2","",writeData,pt,101..200,1000,1000)
job3=submitJob("write3","",writeData,pt,201..300,1000,1000)

上面的代碼中,同時有3個線程對pt的3個不一樣的分區進行寫入。須要注意的是,咱們要避免同時對相同分區進行寫入。例如,下面的代碼可能會致使系統崩潰。

job1=submitJob("write1","",writeData,pt,1..300,1000,1000)
job2=submitJob("write2","",writeData,pt,1..300,1000,1000)

上面的代碼定義了兩個寫入線程,而且寫入的分區相同,這樣會破壞內存。爲了保證每一個分區數據的安全性和一致性,咱們可將分區內存表共享。這樣便可定義多個線程同時對相同分區分入。

share pt as spt
job1=submitJob("write1","",writeData,spt,1..300,1000,1000)
job2=submitJob("write2","",writeData,spt,1..300,1000,1000)


4. 數據操做比較


4.1 增刪改查

下表總結了4種類型內存表在共享/分區的狀況下支持的增刪改查操做。

0048bc37a62dc7736766e58d4282ad9e.png

說明:

  • 常規內存表、鍵值內存表、MVCC內存表都支持增刪改查操做,流數據表僅支持增長數據和查詢,不支持刪除和更新操做。
  • 對於鍵值內存表,若是查詢的過濾條件中包含主鍵,查詢的性能會獲得明顯提高。
  • 對於分區內存表,若是查詢的過濾條件中包含分區列,系統可以縮小要掃描的分區範圍,從而提高查詢的性能。


4.2 併發性

在沒有寫入的狀況下,全部內存表都容許多個線程同時查詢。在有寫入的狀況下,4種內存表的併發性有所差別。下表總結了4種內存表在共享/分區的狀況下支持的併發讀寫狀況。

bdec193a65aece62850130cd4d51c7c1.png

說明:

  • 共享表容許併發讀寫。
  • 對於沒有共享的分區表,不容許多線程對相同分區同時寫入的。


4.3 持久化

  • 常規內存表和鍵值內存表不支持數據持久化。一旦節點重啓,內存中的數據將所有丟失。
  • 只有空的流數據表才支持數據持久化。要對流數據表進行持久化,首先要配置流數據持久化的目錄persistenceDir,再使用enableTableShareAndPersistence使用將流數據表共享,並持久化到磁盤上。例如,將流數據表t共享並持久化到磁盤上。
t=streamTable(1:0,`id`val,[INT,INT])
enableTableShareAndPersistence(t,`st)

流數據表啓用了持久化後,內存中仍然會保留流數據表中部分最新的記錄。默認狀況下,內存會保留最新的10萬條記錄。咱們也能夠根據須要調整這個值。

流數據表持久化能夠設定採用異步/同步、壓縮/不壓縮的方式。一般狀況下,異步模式可以實現更高的吞吐量。

系統重啓後,再次執行enableTableShareAndPersistence函數,會將磁盤中的全部數據加載到內存。

  • MVCC內存表支持持久化。在建立MVCC內存表時,咱們能夠指定持久化的路徑。例如,建立持久化的MVCC內存表。
t=mvccTable(1:0,`id`val,[INT,INT],"/home/user/DolphinDB/mvccTable")
t.append!(table(1..10 as id,rand(100,10) as val))

系統重啓後,咱們可使用loadMvccTable函數將磁盤中的數據加載到內存中。例如:

t=loadMvccTable("/home/user/DolphinDB/mvccTable","t")

5. 表結構操做比較

內存表的結構操做包括新增列、刪除列、修改列(內容和數據類型)以及調整列的順序。下表總結了4種類型內存表在共享/分區的狀況下支持的結構操做。

affb345b39d17824f09467523e757971.jpeg

說明:

  • 分區表以及MVCC內存表不能經過addColumn函數新增列。
  • 分區表能夠經過update語句來新增列,可是流數據表不容許修改,所以流數據表不能經過update語句來新增列。


6. 小結

DolphinDB支持4種類型內存表,還引入了共享和分區的概念,基本可以知足內存計算和流計算的各類需求。

相關文章
相關標籤/搜索