TDengine中訂閱的用途和用法 - TDengine | 濤思數據 (taosdata.com)
默認創建的數據庫在create topic語句時會出錯,表示:WAL_RETENTION_PERIOD爲0
文檔中說創建數據庫時WAL_RETENTION_PERIOD默認爲3600秒,和實際不符合,因爲topic需要WAL_RETENTION_PERIOD支持,因此修改數據庫即可:
alter database ttt WAL_RETENTION_PERIOD 3600
本文將介紹TDengine Database訂閱功能的使用場景、使用方法和一些限制,並與InfluxDB的訂閱功能進行簡單的對比。本文的預期讀者是基於TDengine開發各種應用的軟件開發人員。
什麼是訂閱?
訂閱,是一種數據查詢方式,其特點爲:客戶端執行一個查詢語句後,可以增量形式,不斷收到新到達服務端的、符合查詢條件的數據。訂閱的實現模型有兩種,一種是“推”,即服務器主動將數據發到客戶端;另一種是“拉”,即客戶端主動向服務器請求數據。兩種方式各有優缺點,這裏不做詳細的對比,只是說明一下,TDengine Database使用的是“拉”模型。
什麼時候需要使用訂閱?
爲了便於用戶程序消費TDengine Database中的數據,TDengine實現了基於SQL的數據查詢語法,並提供了豐富的聚合函數,這種方式的優勢已在多個實際案例中得到了體現。但由於時序數據的特點,單純的直接數據查詢並不能滿足用戶程序的需求,比如:我們管理着一批溫度測量設備,希望當某個設備檢測到的溫度超過限制(比如80°C)後能得到通知並進行一些處理時,肯定會先爲所有的設備建立一張超級表:
create database test;
use test;
create table devices (ts timestamp, temperature float) tags(id int);
併爲每個設備創建一張子表:
create table device1 using devices tags(1);
create table device2 using devices tags(2);
...
這種設計滿足了設備管理的需求,但如何滿足溫度監測的需求呢?如果僅使用普通的查詢,有兩種方法:一是分別對每張子表進行查詢,每次查詢後記錄最後一條數據的時間戳,後續只查詢這個時間戳之後的數據:
select * from device1 where ts > last_timestamp1 and temperature > 80;
select * from device2 where ts > last_timestamp2 and temperature > 80;
...
這確實可行,但隨着設備數量的增加,查詢數量也會增加,客戶端和服務端的性能都會受到影響,當設備數增長到一定的程度,系統就無法承受了。
另一種方法是對超級表進行查詢。這樣,無論有多少設備,都只需一次查詢:
select * from devices where ts > last_timestamp and temperature > 80;
但是,如何選擇 last_timestamp 就成了一個新的問題。因爲,一方面數據的產生時間(也就是數據時間戳)和數據入庫的時間一般並不相同,有時偏差還很大;另一方面,不同設備的數據到達TDengine的時間也會有差異。所以,如果我們在查詢中使用最慢的那臺設備的數據的時間戳作爲 last_timestamp ,就可能重複讀入其它設備的數據;如果使用最快的設備的時間戳,其它設備的數據就可能被漏掉。
TDengine的訂閱功能爲上面這個問題提供了一個徹底的解決方案。
如何使用TDengine中的訂閱功能?
TDengine的API中,與訂閱相關的主要有以下三個:
- taos_subscribe
- taos_consume
- taos_unsubscribe
這三個API的具體說明請見《C/C++數據訂閱接口》,下面結合一個示例,介紹下其使用方法,完整的示例代碼可以在這裏找到。
首先是創建訂閱:
TAOS_SUB* tsub = NULL;
if (async) {
// create an asynchronized subscription, the callback function will be called every 1s
tsub = taos_subscribe(taos, restart, topic, sql, subscribe_callback, &blockFetch, 1000);
} else {
// create an synchronized subscription, need to call 'taos_consume' manually
tsub = taos_subscribe(taos, restart, topic, sql, NULL, NULL, 0);
}
TDengine中的訂閱既可以是同步的,也可以是異步的,上面的代碼會根據從命令行獲取的參數async的值來決定使用哪種方式。這裏,同步的意思是用戶程序要直接調用 taos_consume來拉取數據,而異步則由API在內部的另一個線程中調用taos_consume,然後把拉取到的數據交給回調函數 subscribe_callback去處理。
參數taos是一個已經建立好的數據庫連接,在同步模式下無特殊要求。但在異步模式下,需要注意它不會被其它線程使用,否則可能導致不可預計的錯誤,因爲回調函數在API的內部線程中被調用,而TDengine的部分API不是線程安全的。
參數sql是查詢語句,可以在其中使用where子句指定過濾條件。回到開頭的例子,如果我們只想訂閱設備溫度超過 80°C 時的數據,可以這樣寫:
select * from devices where temperature > 80;
注意,這裏沒有指定起始時間,所以會讀到所有時間的數據。如果只想從一天前的數據開始訂閱,而不需要更早的歷史數據,可以再加上一個時間條件:
select * from devices where ts > now - 1d and temperature > 80;
訂閱的topic實際上是它的名字,因爲訂閱功能是在客戶端API中實現的,所以沒必要保證它全局唯一,但需要它在一臺客戶端機器上唯一。
如果名topic的訂閱不存在,參數restart沒有意義;但如果用戶程序創建這個訂閱後退出,當它再次啓動並重新使用這個topic時,restart就會被用於決定是從頭開始讀取數據,還是接續上次的位置進行讀取。本例中,如果restart是true(非零值),用戶程序肯定會讀到所有數據。但如果這個訂閱之前就存在了,並且已經讀取了一部分數據,且restart是false(0),用戶程序就不會讀到之前已經讀取的數據了。
taos_subscribe的最後一個參數是以毫秒爲單位的輪詢週期。在同步模式下,如過前後兩次調用taos_consume的時間間隔小於此時間,taos_consume會阻塞,直到間隔超過此時間。異步模式下,這個時間是兩次調用回調函數的最小時間間隔。
taos_subscribe的倒數第二個參數用於用戶程序向回調函數傳遞附加參數,訂閱API不對其做任何處理,只原樣傳遞給回調函數。此參數在同步模式下無意義。
訂閱創建以後,就可以消費其數據了,同步模式下,示例代碼是下面的 else 部分:
if (async) {
getchar();
} else while(1) {
TAOS_RES* res = taos_consume(tsub);
if (res == NULL) {
printf("failed to consume data.");
break;
} else {
print_result(res, blockFetch);
getchar();
}
}
這裏是一個while循環,用戶每按一次回車鍵就調用一次taos_consume,而taos_consume的返回值是查詢到的結果集,與taos_use_result完全相同,例子中使用這個結果集的代碼是函數print_result:
void print_result(TAOS_RES* res, int blockFetch) {
TAOS_ROW row = NULL;
int num_fields = taos_num_fields(res);
TAOS_FIELD* fields = taos_fetch_fields(res);
int nRows = 0;
if (blockFetch) {
nRows = taos_fetch_block(res, &row);
for (int i = 0; i < nRows; i++) {
char temp[256];
taos_print_row(temp, row + i, fields, num_fields);
puts(temp);
}
} else {
while ((row = taos_fetch_row(res))) {
char temp[256];
taos_print_row(temp, row, fields, num_fields);puts(temp);
nRows++;
}
}
printf("%d rows consumed.\n", nRows);
}
其中的 taos_print_row 用於處理訂閱到數據,在我們的例子中,它會打印出所有符合條件的記錄。而異步模式下,消費訂閱到的數據則顯得更爲簡單:
void subscribe_callback(TAOS_SUB* tsub, TAOS_RES *res, void* param, int code) {
print_result(res, *(int*)param);
}
當要結束一次數據訂閱時,需要調用taos_unsubscribe:
taos_unsubscribe(tsub, keep);
其第二個參數,用於決定是否在客戶端保留訂閱的進度信息,如果大家還記得前面說過“訂閱功能是在客戶端API中實現的”的話,應該可以猜到,如果這個參數是false(0),那無論下次調用taos_subscribe的時的restart參數是什麼,訂閱都只能重新開始了。另外,進度信息的保存位置是{DataDir}/subscribe/,這個目錄下,每個訂閱有一個與其topic同名的文件,刪掉某個文件,同樣會導致下次創建其對應的訂閱時只能重新開始。
代碼介紹完畢,我們來看一下實際的運行效果。假設:
- 示例代碼已經下載到本地
- TDengine 也已經在同一臺機器上安裝好
- 已經按照本文開頭的腳本創建數據庫、超級表和一些子表
則可以在示例代碼所在目錄執行以下命令來編譯並啓動示例程序:
$ make
$ ./subscribe -sql='select * from devices where temperature > 80;'
示例程序啓動後,打開另一個終端窗口,啓動 TDengine 的 shell 向 device1 插入一條溫度爲 90 °C 的數據:
$ taos
> use test;
> insert into device1 values(0, 90);
這時,因爲溫度超過了 80 °C ,您應該可以看到示例程序將它輸出到了屏幕上。您可以繼續插入一些數據觀察示例程序的輸出。
用作消息隊列
本文開頭的例子,是用訂閱實現了一個報警監控的功能,但其實訂閱也可以用在其它場景中,比如:消息隊列。
應用程序可以訂閱數據庫某些表的內容,同一個表也可以被多個應用訂閱,一旦表有新的記錄,應用將立即得到通知。這樣,再把數據插入看做Publish操作,用戶完全可以把TDengine作爲一個消息隊列中間件來使用。
所以,當下次面對需要使用Kafka的場景時,不妨先考慮下TDengine,因爲TDengine除了安裝包超小、運維超簡單的優點外,還有一個Kafka不具備的功能——數據過濾:可以在查詢語句中指定過濾條件,保證讀到的數據都是有用的,不用再在代碼中手寫過濾邏輯了。
與InfluxDB的對比
概念上說,InfluxDB的訂閱和TDengine的訂閱區別很大,我們可以認爲訂閱在InfluxDB中更像一種數據同步機制,而TDengine中的訂閱則是一種數據查詢機制:
- InfluxDB將收到的數據實時推送給其它節點,TDengine通過輪詢的方式拉取數據,InfluxDB具有更好的實時性。
- InfluxDB中只能訂閱全部數據,TDengine中可以指定數據過濾條件。
- InfluxDB中只能訂閱當前時間之後的數據,TDengine中可以在訂閱中讀到歷史數據。
所以,兩相對比,InfluxDB的優勢是實時性,而TDengine則以稍微犧牲實時性爲代價提供了更強大的功能。
限制條件
下面是一些TDengine訂閱功能的侷限,大家需要在使用中注意。
- 訂閱的查詢語句只能是
select
語句,只能查詢原始數據(不支持聚合函數),只能按時間正序查詢數據。 - 在滿足應用需求的情況下,請儘量將輪詢週期設置的大一些,否則會對系統性能造成影響。
- 暫不支持亂序數據,用戶程序可能讀不到使用import方式插入的數據。
- 如果用戶程序異常退出或沒有正確調用taos_unsubscribe,進度信息可能會有錯誤,這時,後續的同名訂閱可能讀到之前已經讀過的數據。