07-subscribe.mdx 12.7 KB
Newer Older
B
Bo Ding 已提交
1
---
2
sidebar_label: 数据订阅
D
dingbo 已提交
3
description: "轻量级的数据订阅与推送服务。连续写入到 TDengine 中的时序数据能够被自动推送到订阅客户端。"
D
dingbo 已提交
4
title: 数据订阅
B
Bo Ding 已提交
5 6
---

7 8
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
B
Bo Ding 已提交
9 10 11 12 13 14 15
import Java from "./_sub_java.mdx";
import Python from "./_sub_python.mdx";
import Go from "./_sub_go.mdx";
import Rust from "./_sub_rust.mdx";
import Node from "./_sub_node.mdx";
import CSharp from "./_sub_cs.mdx";
import CDemo from "./_sub_c.mdx";
B
Bo Ding 已提交
16

17
基于数据天然的时间序列特性,TDengine 的数据写入(insert)与消息系统的数据发布(pub)逻辑上一致,均可视为系统中插入一条带时间戳的新记录。同时,TDengine 在内部严格按照数据时间序列单调递增的方式保存数据。本质上来说,TDengine 中每一张表均可视为一个标准的消息队列。
B
Bo Ding 已提交
18 19 20

TDengine 内嵌支持轻量级的消息订阅与推送服务。使用系统提供的 API,用户可使用普通查询语句订阅数据库中的一张或多张表。订阅的逻辑和操作状态的维护均是由客户端完成,客户端定时轮询服务器是否有新的记录到达,有新的记录到达就会将结果反馈到客户。

21
TDengine 的订阅与推送服务的状态是由客户端维持,TDengine 服务端并不维持。因此如果应用重启,从哪个时间点开始获取最新数据,由应用决定。
B
Bo Ding 已提交
22 23 24 25 26 27 28 29 30

TDengine 的 API 中,与订阅相关的主要有以下三个:

```c
taos_subscribe
taos_consume
taos_unsubscribe
```

31
这些 API 的文档请见 [C/C++ Connector](/reference/connector/cpp),下面仍以智能电表场景为例介绍一下它们的具体用法(超级表和子表结构请参考上一节“连续查询”),完整的示例代码可以在 [这里](https://github.com/taosdata/TDengine/blob/master/examples/c/subscribe.c) 找到。
B
Bo Ding 已提交
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

如果我们希望当某个电表的电流超过一定限制(比如 10A)后能得到通知并进行一些处理, 有两种方法:一是分别对每张子表进行查询,每次查询后记录最后一条数据的时间戳,后续只查询这个时间戳之后的数据:

```sql
select * from D1001 where ts > {last_timestamp1} and current > 10;
select * from D1002 where ts > {last_timestamp2} and current > 10;
...
```

这确实可行,但随着电表数量的增加,查询数量也会增加,客户端和服务端的性能都会受到影响,当电表数增长到一定的程度,系统就无法承受了。

另一种方法是对超级表进行查询。这样,无论有多少电表,都只需一次查询:

```sql
select * from meters where ts > {last_timestamp} and current > 10;
```

但是,如何选择 `last_timestamp` 就成了一个新的问题。因为,一方面数据的产生时间(也就是数据时间戳)和数据入库的时间一般并不相同,有时偏差还很大;另一方面,不同电表的数据到达 TDengine 的时间也会有差异。所以,如果我们在查询中使用最慢的那台电表的数据的时间戳作为 `last_timestamp`,就可能重复读入其它电表的数据;如果使用最快的电表的时间戳,其它电表的数据就可能被漏掉。

TDengine 的订阅功能为上面这个问题提供了一个彻底的解决方案。

首先是使用 `taos_subscribe` 创建订阅:

```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`去处理。(注意,`subscribe_callback` 中不宜做较为耗时的操作,否则有可能导致客户端阻塞等不可控的问题。)

参数 `taos` 是一个已经建立好的数据库连接,在同步模式下无特殊要求。但在异步模式下,需要注意它不会被其它线程使用,否则可能导致不可预计的错误,因为回调函数在 API 的内部线程中被调用,而 TDengine 的部分 API 不是线程安全的。

参数 `sql` 是查询语句,可以在其中使用 where 子句指定过滤条件。在我们的例子中,如果只想订阅电流超过 10A 时的数据,可以这样写:

```sql
select * from meters where current > 10;
```

注意,这里没有指定起始时间,所以会读到所有时间的数据。如果只想从一天前的数据开始订阅,而不需要更早的历史数据,可以再加上一个时间条件:

```sql
select * from meters where ts > now - 1d and current > 10;
```

订阅的 `topic` 实际上是它的名字,因为订阅功能是在客户端 API 中实现的,所以没必要保证它全局唯一,但需要它在一台客户端机器上唯一。

如果名为 `topic` 的订阅不存在,参数 `restart` 没有意义;但如果用户程序创建这个订阅后退出,当它再次启动并重新使用这个 `topic` 时,`restart` 就会被用于决定是从头开始读取数据,还是接续上次的位置进行读取。本例中,如果 `restart` 是 **true**(非零值),用户程序肯定会读到所有数据。但如果这个订阅之前就存在了,并且已经读取了一部分数据,且 `restart` 是 **false**(**0**),用户程序就不会读到之前已经读取的数据了。

`taos_subscribe`的最后一个参数是以毫秒为单位的轮询周期。在同步模式下,如果前后两次调用 `taos_consume` 的时间间隔小于此时间,`taos_consume` 会阻塞,直到间隔超过此时间。异步模式下,这个时间是两次调用回调函数的最小时间间隔。

`taos_subscribe` 的倒数第二个参数用于用户程序向回调函数传递附加参数,订阅 API 不对其做任何处理,只原样传递给回调函数。此参数在同步模式下无意义。

订阅创建以后,就可以消费其数据了,同步模式下,示例代码是下面的 else 部分:

```c
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`:

```c
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` 用于处理订阅到数据,在我们的例子中,它会打印出所有符合条件的记录。而异步模式下,消费订阅到的数据则显得更为简单:

```c
void subscribe_callback(TAOS_SUB* tsub, TAOS_RES *res, void* param, int code) {
  print_result(res, *(int*)param);
}
```

当要结束一次数据订阅时,需要调用 `taos_unsubscribe`:

```c
taos_unsubscribe(tsub, keep);
```

Y
Yu Chen 已提交
148
其第二个参数,用于决定是否在客户端保留订阅的进度信息。如果这个参数是**false**(**0**),那无论下次调用 `taos_subscribe` 时的 `restart` 参数是什么,订阅都只能重新开始。另外,进度信息的保存位置是 _{DataDir}/subscribe/_ 这个目录下(注:`taos.cfg` 配置文件中 `DataDir` 参数值默认为 **/var/lib/taos/**,但是 Windows 服务器上本身不存在该目录,所以需要在 Windows 的配置文件中修改 `DataDir` 参数值为相应的已存在目录"),每个订阅有一个与其 `topic` 同名的文件,删掉某个文件,同样会导致下次创建其对应的订阅时只能重新开始。
B
Bo Ding 已提交
149 150 151 152 153 154 155 156 157 158 159 160 161 162

代码介绍完毕,我们来看一下实际的运行效果。假设:

- 示例代码已经下载到本地
- TDengine 也已经在同一台机器上安装好
- 示例所需的数据库、超级表、子表已经全部创建好

则可以在示例代码所在目录执行以下命令来编译并启动示例程序:

```bash
make
./subscribe -sql='select * from meters where current > 10;'
```

D
dingbo 已提交
163
示例程序启动后,打开另一个终端窗口,启动 TDengine CLI 向 **D1001** 插入一条电流为 12A 的数据:
B
Bo Ding 已提交
164 165 166 167 168 169 170 171 172

```sql
$ taos
> use test;
> insert into D1001 values(now, 12, 220, 1);
```

这时,因为电流超过了 10A,您应该可以看到示例程序将它输出到了屏幕上。您可以继续插入一些数据观察示例程序的输出。

B
Bo Ding 已提交
173
## 示例程序
174

D
dingbo 已提交
175
下面的示例程序展示是如何使用连接器订阅所有电流超过 10A 的记录。
B
Bo Ding 已提交
176 177 178

### 准备数据

179
```
W
wade zhang 已提交
180
# create database "power"
B
Bo Ding 已提交
181
taos> create database power;
W
wade zhang 已提交
182
# use "power" as the database in following operations
B
Bo Ding 已提交
183
taos> use power;
W
wade zhang 已提交
184
# create super table "meters"
B
Bo Ding 已提交
185
taos> create table meters(ts timestamp, current float, voltage int, phase int) tags(location binary(64), groupId int);
W
wade zhang 已提交
186
# create tabes using the schema defined by super table "meters"
G
gccgdb1234 已提交
187 188
taos> create table d1001 using meters tags ("California.SanFrancisco", 2);
taos> create table d1002 using meters tags ("California.LosAngeles", 2);
W
wade zhang 已提交
189
# insert some rows
B
Bo Ding 已提交
190 191
taos> insert into d1001 values("2020-08-15 12:00:00.000", 12, 220, 1),("2020-08-15 12:10:00.000", 12.3, 220, 2),("2020-08-15 12:20:00.000", 12.2, 220, 1);
taos> insert into d1002 values("2020-08-15 12:00:00.000", 9.9, 220, 1),("2020-08-15 12:10:00.000", 10.3, 220, 1),("2020-08-15 12:20:00.000", 11.2, 220, 1);
W
wade zhang 已提交
192
# filter out the rows in which current is bigger than 10A
B
Bo Ding 已提交
193 194 195
taos> select * from meters where current > 10;
           ts            |    current   |    voltage   |  phase |         location          |   groupid   |
===========================================================================================================
G
gccgdb1234 已提交
196 197 198 199 200
 2020-08-15 12:10:00.000 |    10.30000  |     220      |      1 |      California.LosAngeles      |           2 |
 2020-08-15 12:20:00.000 |    11.20000  |     220      |      1 |      California.LosAngeles      |           2 |
 2020-08-15 12:00:00.000 |    12.00000  |     220      |      1 |      California.SanFrancisco     |           2 |
 2020-08-15 12:10:00.000 |    12.30000  |     220      |      2 |      California.SanFrancisco     |           2 |
 2020-08-15 12:20:00.000 |    12.20000  |     220      |      1 |      California.SanFrancisco     |           2 |
B
Bo Ding 已提交
201 202
Query OK, 5 row(s) in set (0.004896s)
```
G
gccgdb1234 已提交
203

B
Bo Ding 已提交
204
### 示例代码
B
Bo Ding 已提交
205

B
Bo Ding 已提交
206 207
<Tabs defaultValue="java" groupId="lang">
  <TabItem label="Java" value="java">
G
gccgdb1234 已提交
208
    <Java />
B
Bo Ding 已提交
209
  </TabItem>
D
dingbo 已提交
210
  <TabItem label="Python" value="Python">
G
gccgdb1234 已提交
211
    <Python />
B
Bo Ding 已提交
212
  </TabItem>
D
dingbo 已提交
213
  {/* <TabItem label="Go" value="go">
B
Bo Ding 已提交
214
      <Go/>
D
dingbo 已提交
215
  </TabItem> */}
B
Bo Ding 已提交
216
  <TabItem label="Rust" value="rust">
G
gccgdb1234 已提交
217
    <Rust />
B
Bo Ding 已提交
218
  </TabItem>
D
dingbo 已提交
219
  {/* <TabItem label="Node.js" value="nodejs">
B
Bo Ding 已提交
220 221 222 223
      <Node/>
  </TabItem>
  <TabItem label="C#" value="csharp">
      <CSharp/>
224
  </TabItem> */}
B
Bo Ding 已提交
225
  <TabItem label="C" value="c">
G
gccgdb1234 已提交
226 227
    <CDemo />
  </TabItem>
B
Bo Ding 已提交
228
</Tabs>
B
Bo Ding 已提交
229

B
Bo Ding 已提交
230
### 运行示例程序
G
gccgdb1234 已提交
231

B
Bo Ding 已提交
232
示例程序会先消费符合查询条件的所有历史数据:
B
Bo Ding 已提交
233 234

```bash
G
gccgdb1234 已提交
235 236 237 238 239
ts: 1597464000000	current: 12.0	voltage: 220	phase: 1	location: California.SanFrancisco	groupid : 2
ts: 1597464600000	current: 12.3	voltage: 220	phase: 2	location: California.SanFrancisco	groupid : 2
ts: 1597465200000	current: 12.2	voltage: 220	phase: 1	location: California.SanFrancisco	groupid : 2
ts: 1597464600000	current: 10.3	voltage: 220	phase: 1	location: California.LosAngeles	groupid : 2
ts: 1597465200000	current: 11.2	voltage: 220	phase: 1	location: California.LosAngeles	groupid : 2
B
Bo Ding 已提交
240 241
```

D
dingbo 已提交
242
接着,使用 TDengine CLI 向表中新增一条数据:
B
Bo Ding 已提交
243

244
```
B
Bo Ding 已提交
245 246
# taos
taos> use power;
247
taos> insert into d1001 values(now, 12.4, 220, 1);
B
Bo Ding 已提交
248 249 250 251 252
```

因为这条数据的电流大于 10A,示例程序会将其消费:

```
G
gccgdb1234 已提交
253
ts: 1651146662805	current: 12.4	voltage: 220	phase: 1	location: California.SanFrancisco	groupid: 2
B
Bo Ding 已提交
254
```