Apache IoTDB(9):数据库操作——数据写入从CLI到集群部署的六种实战

引言

在工业物联网(IIoT)场景中,每台设备每秒可能产生数百个数据点,一个中型工厂每天就会生成TB级的时序数据。传统关系型数据库在处理这种高频写入时,往往面临三大痛点:高延迟、低吞吐量、存储成本失控。Apache IoTDB作为专为时序数据设计的数据库,通过分层存储架构和LSM树优化,实现了单节点百万级数据点/秒的写入能力。

Apache IoTDB 时序数据库【系列篇章】

No.文章地址(点击进入)
1Apache IoTDB(1):时序数据库介绍与单机版安装部署指南
2Apache IoTDB(2):时序数据库 IoTDB 集群安装部署的技术优势与适用场景分析
3Apache IoTDB(3):时序数据库 IoTDB Docker部署从单机到集群的全场景部署与实践指南
4Apache IoTDB(4):深度解析时序数据库 IoTDB 在Kubernetes 集群中的部署与实践指南
5Apache IoTDB(5):深度解析时序数据库 IoTDB 中 AINode 工具的部署与实践
6Apache IoTDB(6):深入解析数据库管理操作——增删改查与异构数据库实战指南
7Apache IoTDB(7):设备模板管理——工业物联网元数据标准化的破局之道
8Apache IoTDB(8):时间序列管理——从创建到分析的实战指南

本文将深度解析IoTDB的六种数据写入方式,结合工业场景,提供可复用的代码模板与性能调优公式。
在这里插入图片描述

一、 CLI写入数据

IoTDB 为用户提供多种插入实时数据的方式,例如在 Cli/Shell 工具 中直接输入插入数据的 INSERT 语句,或使用 Java API(标准 Java JDBC 接口)单条或批量执行插入数据的 INSERT 语句。

注:写入重复时间戳的数据则原时间戳数据被覆盖,可视为更新数据

1.1 使用 INSERT 语句

使用 INSERT 语句可以向指定的已经创建的一条或多条时间序列中插入数据。对于每一条数据,均由一个时间戳类型的时间戳和一个数值或布尔值、字符串类型的传感器采集值组成。

在本节的场景实例下,以其中的两个时间序列root.ln.wf02.wt02.statusroot.ln.wf02.wt02.hardware为例 ,它们的数据类型分别为 BOOLEAN 和 TEXT。

单列数据插入示例代码如下:

IoTDB > insert into root.ln.wf02.wt02(timestamp,status) values(1,true)
IoTDB > insert into root.ln.wf02.wt02(timestamp,hardware) values(1, 'v1')

以上示例代码将长整型的 timestamp 以及值为 true 的数据插入到时间序列root.ln.wf02.wt02.status中和将长整型的 timestamp 以及值为”v1”的数据插入到时间序列root.ln.wf02.wt02.hardware中。执行成功后会返回执行时间,代表数据插入已完成。

注意:在 IoTDB 中,TEXT 类型的数据单双引号都可以来表示,上面的插入语句是用的是双引号表示 TEXT类型数据,下面的示例将使用单引号表示 TEXT 类型数据。

INSERT 语句还可以支持在同一个时间点下多列数据的插入,同时向 2 时间点插入上述两个时间序列的值,多列数据插入示例代码如下:

IoTDB > insert into root.ln.wf02.wt02(timestamp, status, hardware) values (2, false, 'v2')

此外,INSERT 语句支持一次性插入多行数据,同时向 2 个不同时间点插入上述时间序列的值,示例代码如下:

IoTDB > insert into root.ln.wf02.wt02(timestamp, status, hardware) VALUES (3, false, 'v3'),(4, true, 'v4')

插入数据后我们可以使用 SELECT 语句简单查询已插入的数据。

IoTDB > select * from root.ln.wf02.wt02 where time < 5

结果如图所示。由查询结果可以看出,单列、多列数据的插入操作正确执行。
在这里插入图片描述
此外,我们可以省略 timestamp 列,此时系统将使用当前的系统时间作为该数据点的时间戳,示例代码如下:

IoTDB > insert into root.ln.wf02.wt02(status, hardware) values (false, 'v2')

注意: 当一次插入多行数据时必须指定时间戳

1.2 向对齐时间序列插入数据

向对齐时间序列插入数据只需在SQL中增加ALIGNED关键词,其他类似

示例代码如下:

IoTDB > create aligned timeseries root.sg1.d1(s1 INT32, s2 DOUBLE)
IoTDB > insert into root.sg1.d1(time, s1, s2) aligned values(1, 1, 1)
IoTDB > insert into root.sg1.d1(time, s1, s2) aligned values(2, 2, 2), (3, 3, 3)
IoTDB > select * from root.sg1.d1

结果如图所示。由查询结果可以看出,数据的插入操作正确执行。
在这里插入图片描述

二、REST API写入

IoTDB 的 RESTful 服务可用于查询、写入和管理操作,它使用 OpenAPI 标准来定义接口并生成框架

2.1 开启RESTful 服务

RESTful 服务默认情况是关闭的

找到IoTDB安装目录下面的conf/iotdb-system.properties文件,将 enable_rest_service 设置为 true 以启用该模块。

enable_rest_service=true

2.2 鉴权

除了检活接口 /ping,RESTful 服务使用了基础(basic)鉴权,每次 URL 请求都需要在 header 中携带 ‘Authorization’: 'Basic ’ + base64.encode(username + ‘:’ + password)。

示例中使用的用户名为:root,密码为:root,对应的 Basic 鉴权 Header 格式为Authorization: Basic cm9vdDpyb290

1.若用户名密码认证失败,则返回如下信息:
HTTP 状态码:401
返回结构体如下

{
  "code": 600,
  "message": "WRONG_LOGIN_PASSWORD_ERROR"
}

2.若未设置 Authorization,则返回如下信息:

HTTP 状态码:401

返回结构体如下

{
  "code": 603,
  "message": "UNINITIALIZED_AUTH_ERROR"
}

2.3 接口

2.3.1 ping

ping 接口可以用于线上服务检活。

请求方式:GET

请求路径:http://ip:port/ping

请求示例:

curl http://127.0.0.1:18080/ping

返回的 HTTP 状态码:

200:当前服务工作正常,可以接收外部请求
503:当前服务出现异常,不能接收外部请求

响应参数:

在这里插入图片描述

2.3.2 query

query 接口可以用于处理数据查询和元数据查询。

请求方式:POST

请求头:application/json

请求路径: http://ip:port/rest/v2/query

参数说明:
在这里插入图片描述
响应参数:
在这里插入图片描述
示例(表达式查询):

curl -H "Content-Type:application/json" -H "Authorization:Basic cm9vdDpyb290" -X POST --data '{"sql":"select s3, s4, s3 + 1 from root.sg27 limit 2"}' http://127.0.0.1:18080/rest/v2/query

响应

{
  "expressions": [
    "root.sg27.s3",
    "root.sg27.s4",
    "root.sg27.s3 + 1"
  ],
  "column_names": null,
  "timestamps": [
    1635232143960,
    1635232153960
  ],
  "values": [
    [
      11,
      null
    ],
    [
      false,
      true
    ],
    [
      12.0,
      null
    ]
  ]
}
2.3.3 nonQuery

请求方式:POST

请求头:application/json

请求路径:http://ip:port/rest/v2/nonQuery

示例:

curl -H "Content-Type:application/json" -H "Authorization:Basic cm9vdDpyb290" -X POST --data '{"sql":"CREATE DATABASE root.ln"}' http://127.0.0.1:18080/rest/v2/nonQuery

响应

{
  "code": 200,
  "message": "SUCCESS_STATUS"
}
2.3.4 insertTablet

请求方式:POST

请求头:application/json

请求路径:http://ip:port/rest/v2/insertTablet

参数说明:

在这里插入图片描述
示例:

curl -H "Content-Type:application/json" -H "Authorization:Basic cm9vdDpyb290" -X POST --data '{"timestamps":[1635232143960,1635232153960],"measurements":["s3","s4"],"data_types":["INT32","BOOLEAN"],"values":[[11,null],[false,true]],"is_aligned":false,"device":"root.sg27"}' http://127.0.0.1:18080/rest/v2/insertTablet

响应

{
  "code": 200,
  "message": "SUCCESS_STATUS"
}
2.3.5 insertRecords

请求方式:POST

请求头:application/json

请求路径:http://ip:port/rest/v2/insertRecords

参数说明:

在这里插入图片描述

示例:

curl -H "Content-Type:application/json" -H "Authorization:Basic cm9vdDpyb290" -X POST --data '{"timestamps":[1635232113960,1635232151960,1635232143960,1635232143960],"measurements_list":[["s33","s44"],["s55","s66"],["s77","s88"],["s771","s881"]],"data_types_list":[["INT32","INT64"],["FLOAT","DOUBLE"],["FLOAT","DOUBLE"],["BOOLEAN","TEXT"]],"values_list":[[1,11],[2.1,2],[4,6],[false,"cccccc"]],"is_aligned":false,"devices":["root.s1","root.s1","root.s1","root.s3"]}' http://127.0.0.1:18080/rest/v2/insertRecords

响应

{
  "code": 200,
  "message": "SUCCESS_STATUS"
}

三、MQTT写入

3.1 概述

MQTT 是一种专为物联网(IoT)和低带宽环境设计的轻量级消息传输协议,基于发布/订阅(Pub/Sub)模型,支持设备间高效、可靠的双向通信。其核心目标是低功耗、低带宽消耗和高实时性,尤其适合网络不稳定或资源受限的场景(如传感器、移动设备)。

IoTDB 深度集成了 MQTT 协议能力,完整兼容 MQTT v3.1(OASIS 国际标准协议)。IoTDB 服务器内置高性能 MQTT Broker 服务模块,无需第三方中间件,支持设备通过 MQTT 报文将时序数据直接写入 IoTDB 存储引擎。
在这里插入图片描述

3.2 内置 MQTT 服务

内置的 MQTT 服务提供了通过 MQTT 直接连接到 IoTDB 的能力。 它侦听来自 MQTT 客户端的发布消息,然后立即将数据写入存储。
MQTT 主题与 IoTDB 时间序列相对应。
消息有效载荷可以由 Java SPI 加载的PayloadFormatter格式化为事件,默认实现为JSONPayloadFormatter
默认的json格式化程序支持两种 json 格式以及由他们组成的json数组,以下是 MQTT 消息有效负载示例:

 {
      "device":"root.sg.d1",
      "timestamp":1586076045524,
      "measurements":["s1","s2"],
      "values":[0.530635,0.530635]
 }

或者

 {
      "device":"root.sg.d1",
      "timestamps":[1586076045524,1586076065526],
      "measurements":["s1","s2"],
      "values":[[0.530635,0.530635], [0.530655,0.530695]]
 }

或者以上两者的JSON数组形式。
在这里插入图片描述

3.3 MQTT 配置

默认情况下,IoTDB MQTT 服务从${IOTDB_HOME}/${IOTDB_CONF}/iotdb-system.properties加载配置。

配置如下:
在这里插入图片描述

3.4 示例代码

以下是 mqtt 客户端将消息发送到 IoTDB 服务器的示例。

MQTT mqtt = new MQTT();
mqtt.setHost("127.0.0.1", 1883);
mqtt.setUserName("root");
mqtt.setPassword("root");

BlockingConnection connection = mqtt.blockingConnection();
connection.connect();

Random random = new Random();
for (int i = 0; i < 10; i++) {
   String payload = String.format("{\n" +
           "\"device\":\"root.sg.d1\",\n" +
           "\"timestamp\":%d,\n" +
           "\"measurements\":[\"s1\"],\n" +
           "\"values\":[%f]\n" +
           "}", System.currentTimeMillis(), random.nextDouble());

   connection.publish("root.sg.d1.s1", payload.getBytes(), QoS.AT_LEAST_ONCE, false);
}

connection.disconnect();

3.5 自定义 MQTT 消息格式

在生产环境中,每个设备通常都配备了自己的 MQTT 客户端,且这些客户端的消息格式已经预先设定。如果按照 IoTDB 所支持的 MQTT 消息格式进行通信,就需要对现有的所有客户端进行全面的升级改造,这无疑会带来较高的成本。然而,我们可以通过简单的编程手段,轻松实现 MQTT 消息格式的自定义,而无需改造客户端。
可以在源码的example/mqtt-customize项目中找到一个简单示例。

假定mqtt客户端传过来的是以下消息格式:

 {
    "time":1586076045523,
    "deviceID":"car_1",
    "deviceType":"油车",
    "point":"油量",
    "value":10.0
}

或者JSON的数组形式:

[
    {        
        "time":1586076045523,        
        "deviceID":"car_1",        
        "deviceType":"油车",        
        "point":"油量",        
        "value":10.0
    },
    {       
        "time":1586076045524,       
        "deviceID":"car_2",
        "deviceType":"新能源车",       
        "point":"速度",       
        "value":80.0
    }
]

则可以通过以下步骤设置设置自定义MQTT消息格式:

  1. 创建一个 Java 项目,增加如下依赖
        <dependency>
            <groupId>org.apache.iotdb</groupId>
            <artifactId>iotdb-server</artifactId>
            <version>2.0.4-SNAPSHOT</version>
        </dependency>
  1. 创建一个实现类,实现接口 org.apache.iotdb.db.mqtt.protocol.PayloadFormatter
package org.apache.iotdb.mqtt.server;

import org.apache.iotdb.db.protocol.mqtt.Message;
import org.apache.iotdb.db.protocol.mqtt.PayloadFormatter;
import org.apache.iotdb.db.protocol.mqtt.TableMessage;

import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import io.netty.buffer.ByteBuf;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.tsfile.enums.TSDataType;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * The Customized JSON payload formatter. one json format supported: { "time":1586076045523,
 * "deviceID":"car_1", "deviceType":"新能源车", "point":"速度", "value":80.0 }
 */
public class CustomizedJsonPayloadFormatter implements PayloadFormatter {
    private static final String JSON_KEY_TIME = "time";
    private static final String JSON_KEY_DEVICEID = "deviceID";
    private static final String JSON_KEY_DEVICETYPE = "deviceType";
    private static final String JSON_KEY_POINT = "point";
    private static final String JSON_KEY_VALUE = "value";
    private static final Gson GSON = new GsonBuilder().create();

    @Override
    public List<Message> format(String topic, ByteBuf payload) {
        if (payload == null) {
            return new ArrayList<>();
        }
        String txt = payload.toString(StandardCharsets.UTF_8);
        JsonElement jsonElement = GSON.fromJson(txt, JsonElement.class);
        if (jsonElement.isJsonObject()) {
            JsonObject jsonObject = jsonElement.getAsJsonObject();
            return formatTableRow(topic, jsonObject);
        } else if (jsonElement.isJsonArray()) {
            JsonArray jsonArray = jsonElement.getAsJsonArray();
            List<Message> messages = new ArrayList<>();
            for (JsonElement element : jsonArray) {
                JsonObject jsonObject = element.getAsJsonObject();
                messages.addAll(formatTableRow(topic, jsonObject));
            }
            return messages;
        }
        throw new JsonParseException("payload is invalidate");
    }

    @Override
    @Deprecated
    public List<Message> format(ByteBuf payload) {
        throw new NotImplementedException();
    }

    private List<Message> formatTableRow(String topic, JsonObject jsonObject) {
        TableMessage message = new TableMessage();
        String database = !topic.contains("/") ? topic : topic.substring(0, topic.indexOf("/"));
        String table = "test_table";

        // Parsing Database Name
        message.setDatabase((database));

        // Parsing Table Name
        message.setTable(table);

        // Parsing Tags
        List<String> tagKeys = new ArrayList<>();
        tagKeys.add(JSON_KEY_DEVICEID);
        List<Object> tagValues = new ArrayList<>();
        tagValues.add(jsonObject.get(JSON_KEY_DEVICEID).getAsString());
        message.setTagKeys(tagKeys);
        message.setTagValues(tagValues);

        // Parsing Attributes
        List<String> attributeKeys = new ArrayList<>();
        List<Object> attributeValues = new ArrayList<>();
        attributeKeys.add(JSON_KEY_DEVICETYPE);
        attributeValues.add(jsonObject.get(JSON_KEY_DEVICETYPE).getAsString());
        message.setAttributeKeys(attributeKeys);
        message.setAttributeValues(attributeValues);

        // Parsing Fields
        List<String> fields = Arrays.asList(JSON_KEY_POINT);
        List<TSDataType> dataTypes = Arrays.asList(TSDataType.FLOAT);
        List<Object> values = Arrays.asList(jsonObject.get(JSON_KEY_VALUE).getAsFloat());
        message.setFields(fields);
        message.setDataTypes(dataTypes);
        message.setValues(values);

        // Parsing timestamp
        message.setTimestamp(jsonObject.get(JSON_KEY_TIME).getAsLong());
        return Lists.newArrayList(message);
    }

    @Override
    public String getName() {
        // set the value of mqtt_payload_formatter in iotdb-common.properties as the following string:
        return "CustomizedJson2Table";
    }

    @Override
    public String getType() {
        return PayloadFormatter.TABLE_TYPE;
    }
}
  1. 修改项目中的src/main/resources/META-INF/services/org.apache.iotdb.db.protocol.mqtt.PayloadFormatter文件:
    将示例中的文件内容清除,并将刚才的实现类的全名(包名.类名)写入文件中。注意,这个文件中只有一行。
    在本例中,文件内容为: org.apache.iotdb.mqtt.server.CustomizedJsonPayloadFormatter
  2. 编译项目生成一个 jar 包: mvn package -DskipTests

在 IoTDB 服务端:

  1. 创建 ${IOTDB_HOME}/ext/mqtt/ 文件夹, 将刚才的 jar 包放入此文件夹。
  2. 打开 MQTT 服务参数. (enable_mqtt_service=trueinconf/iotdb-system.properties)
  3. 用刚才的实现类中的 getName() 方法的返回值 设置为conf/iotdb-system.propertiesmqtt_payload_formatter 的值,
    , 在本例中,为 CustomizedJson2Table
  4. 启动 IoTDB
  5. 搞定

More: MQTT 协议的消息不限于 json,你还可以用任意二进制。通过如下函数获得:
payload.forEachByte() or payload.array

3.6 注意事项

为避免因缺省client_id引发的兼容性问题,强烈建议在所有MQTT客户端中始终显式地提供唯一且非空的 client_id。
不同客户端在client_id缺失或为空时的表现并不一致,常见示例如下:

  1. 显式传入空字符串
    • MQTTX:client_id="“时,IoTDB会直接丢弃消息;
    • mosquitto_pub:client_id=”"时,IoTDB能正常接收消息。
  2. 完全不传client_id
    • MQTTX:消息可被IoTDB正常接收;
    • mosquitto_pub:IoTDB拒绝连接。
    由此可见,显式指定唯一且非空的client_id是消除上述差异、确保消息可靠投递的最简单做法。

四、批量数据导入

针对于不同场景,IoTDB 为用户提供多种批量导入数据的操作方式,本章节向大家介绍最为常用的两种方式为 CSV文本形式的导入 和 TsFile文件形式的导入。

4.1 TsFile批量导入

TsFile 是在 IoTDB 中使用的时间序列的文件格式,您可以通过CLI等工具直接将存有时间序列的一个或多个 TsFile 文件导入到另外一个正在运行的IoTDB实例中

4.2 CSV批量导入

CSV 是以纯文本形式存储表格数据,您可以在CSV文件中写入多条格式化的数据,并批量的将这些数据导入到 IoTDB 中,在导入数据之前,建议在IoTDB中创建好对应的元数据信息。如果忘记创建元数据也不要担心,IoTDB 可以自动将CSV中数据推断为其对应的数据类型,前提是你每一列的数据类型必须唯一。除单个文件外,此工具还支持以文件夹的形式导入多个 CSV 文件,并且支持设置如时间精度等优化参数

五、无模式写入

在物联网场景中,由于设备的类型、数量可能随时间动态增减,不同设备可能产生不同字段的数据(如温度、湿度、状态码等),业务上又往往需要快速部署,需要灵活接入新设备且无需繁琐的预定义流程。因此,不同于传统时序数据库通常需要预先定义数据模型,IoTDB支持不提前创建元数据,在写入数据时,数据库中将自动识别并注册所需的元数据,实现自动建模。

用户既可以通过CLI使用insert语句或者原生接口的方式,批量或者单行实时写入一个设备或者多个设备的测点数据,也可以通过导入工具导入csv,TsFile等格式的历史数据,在导入过程中会自动创建序列,数据类型,压缩编码方式等元数据。

结语

Apache IoTDB通过创新的分层存储架构和优化的写入流程,解决了时序数据写入的高并发、低延迟、高压缩率三大核心需求。后续博主会带来更多的 IoTDB 数据库操作~

评论 121
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个天蝎座白勺程序猿

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值