SpringBoot集成TDengine的完整使用指南
一、什么是 TDengine
先说说几个关键点。TDengine 是一款高性能、分布式时序数据库,专为物联网、工业互联网、金融监控等场景量身打造。它到底有多强?来看看它的核心特性。

- 高性能写入:单表每秒可以达到百万级数据写入,这速度,对于海量设备数据来说,简直不要太爽。
- 高压缩比:时序数据专用压缩算法,让存储空间节省 90% 以上,成本控制这块拿捏得死死的。
- 超级表设计:一次定义数据结构,就能自动为每个设备创建自表,这个设计思路相当巧妙。
- 标签查询:支持按设备类型、位置等维度快速聚合,让数据筛选变得简单直接。
- SQL 语法:采用类 SQL 语法,学习成本低,团队上手非常快。
二、快速开始
那么,我们需要准备什么环境呢?先来看看这张表格,一目了然。
2.1 环境要求
| 组件 | 版本 |
|---|---|
| JDK | 1.8+ |
| Spring Boot | 2.5.x+ |
| TDengine | 2.6.x+ |
| Ma ven | 3.6+ |
2.2 TDengine 安装
接下来,安装 TDengine。这里给出两种常见方式,一个用于 Linux 环境,另一个是 Docker 部署,强烈推荐后者,省心省力。
Linux 环境
wget https://www.taosdata.com/assets-download/taos/TD-server-2.6.0-Linux-x64-2.0.15.0.tar.gz tar -xzf TD-server-2.6.0-Linux-x64-2.0.15.0.tar.gz cd TD-server-2.6.0-Linux-x64-2.0.15.0 sudo ./install.sh
Docker 环境(推荐)
docker run -d --name tdengine -p 6030:6030 tdengine/tdengine:2.6.0
验证安装
taos
三、项目集成步骤
现在来看看如何将 TDengine 集成到 Spring Boot 项目中。
3.1 创建 Spring Boot 项目
你可以使用 Spring Initializr 或者手动创建一个 Ma ven 项目,这都不是问题。
3.2 添加 Ma ven 依赖
在 pom.xml 中添加以下依赖。这里是核心配置,JDBC 驱动和连接池都安排上了。
4.0.0 com.example tdengine-demo 1.0.0 jar org.springframework.boot spring-boot-starter-parent 2.5.12 1.8 2.0.0 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc com.taosdata.jdbc taos-jdbcdriver ${taos.version} com.zaxxer HikariCP cn.hutool hutool-all 5.7.22 org.projectlombok lombok true org.springframework.boot spring-boot-ma ven-plugin
3.3 配置文件
接下来是 application.yml 配置文件。数据源、连接池、日志级别,一次搞定。
src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: tdengine-demo
# TDengine 数据源配置
taos:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.taosdata.jdbc.TSDBDriver
url: jdbc:TAOS://localhost:6030/iot
username: root
password: taosdata
# 连接池配置
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# SQL 执行日志 (开发环境开启)
logging:
level:
com.taosdata.jdbc: debug
com.example.taos: trace
四、自定义注解实现
为了让代码更优雅、更可配置,我们引入了一些自定义注解。这就是框架设计的美妙之处——用注解来完成元数据映射,把复杂的事情简单化。
4.1 @STable - 超级表注解
这个注解用来标记实体类与 TDengine 超级表的映射关系。你可以看到,它支持子表名模板,比如 t_${deviceId},动态解析,非常灵活。
src/main/ja va/com/example/taos/annotation/STable.ja va
package com.example.taos.annotation;
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
/**
* 超级表注解
* 用于标记 TDengine 的超级表和数据表映射关系
*
* 使用示例:
* @STable(
* value = "meters", // 超级表名
* table = "t_${deviceId}", // 子表名模板
* using = true // 启用自动创建子表
* )
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface STable {
/**
* 超级表表名
* @return 超级表名称,如果不使用超级表则返回空字符串
*/
String value() default "";
/**
* 数据表表名
* 支持使用 ${} 占位符动态生成表名
* 示例:数据表名 = "t_${deviceId}" → 实际表名:t_device001, t_device002
* @return 数据表名称模板
*/
String table();
/**
* 是否自动创建数据表
* true: 如果数据表不存在,插入时自动创建
* false: 数据表必须预先创建好
* @return 是否启用自动创建
*/
boolean using() default false;
}
4.2 @IotTableId - 时间戳字段注解
每个时序数据表都少不了一个时间戳字段。这个注解就是用来标记那个字段的,而且强制要求一个实体类只能有一个。
src/main/ja va/com/example/taos/annotation/IotTableId.ja va
package com.example.taos.annotation;
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
/**
* 时间戳字段注解
* 用于标记 TDengine 表的主键时间字段
*
* 使用要求:
* - 字段类型必须是 Date、Long 或 Timestamp
* - 每个实体类必须有且仅有一个 @IotTableId 字段
*
* 使用示例:
* @IotTableId
* private Date ts;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTableId {
/**
* 字段名称 (默认使用实体字段名)
* @return 表字段名
*/
String value() default "";
}
4.3 @IotField - 普通数据字段注解
数据字段都交给它。它支持自定义字段名、数据库字段类型,以及小数位数的设置,灵活性相当高。
src/main/ja va/com/example/taos/annotation/IotField.ja va
package com.example.taos.annotation;
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
import ja va.sql.Types;
/**
* 普通数据字段注解
* 用于标记 TDengine 表的数据列
*
* 使用示例:
* @IotField("voltage")
* private Double voltage;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotField {
/**
* 表字段名 (默认使用实体字段名)
* @return 字段名
*/
String value() default "";
/**
* 表字段类型 (默认使用实体字段类型)
* @see ja va.sql.Types
* @return 字段类型
*/
int type() default Types.NULL;
/**
* 保留小数位数 (针对浮点数)
* -1 表示不处理
* @return 小数位数
*/
int scale() default -1;
}
4.4 @IotTag - 标签字段注解
TDengine 的超级表有一个特色功能就是 Tag。它用来存储设备的维度信息,比如设备ID、位置、类型等,是进行高效聚合查询的关键。
src/main/ja va/com/example/taos/annotation/IotTag.ja va
package com.example.taos.annotation;
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
import ja va.sql.Types;
/**
* 标签字段注解
* 用于标记 TDengine 超级表的 TAG 列
* TAG 是 TDengine 的特色,用于设备维度查询
*
* 使用示例:
* @IotTag("device_id")
* private String deviceId;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTag {
/**
* 标签名称 (默认使用实体字段名)
* @return 标签名
*/
String value() default "";
/**
* 标签字段类型 (默认使用实体字段类型)
* @see ja va.sql.Types
* @return 字段类型
*/
int type() default Types.NULL;
}
五、核心工具类实现
好,注解看完了,核心工具类来了。有了这些类,元数据解析、SQL 生成、表名解析,一切都变得井井有条。
5.1 字段元数据类
这个类封装了实体字段和数据库列的映射信息。它负责把 Ja va 反射的 Field 变成一个更易于管理的元数据对象。
src/main/ja va/com/example/taos/sql/FieldMeta.ja va
package com.example.taos.sql;
import ja va.lang.reflect.Field;
/**
* 字段元数据
* 封装实体类字段的映射信息
*/
public class FieldMeta {
/** 字段名 */
private String fieldName;
/** 字段类型 (Ja va 类型) */
private String fieldType;
/** 数据库字段名 */
private String columnName;
/** 数据库字段类型 */
private int columnType;
/** 反射字段对象 */
private Field field;
/** 小数位数 */
private int scale = -1;
public FieldMeta() {
}
public FieldMeta(Field field) {
this.field = field;
this.fieldName = field.getName();
this.fieldType = field.getType().getSimpleName();
this.columnName = field.getName();
field.setAccessible(true);
}
// Getter 和 Setter
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public String getFieldType() {
return fieldType;
}
public void setFieldType(String fieldType) {
this.fieldType = fieldType;
}
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public int getColumnType() {
return columnType;
}
public void setColumnType(int columnType) {
this.columnType = columnType;
}
public Field getField() {
return field;
}
public void setField(Field field) {
this.field = field;
}
public int getScale() {
return scale;
}
public void setScale(int scale) {
this.scale = scale;
}
/**
* 获取字段值
*/
public Object getFieldValue(Object entity) throws IllegalAccessException {
return field.get(entity);
}
}
5.2 表元数据类
这个类封装了完整的表结构映射信息,包括超级表名、子表名模板、字段列表和标签列表。它放了一堆生成 SQL 的方法,很实用。
src/main/ja va/com/example/taos/sql/TaosTableMeta.ja va
package com.example.taos.sql;
import ja va.util.ArrayList;
import ja va.util.List;
/**
* TDengine 表元数据
* 封装超级表、数据表、字段的完整映射信息
*/
public class TaosTableMeta {
/** 超级表名 */
private String stableName;
/** 数据表名模板 */
private String tableNameTemplate;
/** 是否自动创建数据表 */
private boolean using;
/** 时间戳字段 */
private FieldMeta timeField;
/** 数据字段列表 */
private List dataFields = new ArrayList<>();
/** 标签字段列表 */
private List tagFields = new ArrayList<>();
// Getter 和 Setter
public String getStableName() {
return stableName;
}
public void setStableName(String stableName) {
this.stableName = stableName;
}
public String getTableNameTemplate() {
return tableNameTemplate;
}
public void setTableNameTemplate(String tableNameTemplate) {
this.tableNameTemplate = tableNameTemplate;
}
public boolean isUsing() {
return using;
}
public void setUsing(boolean using) {
this.using = using;
}
public FieldMeta getTimeField() {
return timeField;
}
public void setTimeField(FieldMeta timeField) {
this.timeField = timeField;
}
public List getDataFields() {
return dataFields;
}
public void setDataFields(List dataFields) {
this.dataFields = dataFields;
}
public List getTagFields() {
return tagFields;
}
public void setTagFields(List tagFields) {
this.tagFields = tagFields;
}
/**
* 生成 INSERT SQL 语句
*/
public String generateInsertSql() {
StringBuilder sb = new StringBuilder();
sb.append("(");
// 时间戳字段
sb.append(timeField.getColumnName());
// 数据字段
for (FieldMeta field : dataFields) {
sb.append(", ").append(field.getColumnName());
}
sb.append(")");
return sb.toString();
}
/**
* 生成参数占位符 SQL
*/
public String generateParamSql() {
StringBuilder sb = new StringBuilder();
sb.append("(");
sb.append("?");
int totalFields = 1 + dataFields.size();
for (int i = 1; i < totalFields; i++) {
sb.append(", ?");
}
sb.append(")");
return sb.toString();
}
/**
* 生成超级表 USING 子句
*/
public String generateUsingClause() {
if (stableName == null || stableName.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("USING ").append(stableName);
if (!tagFields.isEmpty()) {
sb.append(" (");
for (int i = 0; i < tagFields.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(tagFields.get(i).getColumnName());
}
sb.append(")");
}
return sb.toString();
}
/**
* 生成 TAG 值列表
*/
public String generateTagValues() {
if (tagFields.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < tagFields.size(); i++) {
if (i > 0) sb.append(", ");
sb.append("?");
}
sb.append(")");
return sb.toString();
}
/**
* 获取总字段数
*/
public int getTotalFields() {
return 1 + dataFields.size(); // 时间戳 + 数据字段
}
}
5.3 SQL 执行上下文
一个轻量级的封装,将 SQL 语句和参数打包在一起,方便传递和执行。
src/main/ja va/com/example/taos/sql/SqlExecContext.ja va
package com.example.taos.sql;
/**
* SQL 执行上下文
* 封装要执行的 SQL 语句和参数
*/
public class SqlExecContext {
/** SQL 语句 */
private String sql;
/** 参数值数组 */
private Object[] params;
public SqlExecContext(String sql, Object[] params) {
this.sql = sql;
this.params = params;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SQL: ").append(sql).append("n");
sb.append("Params: ");
if (params != null) {
for (Object param : params) {
sb.append(param).append(", ");
}
}
return sb.toString();
}
}
5.4 元数据解析器
这才是重头戏。它负责扫描实体类上的注解,解析出结构化的元数据。它还引入了缓存,避免重复解析开销。
src/main/ja va/com/example/taos/sql/TaosMetaExtractor.ja va
package com.example.taos.annotation;
import com.example.taos.sql.FieldMeta;
import com.example.taos.sql.TaosTableMeta;
import ja va.lang.reflect.Field;
import ja va.sql.Date;
import ja va.sql.Types;
import ja va.util.Map;
import ja va.util.concurrent.ConcurrentHashMap;
/**
* TDengine 元数据解析器
* 从实体类注解中提取表结构信息
*/
public class TaosMetaExtractor {
/** 元数据缓存 */
private static final Map, TaosTableMeta> META_CACHE = new ConcurrentHashMap<>();
/**
* 解析实体类的表元数据
*/
public static TaosTableMeta parse(Class> entityClass) {
return META_CACHE.computeIfAbsent(entityClass, TaosMetaExtractor::doParse);
}
/**
* 清空缓存
*/
public static void clearCache() {
META_CACHE.clear();
}
private static TaosTableMeta doParse(Class> entityClass) {
STable sTable = entityClass.getAnnotation(STable.class);
if (sTable == null) {
throw new IllegalArgumentException(
"实体类必须使用 @STable 注解:" + entityClass.getName()
);
}
TaosTableMeta meta = new TaosTableMeta();
meta.setStableName(sTable.value());
meta.setTableNameTemplate(sTable.table());
meta.setUsing(sTable.using());
// 解析字段
FieldMeta timeField = null;
for (Field field : entityClass.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(IotTableId.class)) {
// 时间戳字段
if (timeField != null) {
throw new IllegalArgumentException(
"实体类只能有一个 @IotTableId 字段:" + entityClass.getName()
);
}
timeField = parseField(field, Types.TIMESTAMP);
// 验证类型
Class> fieldType = field.getType();
if (!Date.class.isAssignableFrom(fieldType)
&& !Long.class.isAssignableFrom(fieldType)
&& field.getType() != long.class) {
throw new IllegalArgumentException(
"@IotTableId 字段必须是 Date 或 Long 类型:" + field.getName()
);
}
} else if (field.isAnnotationPresent(IotField.class)) {
// 普通数据字段
IotField annotation = field.getAnnotation(IotField.class);
FieldMeta fieldMeta = parseField(field, annotation.type());
// 自定义字段名
if (!annotation.value().isEmpty()) {
fieldMeta.setColumnName(annotation.value());
}
// 小数位数
if (annotation.scale() >= 0) {
fieldMeta.setScale(annotation.scale());
}
meta.getDataFields().add(fieldMeta);
} else if (field.isAnnotationPresent(IotTag.class)) {
// 标签字段
IotTag annotation = field.getAnnotation(IotTag.class);
FieldMeta fieldMeta = parseField(field, annotation.type());
// 自定义标签名
if (!annotation.value().isEmpty()) {
fieldMeta.setColumnName(annotation.value());
}
meta.getTagFields().add(fieldMeta);
}
}
if (timeField == null) {
throw new IllegalArgumentException(
"实体类必须有且仅有一个 @IotTableId 字段:" + entityClass.getName()
);
}
meta.setTimeField(timeField);
return meta;
}
private static FieldMeta parseField(Field field, int columnType) {
FieldMeta meta = new FieldMeta(field);
// 根据 Ja va 类型推断数据库类型
if (columnType == Types.NULL) {
columnType = inferSqlType(field.getType());
}
meta.setColumnType(columnType);
return meta;
}
/**
* 根据 Ja va 类型推断 SQL 类型
*/
private static int inferSqlType(Class> type) {
if (type == String.class) {
return Types.VARCHAR;
} else if (type == Integer.class || type == int.class) {
return Types.INTEGER;
} else if (type == Long.class || type == long.class) {
return Types.BIGINT;
} else if (type == Double.class || type == double.class) {
return Types.DOUBLE;
} else if (type == Float.class || type == float.class) {
return Types.FLOAT;
} else if (type == Boolean.class || type == boolean.class) {
return Types.BOOLEAN;
} else if (ja va.util.Date.class.isAssignableFrom(type)
|| ja va.sql.Date.class.isAssignableFrom(type)
|| ja va.sql.Timestamp.class.isAssignableFrom(type)) {
return Types.TIMESTAMP;
} else {
return Types.VARCHAR;
}
}
}
5.5 动态表名解析器
当表名里包含 ${fieldName} 这样的占位符时,就需要这个解析器来干活了。它负责从实体对象中提取对应字段的值,替换占位符,生成真实的子表名。
src/main/ja va/com/example/taos/sql/TableNameResolver.ja va
package com.example.taos.sql;
import ja va.lang.reflect.Field;
import ja va.util.regex.Matcher;
import ja va.util.regex.Pattern;
/**
* 动态表名解析器
* 解析表名模板中的 ${fieldName} 占位符
*/
public class TableNameResolver {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\$\{([^}]+)\}");
/**
* 解析表名
* 示例:tableTemplate = "t_${deviceId}", entity.deviceId = "device001"
* 返回:"t_device001"
*/
public static String resolve(String tableTemplate, Object entity) {
if (tableTemplate == null || tableTemplate.isEmpty()) {
throw new IllegalArgumentException("表名模板不能为空");
}
Matcher matcher = PLACEHOLDER_PATTERN.matcher(tableTemplate);
String tableName = tableTemplate;
while (matcher.find()) {
String fieldName = matcher.group(1);
String fieldValue = getFieldValue(entity, fieldName);
if (fieldValue == null) {
throw new IllegalArgumentException(
"解析表名失败,字段 [" + fieldName + "] 的值为 null"
);
}
tableName = tableName.replace("${" + fieldName + "}", fieldValue);
}
return tableName;
}
/**
* 获取字段值
*/
private static String getFieldValue(Object entity, String fieldName) {
try {
Field field = entity.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(entity);
return value != null ? value.toString() : null;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(
"无法获取字段值:" + fieldName, e
);
}
}
}
六、数据访问层实现
底层工具类准备就绪,数据访问层就可以直接上手了。
6.1 数据源配置类
Spring Boot 项目,数据源配置是第一步。这个类把配置文件中 taos.datasource 前缀的属性绑定到 HikariCP 连接池。
src/main/ja va/com/example/taos/config/TaosConfig.ja va
package com.example.taos.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ja vax.sql.DataSource;
/**
* TDengine 数据源配置
*/
@Configuration
public class TaosConfig {
/**
* 创建 TDengine 数据源
*/
@Bean("taosDataSource")
@ConfigurationProperties(prefix = "taos.datasource")
public DataSource taosDataSource() {
return new HikariDataSource();
}
}
6.2 TDengine 操作接口
定义一个 Repository 接口,先约定好数据访问的方法。
src/main/ja va/com/example/taos/repository/TaosRepository.ja va
package com.example.taos.repository;
import ja va.util.List;
/**
* TDengine 数据访问接口
*/
public interface TaosRepository {
/**
* 插入单条数据
* @param entity 实体对象
* @return 影响的行数
*/
int insert(Object entity);
/**
* 批量插入数据
* @param entities 实体对象列表
* @return 影响的行数
*/
int batchInsert(List
6.3 TDengine 操作实现类
这是具体的数据访问实现。它依赖上面写的工具类,通过元数据解析、动态表名,拼接出正确的 INSERT SQL,然后通过 JDBC 执行。批量插入时还按表名做了分组处理,很周到。
src/main/ja va/com/example/taos/repository/TaosRepositoryImpl.ja va
package com.example.taos.repository;
import com.example.taos.annotation.IotTableId;
import com.example.taos.annotation.STable;
import com.example.taos.sql.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import ja vax.sql.DataSource;
import ja va.sql.*;
import ja va.util.*;
/**
* TDengine 数据访问实现类
*/
@Repository
public class TaosRepositoryImpl implements TaosRepository {
private static final Logger logger = LoggerFactory.getLogger(TaosRepositoryImpl.class);
private final DataSource dataSource;
public TaosRepositoryImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public int insert(Object entity) {
try {
SqlExecContext context = buildInsertContext(entity);
return executeUpdate(context);
} catch (Exception e) {
logger.error("插入数据失败", e);
throw new RuntimeException("插入数据失败", e);
}
}
@Override
public int batchInsert(List entities) {
if (entities == null || entities.isEmpty()) {
return 0;
}
try {
return executeBatch(entities);
} catch (Exception e) {
logger.error("批量插入数据失败", e);
throw new RuntimeException("批量插入数据失败", e);
}
}
@Override
public int batchInsert(List entities, int batchSize) {
if (entities == null || entities.isEmpty()) {
return 0;
}
int totalInserted = 0;
int totalSize = entities.size();
int batchCount = (totalSize + batchSize - 1) / batchSize;
for (int i = 0; i < batchCount; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, totalSize);
List batch = entities.subList(fromIndex, toIndex);
totalInserted += batchInsert(batch);
}
return totalInserted;
}
/**
* 构建 INSERT SQL 上下文
*/
private SqlExecContext buildInsertContext(Object entity) throws Exception {
Class> entityClass = entity.getClass();
// 解析元数据
TaosTableMeta meta = TaosMetaExtractor.parse(entityClass);
// 解析表名 (支持动态表名)
String tableName = TableNameResolver.resolve(
meta.getTableNameTemplate(), entity
);
// 构建 SQL
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(tableName);
// 如果使用超级表自动创建
if (meta.isUsing() && meta.getStableName() != null && !meta.getStableName().isEmpty()) {
String usingClause = meta.generateUsingClause();
if (usingClause != null) {
sql.append(" ").append(usingClause);
}
String tagValues = meta.generateTagValues();
if (tagValues != null) {
sql.append(" tags ").append(tagValues);
}
}
// 数据字段
sql.append(" ").append(meta.generateInsertSql());
sql.append(" VALUES ");
sql.append(meta.generateParamSql());
// 构建参数
List params = new ArrayList<>();
// 时间戳参数
FieldMeta timeField = meta.getTimeField();
Object timeValue = timeField.getFieldValue(entity);
if (timeValue == null) {
// 自动生成时间戳
timeValue = new ja va.util.Date();
}
params.add(convertTimestamp(timeValue));
// 数据字段参数
for (FieldMeta field : meta.getDataFields()) {
params.add(field.getFieldValue(entity));
}
// TAG 字段参数 (如果有 USING 子句)
if (meta.isUsing() && !meta.getTagFields().isEmpty()) {
for (FieldMeta tagField : meta.getTagFields()) {
params.add(tagField.getFieldValue(entity));
}
}
return new SqlExecContext(sql.toString(), params.toArray());
}
/**
* 转换时间戳
*/
private Object convertTimestamp(Object timeValue) {
if (timeValue instanceof ja va.util.Date) {
return new Timestamp(((ja va.util.Date) timeValue).getTime());
} else if (timeValue instanceof Long) {
return new Timestamp((Long) timeValue);
} else {
return timeValue;
}
}
/**
* 批量执行
*/
private int executeBatch(List entities) throws Exception {
if (entities.isEmpty()) {
return 0;
}
Class> entityClass = entities.get(0).getClass();
TaosTableMeta meta = TaosMetaExtractor.parse(entityClass);
// 按表名分组 (不同设备的数据表可能不同)
Map> groupedEntities = new HashMap<>();
for (Object entity : entities) {
String tableName = TableNameResolver.resolve(
meta.getTableNameTemplate(), entity
);
groupedEntities.computeIfAbsent(tableName, k -> new ArrayList<>()).add(entity);
}
// 按表名分别执行批量插入
int totalInserted = 0;
for (Map.Entry> entry : groupedEntities.entrySet()) {
String tableName = entry.getKey();
List tableEntities = entry.getValue();
// 构建批量 SQL
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(tableName);
// USING 子句 (只取第一个实体的 TAG 值)
if (meta.isUsing() && meta.getStableName() != null && !meta.getStableName().isEmpty()) {
String usingClause = meta.generateUsingClause();
if (usingClause != null) {
sql.append(" ").append(usingClause);
}
Object firstEntity = tableEntities.get(0);
String tagValues = buildTagValues(firstEntity, meta);
if (tagValues != null) {
sql.append(" tags ").append(tagValues);
}
}
sql.append(" ").append(meta.generateInsertSql());
sql.append(" VALUES ");
// 多行 VALUES
List allParams = new ArrayList<>();
for (int i = 0; i < tableEntities.size(); i++) {
if (i > 0) sql.append(", ");
sql.append(meta.generateParamSql());
Object entity = tableEntities.get(i);
List params = buildParams(entity, meta);
allParams.addAll(params);
}
SqlExecContext context = new SqlExecContext(sql.toString(), allParams.toArray());
totalInserted += executeUpdate(context);
}
return totalInserted;
}
/**
* 构建 TAG 值字符串
*/
private String buildTagValues(Object entity, TaosTableMeta meta) {
if (meta.getTagFields().isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < meta.getTagFields().size(); i++) {
if (i > 0) sb.append(", ");
try {
Object value = meta.getTagFields().get(i).getFieldValue(entity);
if (value instanceof String) {
sb.append("'").append(value).append("'");
} else {
sb.append(value);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("获取 TAG 字段值失败", e);
}
}
sb.append(")");
return sb.toString();
}
/**
* 构建参数列表
*/
private List buildParams(Object entity, TaosTableMeta meta) throws IllegalAccessException {
List params = new ArrayList<>();
// 时间戳
FieldMeta timeField = meta.getTimeField();
Object timeValue = timeField.getFieldValue(entity);
if (timeValue == null) {
timeValue = new ja va.util.Date();
}
params.add(convertTimestamp(timeValue));
// 数据字段
for (FieldMeta field : meta.getDataFields()) {
params.add(field.getFieldValue(entity));
}
return params;
}
/**
* 执行 SQL 更新
*/
private int executeUpdate(SqlExecContext context) throws SQLException {
long startTime = System.currentTimeMillis();
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(context.getSql())) {
// 设置参数
Object[] params = context.getParams();
for (int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
int rows = stmt.executeUpdate();
if (logger.isTraceEnabled()) {
logger.trace("TDengine 执行成功 ({}ms): {} 行",
System.currentTimeMillis() - startTime, rows);
}
return rows;
}
}
}
七、业务服务层
数据访问层搭好了,就可以构建业务服务了。这个 DeviceDataService 提供单条保存、批量保存、模拟数据等功能。它把数据写入包装成异步操作,不阻塞上层调用,适合 IoT 高并发采集场景。
src/main/ja va/com/example/taos/service/DeviceDataService.ja va
package com.example.taos.service;
import com.example.taos.entity.DeviceData;
import com.example.taos.repository.TaosRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import ja va.util.ArrayList;
import ja va.util.Date;
import ja va.util.List;
/**
* 设备数据服务类
*/
@Service
public class DeviceDataService {
private static final Logger logger = LoggerFactory.getLogger(DeviceDataService.class);
@Autowired
private TaosRepository taosRepository;
/**
* 保存单条设备数据 (异步)
*/
@Async
public void sa veData(DeviceData data) {
data.setTs(new Date());
taosRepository.insert(data);
logger.debug("保存设备数据:deviceId={}, voltage={}",
data.getDeviceId(), data.getVoltage());
}
/**
* 批量保存设备数据 (异步)
*/
@Async
public void batchSa ve(List dataList) {
for (DeviceData data : dataList) {
data.setTs(new Date());
}
int rows = taosRepository.batchInsert(dataList);
logger.info("批量保存设备数据:{} 条,成功 {} 行", dataList.size(), rows);
}
/**
* 分批保存设备数据 (适合大数据量)
*/
@Async
public void batchSa veWithSize(List dataList, int batchSize) {
for (DeviceData data : dataList) {
data.setTs(new Date());
}
int rows = taosRepository.batchInsert(dataList, batchSize);
logger.info("分批保存设备数据:{} 条 (每批 {} 条),成功 {} 行",
dataList.size(), batchSize, rows);
}
/**
* 模拟设备数据采集
*/
public DeviceData simulateData(String deviceId, String location, String deviceType) {
DeviceData data = new DeviceData(deviceId);
data.setLocation(location);
data.setDeviceType(deviceType);
// 模拟传感器数据
data.setVoltage(220.0 + Math.random() * 10);
data.setCurrent(5.0 + Math.random() * 2);
data.setActivePower(1000.0 + Math.random() * 100);
data.setReactivePower(50.0 + Math.random() * 10);
data.setPowerFactor(0.95 + Math.random() * 0.05);
return data;
}
/**
* 批量模拟数据
*/
public List simulateBatchData(int count, String deviceId,
String location, String deviceType) {
List dataList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
dataList.add(simulateData(deviceId, location, deviceType));
}
return dataList;
}
}
八、控制器层
控制器暴露 RESTful 接口。这个设计亮点在于——它支持单条上报、批量上报和模拟生成数据。对于 IoT 平台来说,API 接口的简洁性非常重要,这里的实现就是正解。
src/main/ja va/com/example/taos/controller/DeviceDataController.ja va
package com.example.taos.controller;
import com.example.taos.entity.DeviceData;
import com.example.taos.service.DeviceDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ja va.util.HashMap;
import ja va.util.List;
import ja va.util.Map;
/**
* 设备数据控制器
*/
@RestController
@RequestMapping("/api/device")
@CrossOrigin(origins = "*")
public class DeviceDataController {
@Autowired
private DeviceDataService deviceDataService;
/**
* 上报单条设备数据
*/
@PostMapping("/report")
public Map reportData(@RequestBody DeviceData data) {
Map result = new HashMap<>();
try {
deviceDataService.sa veData(data);
result.put("code", 200);
result.put("message", "数据上报成功");
} catch (Exception e) {
result.put("code", 500);
result.put("message", "数据上报失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 批量上报设备数据
*/
@PostMapping("/batch-report")
public Map batchReport(@RequestBody List dataList) {
Map result = new HashMap<>();
try {
deviceDataService.batchSa ve(dataList);
result.put("code", 200);
result.put("message", "批量上报成功,共 " + dataList.size() + " 条");
} catch (Exception e) {
result.put("code", 500);
result.put("message", "批量上报失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 模拟设备数据生成
*/
@GetMapping("/simulate")
public Map simulate(
@RequestParam(defaultValue = "device001") String deviceId,
@RequestParam(defaultValue = "10") int count
) {
Map result = new HashMap<>();
try {
List dataList = deviceDataService.simulateBatchData(
count, deviceId, "北京机房", "智能电表"
);
deviceDataService.batchSa veWithSize(dataList, 100);
result.put("code", 200);
result.put("message", "模拟生成 " + count + " 条数据成功");
result.put("data", dataList);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "模拟失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 健康检查接口
*/
@GetMapping("/health")
public Map health() {
Map result = new HashMap<>();
result.put("code", 200);
result.put("message", "服务正常运行");
return result;
}
}
九、实体类定义
实体类 DeviceData 是上面所有注解的集大成者。它映射了超级表 meters,子表名模板 t_${deviceId},并开启了自动创建子表。从字段定义中,也能看出一个智能电表的数据模型该长什么样。
src/main/ja va/com/example/taos/entity/DeviceData.ja va
package com.example.taos.entity;
import com.example.taos.annotation.IotTableId;
import com.example.taos.annotation.STable;
import com.example.taos.annotation.IotField;
import com.example.taos.annotation.IotTag;
import ja va.util.Date;
/**
* 智能电表数据采集实体
*
* 数据库映射:
* - 超级表:meters
* - 子表:t_device001, t_device002, ...
*/
@STable(
value = "meters", // 超级表名
table = "t_${deviceId}", // 子表名模板 (自动替换 ${deviceId})
using = true // 启用自动创建子表
)
public class DeviceData {
// ========== 时间戳字段 (必填) ==========
@IotTableId
private Date ts;
// ========== 普通数据字段 ==========
/** 电压 (V) */
@IotField("voltage")
private Double voltage;
/** 电流 (A) */
@IotField("current")
private Double current;
/** 有功功率 (W) */
@IotField("active_power")
private Double activePower;
/** 无功功率 (var) */
@IotField("reactive_power")
private Double reactivePower;
/** 功率因数 */
@IotField("power_factor")
private Double powerFactor;
// ========== 标签字段 (用于设备维度查询) ==========
/** 设备编号 */
@IotTag("device_id")
private String deviceId;
/** 安装位置 */
@IotTag("location")
private String location;
/** 设备类型 */
@IotTag("device_type")
private String deviceType;
// 默认构造函数
public DeviceData() {
this.ts = new Date();
}
// 带设备编号的构造函数
public DeviceData(String deviceId) {
this();
this.deviceId = deviceId;
}
// Getter 和 Setter
public Date getTs() {
return ts;
}
public void setTs(Date ts) {
this.ts = ts;
}
public Double getVoltage() {
return voltage;
}
public void setVoltage(Double voltage) {
this.voltage = voltage;
}
public Double getCurrent() {
return current;
}
public void setCurrent(Double current) {
this.current = current;
}
public Double getActivePower() {
return activePower;
}
public void setActivePower(Double activePower) {
this.activePower = activePower;
}
public Double getReactivePower() {
return reactivePower;
}
public void setReactivePower(Double reactivePower) {
this.reactivePower = reactivePower;
}
public Double getPowerFactor() {
return powerFactor;
}
public void setPowerFactor(Double powerFactor) {
this.powerFactor = powerFactor;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
@Override
public String toString() {
return "DeviceData{" +
"ts=" + ts +
", deviceId='" + deviceId + ''' +
", voltage=" + voltage +
", current=" + current +
", activePower=" + activePower +
", reactivePower=" + reactivePower +
", powerFactor=" + powerFactor +
'}';
}
}
十、启动类
最后一步,启动类。加上了 @EnableAsync 注解,异步功能就开启了。
src/main/ja va/com/example/taos/TaosDemoApplication.ja va
package com.example.taos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* TDengine Demo 启动类
*/
@SpringBootApplication
@EnableAsync // 启用异步处理
public class TaosDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TaosDemoApplication.class, args);
System.out.println("=========================================");
System.out.println(" TDengine Demo 启动成功!");
System.out.println(" API 地址:http://localhost:8080/api/device");
System.out.println(" 健康检查:http://localhost:8080/api/device/health");
System.out.println("=========================================");
}
}
十一、初始化数据库
运行项目之前,先把 TDengine 数据库和超级表建好。
11.1 创建数据库和超级表
使用 taos shell 或客户端工具执行下面这段 SQL,就能创建数据库、创建超级表、定义 Tag。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS iot;
-- 使用数据库
USE iot;
-- 创建超级表
CREATE STABLE IF NOT EXISTS meters (
ts TIMESTAMP,
voltage DOUBLE,
current DOUBLE,
active_power DOUBLE,
reactive_power DOUBLE,
power_factor DOUBLE
) TAGS (
device_id VARCHAR(50),
location VARCHAR(50),
device_type VARCHAR(50)
);
-- 查看超级表结构
DESCRIBE meters;
-- 查看数据库
SHOW DATABASES;
11.2 验证连接
建完表后,可以手动查一下,确认连接和表结构都是正常的。
# 进入 taos shell taos # 执行查询 USE iot; SELECT * FROM meters LIMIT 10;
十二、测试验证
一切就绪,我们可以跑起来看看效果了。
12.1 启动项目
mvn spring-boot:run
看到以下输出表示启动成功:
========================================= TDengine Demo 启动成功! API 地址:http://localhost:8080/api/device 健康检查:http://localhost:8080/api/device/health =========================================
12.2 使用 cURL 测试
健康检查
curl http://localhost:8080/api/device/health
单条数据上报
curl -X POST http://localhost:8080/api/device/report
-H "Content-Type: application/json"
-d '{
"deviceId": "device001",
"location": "北京机房",
"deviceType": "智能电表",
"voltage": 220.5,
"current": 10.2,
"activePower": 2200.5,
"reactivePower": 100.3,
"powerFactor": 0.95
}'
批量数据上报
curl -X POST http://localhost:8080/api/device/batch-report
-H "Content-Type: application/json"
-d '[
{
"deviceId": "device001",
"location": "北京机房",
"deviceType": "智能电表",
"voltage": 220.5,
"current": 10.2,
"activePower": 2200.5,
"reactivePower": 100.3,
"powerFactor": 0.95
},
{
"deviceId": "device002",
"location": "上海机房",
"deviceType": "智能电表",
"voltage": 219.8,
"current": 9.8,
"activePower": 2150.0,
"reactivePower": 98.5,
"powerFactor": 0.94
}
]'
模拟数据生成
curl "http://localhost:8080/api/device/simulate?deviceId=device003&count=100"
12.3 在 TDengine 中查询
数据写进去之后,在 taos 里就可以灵活查询了。这也是 TDengine 的强项。
-- 使用数据库 USE iot; -- 查询所有数据 SELECT * FROM meters; -- 查询特定设备的数据 SELECT * FROM t_device001; -- 按设备统计 SELECT device_id, COUNT(*), A VG(voltage), A VG(current) FROM meters GROUP BY device_id; -- 时间范围查询 SELECT * FROM meters WHERE ts >= '2024-01-01 00:00:00' AND ts <= NOW(); -- 查看所有子表 SHOW TABLES;
十三、性能优化建议
跑起来只是第一步,如何写得快、用得稳才是关键。下面几条建议,都是实战中总结出来的。
13.1 批量写入
千万不要一条一条写,那是性能杀手。推荐的做法是一次写入 1000-5000 条。
// ✅ 推荐:批量写入,每批 1000-5000 条
service.batchSa veWithSize(dataList, 1000);
// ❌ 避免:单条循环写入
for (DeviceData data : dataList) {
service.sa veData(data); // 性能差
}
13.2 异步处理
用 @Async 把数据写入操作放到后台线程执行,主线程不阻塞,用户体验好。
@Service
public class DeviceDataService {
@Async
public void sa veData(DeviceData data) {
// 异步写入,不阻塞主线程
taosRepository.insert(data);
}
}
配置异步线程池:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("taos-async-");
executor.initialize();
return executor;
}
}
13.3 连接池优化
连接池参数别用默认值。这里给出一个适合高并发场景的配置参考。
taos:
datasource:
# 最小连接数
minimum-idle: 5
# 最大连接数
maximum-pool-size: 20
# 连接超时时间 (ms)
connection-timeout: 30000
# 空闲连接超时时间 (ms)
idle-timeout: 600000
# 连接最大生命周期 (ms)
max-lifetime: 1800000
13.4 表设计优化
- 合理设计 Tag: 把常用作查询过滤条件的字段设为 Tag,能有效提升查询性能。
- 子表数量控制: 建议单超级表下的子表数量不要超过 10 万张,超出后性能会下滑。
- 数据保留策略: 根据业务需求配置数据保留时长,老数据自动清理,避免表无限膨胀。
- 分区策略: 时序数据库天然支持按时间自动分区,这个要利用好。
十四、常见问题
实践过程中,肯定会遇到一些坑。下面列了几个最常见的,一次性说清楚。
Q1: 连接失败 “Unable to load JNI libraries”
原因: TDengine JDBC 驱动依赖本地 JNI 库
解决方案:
Linux:
# 安装 TDengine 客户端 yum install -y TDengine-client # 或设置 LD_LIBRARY_PATH export LD_LIBRARY_PATH=/usr/local/taos/driver:$LD_LIBRARY_PATH
Docker:
# 确保容器已安装 TDengine 客户端 docker exec -it tdengine taos
Q2: 中文乱码
解决方案: 在 JDBC URL 中添加字符集参数
taos:
datasource:
url: jdbc:TAOS://localhost:6030/iot?charset=UTF-8
Q3: 批量插入性能低
优化建议:
- 增加批次大小(1000-5000 条/批)
- 使用异步写入
- 增加连接池大小
- 检查网络延迟
Q4: 表不存在错误
解决方案:
- 确保
@STable(using = true)已设置 - 检查表名模板
${fieldName}对应字段是否有值 - 手动创建超级表
十五、项目目录结构
最后贴一下项目目录结构,方便你对照参考。
tdengine-demo/ ├── src/ │ └── main/ │ ├── ja va/ │ │ └── com/ │ │ └── example/ │ │ └── taos/ │ │ ├── TaosDemoApplication.ja va # 启动类 │ │ ├── annotation/ # 自定义注解 │ │ │ ├── STable.ja va │ │ │ ├── IotTableId.ja va │ │ │ ├── IotField.ja va │ │ │ └── IotTag.ja va │ │ ├── config/ # 配置类 │ │ │ └── TaosConfig.ja va │ │ ├── controller/ # 控制器 │ │ │ └── DeviceDataController.ja va │ │ ├── entity/ # 实体类 │ │ │ └── DeviceData.ja va │ │ ├── repository/ # 数据访问层 │ │ │ ├── TaosRepository.ja va │ │ │ └── TaosRepositoryImpl.ja va │ │ ├── service/ # 服务层 │ │ │ └── DeviceDataService.ja va │ │ └── sql/ # SQL 工具类 │ │ ├── FieldMeta.ja va │ │ ├── TaosTableMeta.ja va │ │ ├── SqlExecContext.ja va │ │ ├── TaosMetaExtractor.ja va │ │ └── TableNameResolver.ja va │ └── resources/ │ └── application.yml # 配置文件 ├── pom.xml # Ma ven 配置 └── README.md # 项目说明
十六、总结
从零开始,把 Spring Boot 集成 TDengine 的完整流程梳理了一遍。从依赖配置、自定义注解、核心工具类,到数据访问层、业务服务层和控制器,再到数据库初始化、常见问题排查,几乎把整个链路都打通了。
TDengine 作为国产开源时序数据库,在 IoT 场景下的表现确实出色。超级表 + 子表的设计,配合标签查询,轻松应对海量设备数据的采集与聚合。
以上,就是 SpringBoot 集成 TDengine 的完整指南。这篇文章覆盖了从理论到落地的全过程,希望能给你带来一些帮助。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Python+Pytest接口自动化测试方案从零到一实战实现指南
该方案基于Python+Pytest,采用分层结构清晰管理配置、接口、用例与数据。使用Requests库发送HTTP请求,通过YAML文件实现数据驱动与多环境灵活切换,利用Allure生成美观的可视化测试报告,支持统一断言机制,并可对接Jenkins实现持续集成自动化。
Python基础教程:列表查找排序与反转的常用方法
Python列表的查找(index与count用法)、排序(sort与sorted区别及key参数自定义排序)和反转(reverse、reversed及切片)操作详解,涵盖常见错误处理(如值不存在、类型错误)与实用技巧(安全查找、多级排序、频率统计),帮助高效处理列表数据。
使用Python扩展Unity编辑器实现自定义工具与工作流
为UnityPythonScripting包新增存根生成器、Python编辑器窗口和脚本浏览器。存根将C API转为Python类型信息,支持IDE代码补全。编辑器窗口避免C 编译等待。脚本窗口实现浏览与执行。
JVM崩溃FatalError问题排查与解决方案
IntelliJIDEA运行Java服务时,启用定时调试器导致JVM因SIGSEGV崩溃,根本原因在于调试器的native代码与G1垃圾回收机制冲突。关闭“启用定时调试器”选项后,问题彻底解决。
基于Map和Bean的策略模式Java实现详解
利用Spring容器将策略实现注入到Map中,通过业务标识直接匹配调用,替代传统构造方法或set方法选择策略。该方法减少上下文类维护成本,策略扩展只需在配置中心增加映射,业务侧完全解耦,并利用IoC管理策略生命周期。
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
相关攻略
2026-06-10 06:55
2026-06-10 06:55
2026-06-10 06:55
2026-06-10 06:54
2026-06-10 06:54
2026-06-10 06:54
2026-06-10 06:54
2026-06-10 06:54
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

