TDengine C++ 系列(8):流式计算与最新值缓存——库内实时处理

核心目标 :掌握 CREATE STREAM 流式计算与 Read Cache 缓存能力,能用 C++ 配合流计算构建实时统计与最新值服务。

前置知识:Part 4(窗口查询)、Part 7(订阅);理解触发-计算分离思想。

验证环境 :TDengine 3.4.x(3.4.1.6);taosc;C++17;Docker(服务端);数据来自 seed.cpp

系列导航写作计划 · Part 7 数据订阅 → 本篇 → Part 9 集群


0. 本篇问题场景

EMS 的实时统计需求很直接,但实现起来很烦:

  • 负荷统计 :每个变电站过去 1 小时的 avg(power) / max(ia),要按小时自动滚动更新,报表直接读;
  • 状态统计:每台断路器当前"合了多久"、今天"动作了几次";
  • 降采样:原始遥测 90 天过期,但"5 分钟均值"要留 3 年------谁来持续做这个压缩?
  • 最新值 :监控大屏要"毫秒级"显示每台设备最新遥测------每次都全表查 ORDER BY ts DESC 显然不行。

传统方案:定时任务轮询聚合 + Redis 存最新值 + 自己维护调度。组件多、延迟高、容易出 bug。TDengine 把两件事做进了数据库:

  1. 流计算(Stream):用 SQL 定义"每当数据写入就自动计算并写结果表",降采样、统计、状态计算都归它;
  2. Read Cache(最新值缓存)LAST/LAST_ROW 查询直接命中内存缓存,最新值秒回。

本篇的目标:掌握 CREATE STREAM 的窗口/触发/结果语义,配合 Read Cache 搭建"写入 → 流计算 → 查询/订阅"的实时闭环。


1. 心智模型:三个"实时处理"怎么分工

能力 触发 产出 典型用途
订阅 TMQ(Part 7) 每条数据变化 事件推给应用 变位、越限告警
流计算 窗口/事件/周期 结果落库 降采样、统计、状态计算
外部流处理(Flink 等) 事件流 任意 复杂多源计算------TDengine 是轻量替代

关键认知 :流计算的产出是新表里的数据(不是推给应用的事件)。应用侧要么查结果表,要么订阅结果表。所以"写入 → 流计算 → 订阅"可以串成管道。

1.1 新流引擎:触发与计算解耦

3.3.7+ 的新流引擎有两个特点(这是理解全部语法的钥匙):

  • 触发源与计算源可以分离 :触发窗口的"表"和参与计算的"数据集"不必是同一个;甚至可以有周期触发(PERIOD)而没有数据触发;
  • 结果延迟可调 :用 STREAM_OPTIONS(水位线、过期时间、最大延迟等)在"时效"和"资源"之间平衡。

1.2 流计算 vs 连续查询(2.x)

2.x 的 CREATE CONTINUOUS QUERY 在 3.x 中已被 CREATE STREAM 取代(迁移见 Part 12)。本系列只讲 3.x 的流计算。

1.3 Read Cache:最新值为什么能秒回

建库参数 cachemodel 决定 LAST/LAST_ROW 是否走内存缓存:

cachemodel 行为
none 不缓存,最新值查询走存储
last_row 缓存每个子表最后一行(LAST_ROW 命中)
both 缓存最后一行 + 最后一条(LAST/LAST_ROW 都命中)

cachesize(MB)是缓存容量上限。命中时查询不触达磁盘,所以"毫秒级"。代价:缓存与数据最终一致(写入即更新),但容量有限,超大子表数时注意预算(Part 11 展开)。


2. 最小可运行示例:两个流任务

项目 src/examples/part8_stream.cpp 完整可编译运行。核心 SQL:

2.1 变电站小时负荷统计

sql 复制代码
CREATE STREAM IF NOT EXISTS station_hourly INTERVAL(1h)
  FROM ems.telemetry PARTITION BY station
  STREAM_OPTIONS(FILL_HISTORY)
  INTO ems.station_hourly (ts, avg_power, max_ia)
  TAGS (station BINARY(32) AS station)
  AS SELECT _twstart, station, avg(power), max(ia) FROM %%trows;

逐段拆解:

片段 含义
INTERVAL(1h) 时间窗口 1 小时,窗口关闭触发(默认 trigger)
FROM ems.telemetry PARTITION BY station 计算源为遥测超级表,按站点分组,每组独立触发
STREAM_OPTIONS(FILL_HISTORY) 回填创建前已有历史数据(否则新流引擎默认只处理创建后的数据)
INTO ems.station_hourly (ts, avg_power, max_ia) 结果表数据列;TAGS(... AS ...) 声明输出标签 → 结果表是超级表,每组自动建子表
AS SELECT _twstart, station, ... 计算逻辑;_twstart 是窗口起点伪列,%%trows 是该组触发时刻的数据集

注意 :输出标签(station)只出现在 TAGS 中,数据列列表不重复声明;标签值通过 AS station 从 SELECT 列取值。

2.2 开关状态持续时长

sql 复制代码
CREATE STREAM IF NOT EXISTS status_duration STATE_WINDOW(state)
  FROM ems.status PARTITION BY tbname
  STREAM_OPTIONS(FILL_HISTORY)
  INTO ems.status_duration (ts, state, seconds)
  TAGS (tbname BINARY(32) AS tbname)
  AS SELECT _twstart, tbname, state, stateduration(state) / 1000.0 FROM %%trows;

注意:超级表配 STATE_WINDOW 必须 PARTITION BY tbname(按子表分组,每个设备独立的状态机)。

2.3 管理语句

sql 复制代码
SHOW STREAMS;                        -- 查看所有流任务
DROP STREAM IF EXISTS station_hourly;-- 删除流任务(结果表保留)
START STREAM station_hourly;         -- 恢复(STOP 后持久生效)
STOP STREAM station_hourly;          -- 暂停

2.4 C++ 侧:StreamManager

src/ems_lab/stream_setup.h/.cpp 把流任务管理封装成三个方法(create_hourly_load / create_status_duration / show / drop),内部就是 TdConn::exec + TdResult流计算本身是 SQL 能力,C++ 不需要特殊 API------这是它轻量的原因。

cpp 复制代码
StreamManager sm(conn);
sm.create_hourly_load();
sm.show();
// 结果表像普通表一样查询
TdResult res(conn.query("SELECT station, avg_power, max_ia FROM station_hourly "
                        "ORDER BY ts DESC LIMIT 10"));

2.5 Read Cache 查询

cpp 复制代码
// cachemodel='both' 时命中内存缓存
TdResult last(conn.query("SELECT LAST_ROW(*) FROM telemetry WHERE tbname = 'd_0001'"));

3. 机制拆解:触发、结果与一致性

3.1 窗口触发(TRIGGER)

触发类型 语法 触发时机
窗口关闭 INTERVAL(n) [SLIDING(m)](默认) 窗口结束(可配水位线提前/推后)
窗口开启 TRIGGER WINDOW_START 窗口开始
非窗口周期 PERIOD(d) 按固定周期触发
状态/事件/计数窗口 STATE_WINDOW / EVENT_WINDOW / COUNT_WINDOW 状态变化/事件匹配/计数达标

水位线 WATERMARK(d):允许迟到数据在 d 内仍计入当前窗口;超过则算入下一窗口。这是"乱序数据在流里怎么办"的答案------用水位线兜底,而不是无限等待

3.2 结果写入语义(必须背下来)

结果表写入按主键(时间戳)规则:

  • 主键为 NULL 的结果被丢弃
  • 同一输出子表、相同时间戳:后写覆盖前写(幂等重算友好);
  • 不同时间戳:追加

配合 DELETE_RECALC(重算窗口时先删旧结果再写)可以做到"流重算不产生脏数据"。

3.3 Read Cache 一致性

  • 写入即更新缓存(数据可见性与缓存同步);
  • cachemodelcachesize建库/改库参数,改后对后续生效;
  • 缓存存的是"每个子表最后一行/条",大屏实时值用它;历史曲线仍走存储查询。

3.4 资源与失败

  • 每个流任务占用 snode 资源;窗口数、滑窗粒度影响内存(窗口内存随并发窗口数增长);
  • 流任务失败会重试/告警,SHOW STREAMS 看状态;FILL_HISTORY 可回填历史窗口。

4. ems-lab 工程实战:实时统计闭环

复制代码
telemetry/status(写入,Part 6)
  → CREATE STREAM(station_hourly / status_duration,落结果表)
  → 结果表查询(报表 / 大屏)
  → 结果表订阅(Part 7 的 TdSubscriber 订阅结果表 topic → 推送)
  → Read Cache(LAST_ROW 秒回最新值)

演进要点:

  1. 降采样 :原始遥测 keep 90,聚合结果 keep 1095(3 年)------用两个库/两张表 + 流计算实现分级存储;
  2. 统计口径一致 :流计算与 Part 4 的窗口查询用同一套 SQL 语义,结果可互相校验(双路径对比是验收手段);
  3. 缓存兜底 :大屏最新值走 LAST_ROW(缓存),报表历史走聚合表,各取所需。

5. 失败实验与根因

5.1 窗口边界与迟到数据

实验:数据在窗口关闭后才到达(乱序迟到)。

现象:迟到数据可能被算进下一窗口(无水位线时),或在水位线允许范围内补进当前窗口。根因:窗口触发是"时间推进"模型,迟到数据需要 WATERMARK 或重算机制兜底。教训 :延迟敏感统计必须配置水位线;不能接受误差时用 DELETE_RECALC 重算。

5.2 滑窗内存暴涨

实验:INTERVAL(1m) SLIDING(1s) 在大量设备上跑。

现象:snode 内存随活跃窗口数增长,峰值明显。根因:滑动窗口同时保有多个窗口状态。教训:滑窗粒度与设备数决定内存,先算预算再上线(Part 11 有估算思路)。

5.3 结果表重复/覆盖不符预期

实验:流重算或迟到数据导致结果表出现"两条同时间戳不同值"。

现象:查出来只有一条(后写覆盖)。根因:同子表同 ts 覆盖是结果写入语义。教训:这是特性不是 bug;要保留多版本就换时间戳精度/加版本列,不要指望流自动去重。

5.4 缓存与数据不一致

实验:cachemodelnone 改成 both 后立即查询。

现象:部分子表 LAST_ROW 仍走存储(缓存未建立)。根因:缓存按需/渐进建立,超大表首次查询仍较慢。教训 :缓存生效是"逐渐的",容量有限;关键实时表才开 both


6. 版本与环境差异

维度 说明
新流引擎 3.3.7+ 引入(触发-计算解耦);3.3.6 LTS 不含新流引擎,为旧流引擎,Part 8 正文以 3.4.x 的 CREATE STREAM 语法为准
_twstart/_twend 新流引擎窗口伪列(3.4.x);旧版流/查询窗口用 _wstart/_wend,注意区分
STREAM_OPTIONS 水位线/过期/最大延迟等选项随版本演进,以官方 Stream Syntax 页为准
CREATE CONTINUOUS QUERY 2.x 能力,3.x 已由 CREATE STREAM 取代
Read Cache cachemodel/cachesize 3.x 建库参数;与 2.x 行为基本一致

发布前核对 :流语法细节(trigger 名、option 名)以所装版本官方文档为准;升级后重跑 part8_stream 与结果校验。


7. 测试与验收

7.1 自动化断言

  • 造数后创建流任务(FILL_HISTORY 回填 2026-08-01 的历史数据),断言结果表能被查询、行结构与预期一致;
  • 双路径校验:station_hourly 的某窗口 avg_power 与 Part 4 同窗口查询结果一致(误差在浮点容差内);
  • 状态窗口:status_durationsecondsseed_status 的变位时间差一致;
  • 幂等重算:重跑流任务(DELETE_RECALC)后结果表行数不变(同 ts 覆盖)。

7.2 本篇验收清单

  • 能画出"写入 → 流计算 → 结果表 → 查询/订阅"的闭环图;
  • StreamManager 并入 ems-lab,part8_stream 编译运行,两个流任务创建成功并可查询结果;
  • 复现 4 个失败实验(迟到数据、滑窗内存、结果覆盖、缓存渐进生效)并解释根因;
  • 能说清 WATERMARKDELETE_RECALC、结果写入三规则(NULL 丢弃/同 ts 覆盖/追加);
  • 知道 3.3.6 LTS 与 3.4.x 的流引擎差异;
  • 大屏最新值用 LAST_ROW(缓存),并知道 cachemodel/cachesize 如何配置。

8. 常见误区

误区 事实
"流计算和订阅一样,都是推给应用" 流计算的产出是结果表数据,应用要查/订阅结果表
"流里迟到数据会自动归正确窗口" 需要 WATERMARK/重算机制;默认可能进下一窗口
"滑动窗口越细越精确" 滑窗内存随窗口数增长,先算预算
"结果表重复了,帮我修" 同子表同 ts 覆盖是语义;要保留多版本需换主键设计
"开了缓存最新值立刻全秒回" 缓存渐进建立、容量有限;超大表首次查询仍走存储
"流计算是独立组件,要单独部署" 3.x 流引擎内置于 taosd(snode 角色),无独立进程

9. 本篇小结

流计算把"持续聚合"从应用搬进了数据库:

  1. 模型CREATE STREAM ... INTO ... AS SELECT,窗口触发(INTERVAL/STATE/EVENT/COUNT/PERIOD),STREAM_OPTIONS 调时效与资源;
  2. 结果语义:主键 NULL 丢弃、同 ts 覆盖、追加------幂等重算的基石;
  3. 水位线WATERMARK 是迟到数据的答案;
  4. Read Cachecachemodel/cachesizeLAST_ROW 秒回,配大屏最新值;
  5. 闭环:写入(Part 6)→ 流计算(本篇)→ 订阅(Part 7)/查询,全部在 TDengine 内完成。

至此,单机上的存取、查询、订阅、流计算全部打通。下一步跳出单机:集群架构与高可用------数据怎么分片、副本怎么同步、节点挂了怎么办。

10. 官方资料

相关推荐
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧9 小时前
C++学习:基础知识的掌握
c++·visualstudio
C++ 老炮儿的技术栈15 小时前
从 Qt Designer 属性编辑器的层级可以看到继承链
c语言·数据库·c++·qt·sqlite·visual studio
new_zhou16 小时前
C++ 项目 AI 协作指南(Windows / MSVC 环境)
c++·人工智能·windows
水饺编程17 小时前
AT&T 汇编语言学习笔记开篇语
c语言·汇编·c++
Iruoyaoxh17 小时前
类和对象~
开发语言·c++
程序喵大人17 小时前
【C++进阶】STL算法与函数对象 - 04 find、count和any_of把查询写成意图
开发语言·c++·算法
豆沙沙包?17 小时前
c++中引用(P7-P11)
java·c++·算法
wuminyu17 小时前
虚拟线程底层ForkJoinPool的工作窃取算法机制
java·linux·c语言·jvm·c++
吹什么轩18 小时前
c++复习:c++11:lambda表达式
开发语言·c++