4. SCADA实时数据库选型与集成
SCADA系统采集的遥测数据具有典型的时序特征:按固定周期产生、带时间戳、写多读少、按时间范围聚合查询。传统关系库在每秒数万点写入时很快遇到瓶颈,因此本项目在采集层与业务层之间引入时序数据库(TSDB)作为实时/历史数据存储。本节围绕InfluxDB与TDengine两条主流路线,讲清选型依据、部署方式、数据模型设计,以及源码中写入与查询接口的实现。
一、选型:InfluxDB 与 TDengine 的取舍
两者都能满足SCADA场景,但定位不同。InfluxDB 2.x 采用 measurement + tag + field 模型,生态成熟、Flux/InfluxQL 查询灵活、Grafana 原生支持好,适合测点种类多、标签维度丰富的项目;其单机写入能力约数十万点/秒,集群版为商业授权。TDengine 采用“一个设备一张表”的模型,超级表(STable)定义schema,子表按设备自动扩展,内置降采样、流式计算和缓存,压缩率高、单机写入可达百万点/秒,且开源版即可用集群,适合测点数量大、以设备为单位的电力/能源监控。
本项目的选型判断可归纳为:若现场设备数在千级以内、需要复杂标签过滤与灵活聚合,选 InfluxDB;若设备数上万、以“设备+测点”固定结构为主、追求写入吞吐与存储成本,选 TDengine。示例项目默认采用 TDengine 3.x,同时保留 InfluxDB 适配层,通过统一接口屏蔽差异。
二、部署:单机起步,容器化落地
示例项目采用 Docker Compose 部署,便于开发与现场一致。TDengine 3.x 单机部署命令如下,映射 6030(原生连接)与 6041(REST/taosAdapter)端口:
docker run -d --name tdengine \
-p 6030:6030 -p 6041:6041 \
-v /data/taos:/var/lib/taos \
-e TAOS_FQDN=tdengine \
tdengine/tdengine:3.2.0.0
InfluxDB 2.x 则需初始化组织、bucket 与 token:
docker run -d --name influxdb \
-p 8086:8086 \
-v /data/influx:/var/lib/influxdb2 \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=scada \
-e DOCKER_INFLUXDB_INIT_PASSWORD=Scada@2024 \
-e DOCKER_INFLUXDB_INIT_ORG=energy \
-e DOCKER_INFLUXDB_INIT_BUCKET=scada_raw \
influxdb:2.7
部署后需验证:TDengine 用 taos -h tdengine -s "show databases;" 确认连通;InfluxDB 用 curl http://localhost:8086/health 返回 {"status":"pass"}。生产环境还应配置数据目录独立磁盘、开启 WAL、设置合理的 maxRows 与缓存参数。
三、数据模型设计
SCADA点位可抽象为“设备—测点—时间—值—质量码”。TDengine 中,用超级表定义一类设备的公共schema,子表按设备实例创建。示例:
CREATE DATABASE scada KEEP 90d PRECISION 'ms';
USE scada;
CREATE STABLE meters (
ts TIMESTAMP,
value DOUBLE,
quality TINYINT
) TAGS (
station BINARY(32),
device BINARY(32),
point BINARY(32),
unit BINARY(16)
);
CREATE TABLE dev_001 USING meters
TAGS ('变电站A', 'METER_001', 'Ua', 'V');
InfluxDB 对应模型为 measurement=meter,tag 为 station/device/point/unit,field 为 value/quality。两者共同原则:标签(tag)用于过滤,字段(field)用于计算;时间戳统一毫秒精度、UTC 存储;质量码 0=正常、1=可疑、2=无效,便于查询时过滤坏点。
四、源码中的写入接口
写入适配层定义统一接口,屏蔽底层差异。以 Go 为例:
type Point struct {
Ts int64 // 毫秒时间戳
Device string
Point string
Value float64
Quality int8
}
type TSDBWriter interface {
WriteBatch(ctx context.Context, pts []Point) error
Close() error
}
TDengine 实现使用参数绑定与批量插入,避免SQL拼接注入并提升性能:
func (w *TaosWriter) WriteBatch(ctx context.Context, pts []Point) error {
tx, err := w.db.Begin()
if err != nil { return err }
stmt, _ := tx.Prepare("INSERT INTO ? USING meters TAGS(?,?,?,?) VALUES(?,?,?)")
for _, p := range pts {
stmt.SetTableName("dev_" + p.Device)
stmt.SetTags("变电站A", p.Device, p.Point, "V")
stmt.SetTimestamp(0, time.UnixMilli(p.Ts))
stmt.SetFloat(1, p.Value)
stmt.SetInt8(2, p.Quality)
if _, err := stmt.Exec(); err != nil { return err }
}
return tx.Commit()
}
InfluxDB 实现使用 Line Protocol 批量写入:
func (w *InfluxWriter) WriteBatch(ctx context.Context, pts []Point) error {
var buf bytes.Buffer
for _, p := range pts {
fmt.Fprintf(&buf, "meter,station=变电站A,device=%s,point=%s value=%f,quality=%di %d\n",
p.Device, p.Point, p.Value, p.Quality, p.Ts*1e6)
}
_, err := w.api.WriteWithResponse(ctx, w.org, w.bucket, buf.String())
return err
}
写入策略上,采集服务将点位放入环形缓冲,每 500 点或每 200ms 触发一次批量写入;失败时指数退避重试,超过阈值写入本地磁盘队列,恢复后补传,保证断网不丢数据。
五、查询接口与结果判读
查询接口需支持三类典型需求:最新值(实时画面)、时间范围原始值(趋势)、降采样聚合(报表)。TDengine 示例:
-- 最新值
SELECT LAST_ROW(value) FROM meters WHERE device='METER_001' AND point='Ua';
-- 5分钟平均,过滤坏点
SELECT _wstart, AVG(value) FROM meters
WHERE point='Ua' AND quality=0 AND ts >= NOW-1h
INTERVAL(5m);
InfluxDB 对应 Flux 查询:
from(bucket:"scada_raw")
|> range(start: -1h)
|> filter(fn:(r) => r._measurement=="meter" and r.point=="Ua" and r.quality==0)
|> aggregateWindow(every: 5m, fn: mean)
结果判读要点:若最新值查询返回空,先确认设备是否在写、时间戳是否落在保留期内;若聚合曲线出现断点,检查 quality 过滤是否过严或采集是否丢点;若查询延迟高,应确认是否命中时间索引、是否对 tag 做了高基数设计。建议在接口层统一返回 {ts, value, quality} 数组,前端不感知底层库差异。