Spring-Boot 集成 TDengine 完整实战

Spring Boot 集成 TDengine 时序数据库完整实战

摘要:本文从零开始详细介绍如何在 Spring Boot 项目中集成 TDengine 时序数据库,包含完整的依赖配置、自定义注解实现、工具类代码、实体定义和可运行 Demo,看完就能直接集成到自己的项目中。


一、什么是 TDengine

TDengine 是一款高性能、分布式、时序数据库,专为物联网、工业互联网、金融监控等场景设计。主要特性包括:

  • 高性能写入:单表每秒百万级数据写入
  • 高压缩比:时序数据专用压缩算法,存储空间节省 90%+
  • 超级表设计:一次定义数据结构,自动为每个设备创建子表
  • 标签查询:支持按设备类型、位置等维度快速聚合
  • SQL 语法:类 SQL 语法,学习成本低

二、快速开始

2.1 环境要求

组件 版本
JDK 1.8+
Spring Boot 2.5.x+
TDengine 2.6.x+
Maven 3.6+

2.2 TDengine 安装

Linux 环境

bash 复制代码
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 环境(推荐)

bash 复制代码
docker run -d --name tdengine -p 6030:6030 tdengine/tdengine:2.6.0

验证安装

bash 复制代码
taos

三、项目集成步骤

3.1 创建 Spring Boot 项目

使用 Spring Initializr 或手动创建 Maven 项目。

3.2 添加 Maven 依赖

pom.xml 中添加以下依赖:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>tdengine-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.12</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <taos.version>2.0.0</taos.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        
        <!-- TDengine JDBC 驱动 (仅支持 JNI 版本) -->
        <dependency>
            <groupId>com.taosdata.jdbc</groupId>
            <artifactId>taos-jdbcdriver</artifactId>
            <version>${taos.version}</version>
        </dependency>
        
        <!-- HikariCP 连接池 -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        
        <!-- Hutool 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.22</version>
        </dependency>
        
        <!-- Lombok (可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.3 配置文件

src/main/resources/application.yml

yaml 复制代码
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 - 超级表注解

src/main/java/com/example/taos/annotation/STable.java

java 复制代码
package com.example.taos.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 超级表注解
 * 用于标记 TDengine 的超级表和数据表映射关系
 * 
 * 使用示例:
 * &#64;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/java/com/example/taos/annotation/IotTableId.java

java 复制代码
package com.example.taos.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 时间戳字段注解
 * 用于标记 TDengine 表的主键时间字段
 * 
 * 使用要求:
 * - 字段类型必须是 Date、Long 或 Timestamp
 * - 每个实体类必须有且仅有一个 @IotTableId 字段
 * 
 * 使用示例:
 * &#64;IotTableId
 * private Date ts;
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTableId {

    /**
     * 字段名称 (默认使用实体字段名)
     * @return 表字段名
     */
    String value() default "";
}

4.3 @IotField - 普通数据字段注解

src/main/java/com/example/taos/annotation/IotField.java

java 复制代码
package com.example.taos.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Types;

/**
 * 普通数据字段注解
 * 用于标记 TDengine 表的数据列
 * 
 * 使用示例:
 * &#64;IotField("voltage")
 * private Double voltage;
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotField {

    /**
     * 表字段名 (默认使用实体字段名)
     * @return 字段名
     */
    String value() default "";

    /**
     * 表字段类型 (默认使用实体字段类型)
     * @see java.sql.Types
     * @return 字段类型
     */
    int type() default Types.NULL;

    /**
     * 保留小数位数 (针对浮点数)
     * -1 表示不处理
     * @return 小数位数
     */
    int scale() default -1;
}

4.4 @IotTag - 标签字段注解

src/main/java/com/example/taos/annotation/IotTag.java

java 复制代码
package com.example.taos.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Types;

/**
 * 标签字段注解
 * 用于标记 TDengine 超级表的 TAG 列
 * TAG 是 TDengine 的特色,用于设备维度查询
 * 
 * 使用示例:
 * &#64;IotTag("device_id")
 * private String deviceId;
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTag {

    /**
     * 标签名称 (默认使用实体字段名)
     * @return 标签名
     */
    String value() default "";

    /**
     * 标签字段类型 (默认使用实体字段类型)
     * @see java.sql.Types
     * @return 字段类型
     */
    int type() default Types.NULL;
}

五、核心工具类实现

5.1 字段元数据类

src/main/java/com/example/taos/sql/FieldMeta.java

java 复制代码
package com.example.taos.sql;

import java.lang.reflect.Field;

/**
 * 字段元数据
 * 封装实体类字段的映射信息
 */
public class FieldMeta {

    /** 字段名 */
    private String fieldName;

    /** 字段类型 (Java 类型) */
    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 表元数据类

src/main/java/com/example/taos/sql/TaosTableMeta.java

java 复制代码
package com.example.taos.sql;

import java.util.ArrayList;
import java.util.List;

/**
 * TDengine 表元数据
 * 封装超级表、数据表、字段的完整映射信息
 */
public class TaosTableMeta {

    /** 超级表名 */
    private String stableName;

    /** 数据表名模板 */
    private String tableNameTemplate;

    /** 是否自动创建数据表 */
    private boolean using;

    /** 时间戳字段 */
    private FieldMeta timeField;

    /** 数据字段列表 */
    private List<FieldMeta> dataFields = new ArrayList<>();

    /** 标签字段列表 */
    private List<FieldMeta> 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);
    }

    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<FieldMeta> getDataFields() {
        return dataFields;
    }

    public void setDataFields(List<FieldMeta> dataFields) {
        this.dataFields = dataFields;
    }

    public List<FieldMeta> getTagFields() {
        return tagFields;
    }

    public void setTagFields(List<FieldMeta> 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 执行上下文

src/main/java/com/example/taos/sql/SqlExecContext.java

java 复制代码
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/java/com/example/taos/sql/TaosMetaExtractor.java

java 复制代码
package com.example.taos.annotation;
import com.example.taos.sql.FieldMeta;
import com.example.taos.sql.TaosTableMeta;

import java.lang.reflect.Field;
import java.sql.Date;
import java.sql.Types;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * TDengine 元数据解析器
 * 从实体类注解中提取表结构信息
 */
public class TaosMetaExtractor {

    /** 元数据缓存 */
    private static final Map<Class<?>, 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);
        
        // 根据 Java 类型推断数据库类型
        if (columnType == Types.NULL) {
            columnType = inferSqlType(field.getType());
        }
        meta.setColumnType(columnType);
        
        return meta;
    }

    /**
     * 根据 Java 类型推断 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 (java.util.Date.class.isAssignableFrom(type) 
                || java.sql.Date.class.isAssignableFrom(type)
                || java.sql.Timestamp.class.isAssignableFrom(type)) {
            return Types.TIMESTAMP;
        } else {
            return Types.VARCHAR;
        }
    }
}

5.5 动态表名解析器

src/main/java/com/example/taos/sql/TableNameResolver.java

java 复制代码
package com.example.taos.sql;

import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.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 数据源配置类

src/main/java/com/example/taos/config/TaosConfig.java

java 复制代码
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 javax.sql.DataSource;

/**
 * TDengine 数据源配置
 */
@Configuration
public class TaosConfig {

    /**
     * 创建 TDengine 数据源
     */
    @Bean("taosDataSource")
    @ConfigurationProperties(prefix = "taos.datasource")
    public DataSource taosDataSource() {
        return new HikariDataSource();
    }
}

6.2 TDengine 操作接口

src/main/java/com/example/taos/repository/TaosRepository.java

java 复制代码
package com.example.taos.repository;

import java.util.List;

/**
 * TDengine 数据访问接口
 */
public interface TaosRepository {

    /**
     * 插入单条数据
     * @param entity 实体对象
     * @return 影响的行数
     */
    int insert(Object entity);

    /**
     * 批量插入数据
     * @param entities 实体对象列表
     * @return 影响的行数
     */
    int batchInsert(List<Object> entities);

    /**
     * 分批批量插入数据
     * @param entities 实体对象列表
     * @param batchSize 每批数量
     * @return 影响的行数
     */
    int batchInsert(List<Object> entities, int batchSize);
}

6.3 TDengine 操作实现类

src/main/java/com/example/taos/repository/TaosRepositoryImpl.java

java 复制代码
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 javax.sql.DataSource;
import java.sql.*;
import java.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<Object> 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<Object> 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<Object> 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<Object> params = new ArrayList<>();
        
        // 时间戳参数
        FieldMeta timeField = meta.getTimeField();
        Object timeValue = timeField.getFieldValue(entity);
        if (timeValue == null) {
            // 自动生成时间戳
            timeValue = new java.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 java.util.Date) {
            return new Timestamp(((java.util.Date) timeValue).getTime());
        } else if (timeValue instanceof Long) {
            return new Timestamp((Long) timeValue);
        } else {
            return timeValue;
        }
    }

    /**
     * 批量执行
     */
    private int executeBatch(List<Object> entities) throws Exception {
        if (entities.isEmpty()) {
            return 0;
        }

        Class<?> entityClass = entities.get(0).getClass();
        TaosTableMeta meta = TaosMetaExtractor.parse(entityClass);

        // 按表名分组 (不同设备的数据表可能不同)
        Map<String, List<Object>> 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<String, List<Object>> entry : groupedEntities.entrySet()) {
            String tableName = entry.getKey();
            List<Object> 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<Object> 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<Object> 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<Object> buildParams(Object entity, TaosTableMeta meta) throws IllegalAccessException {
        List<Object> params = new ArrayList<>();
        
        // 时间戳
        FieldMeta timeField = meta.getTimeField();
        Object timeValue = timeField.getFieldValue(entity);
        if (timeValue == null) {
            timeValue = new java.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;
        }
    }
}

七、业务服务层

src/main/java/com/example/taos/service/DeviceDataService.java

java 复制代码
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 java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 设备数据服务类
 */
@Service
public class DeviceDataService {

    private static final Logger logger = LoggerFactory.getLogger(DeviceDataService.class);

    @Autowired
    private TaosRepository taosRepository;

    /**
     * 保存单条设备数据 (异步)
     */
    @Async
    public void saveData(DeviceData data) {
        data.setTs(new Date());
        taosRepository.insert(data);
        logger.debug("保存设备数据:deviceId={}, voltage={}", 
            data.getDeviceId(), data.getVoltage());
    }

    /**
     * 批量保存设备数据 (异步)
     */
    @Async
    public void batchSave(List<DeviceData> dataList) {
        for (DeviceData data : dataList) {
            data.setTs(new Date());
        }
        int rows = taosRepository.batchInsert(dataList);
        logger.info("批量保存设备数据:{} 条,成功 {} 行", dataList.size(), rows);
    }

    /**
     * 分批保存设备数据 (适合大数据量)
     */
    @Async
    public void batchSaveWithSize(List<DeviceData> 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<DeviceData> simulateBatchData(int count, String deviceId, 
                                               String location, String deviceType) {
        List<DeviceData> dataList = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            dataList.add(simulateData(deviceId, location, deviceType));
        }
        return dataList;
    }
}

八、控制器层

src/main/java/com/example/taos/controller/DeviceDataController.java

java 复制代码
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 java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 设备数据控制器
 */
@RestController
@RequestMapping("/api/device")
@CrossOrigin(origins = "*")
public class DeviceDataController {

    @Autowired
    private DeviceDataService deviceDataService;

    /**
     * 上报单条设备数据
     */
    @PostMapping("/report")
    public Map<String, Object> reportData(@RequestBody DeviceData data) {
        Map<String, Object> result = new HashMap<>();
        try {
            deviceDataService.saveData(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<String, Object> batchReport(@RequestBody List<DeviceData> dataList) {
        Map<String, Object> result = new HashMap<>();
        try {
            deviceDataService.batchSave(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<String, Object> simulate(
        @RequestParam(defaultValue = "device001") String deviceId,
        @RequestParam(defaultValue = "10") int count
    ) {
        Map<String, Object> result = new HashMap<>();
        try {
            List<DeviceData> dataList = deviceDataService.simulateBatchData(
                count, deviceId, "北京机房", "智能电表"
            );
            deviceDataService.batchSaveWithSize(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<String, Object> health() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "服务正常运行");
        return result;
    }
}

九、实体类定义

src/main/java/com/example/taos/entity/DeviceData.java

java 复制代码
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 java.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 +
                '}';
    }
}

十、启动类

src/main/java/com/example/taos/TaosDemoApplication.java

java 复制代码
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("=========================================");
    }
}

十一、初始化数据库

11.1 创建数据库和超级表

使用 taos shell 或客户端工具执行:

sql 复制代码
-- 创建数据库
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 验证连接

bash 复制代码
# 进入 taos shell
taos

# 执行查询
USE iot;
SELECT * FROM meters LIMIT 10;

十二、测试验证

12.1 启动项目

bash 复制代码
mvn spring-boot:run

看到以下输出表示启动成功:

复制代码
=========================================
   TDengine Demo 启动成功!
   API 地址:http://localhost:8080/api/device
   健康检查:http://localhost:8080/api/device/health
=========================================

12.2 使用 cURL 测试

健康检查

bash 复制代码
curl http://localhost:8080/api/device/health

单条数据上报

bash 复制代码
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
  }'

批量数据上报

bash 复制代码
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
    }
  ]'

模拟数据生成

bash 复制代码
curl "http://localhost:8080/api/device/simulate?deviceId=device003&count=100"

12.3 在 TDengine 中查询

sql 复制代码
-- 使用数据库
USE iot;

-- 查询所有数据
SELECT * FROM meters;

-- 查询特定设备的数据
SELECT * FROM t_device001;

-- 按设备统计
SELECT device_id, COUNT(*), AVG(voltage), AVG(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 批量写入

java 复制代码
// ✅ 推荐:批量写入,每批 1000-5000 条
service.batchSaveWithSize(dataList, 1000);

// ❌ 避免:单条循环写入
for (DeviceData data : dataList) {
    service.saveData(data);  // 性能差
}

13.2 异步处理

java 复制代码
@Service
public class DeviceDataService {
    
    @Async
    public void saveData(DeviceData data) {
        // 异步写入,不阻塞主线程
        taosRepository.insert(data);
    }
}

配置异步线程池:

java 复制代码
@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 连接池优化

yaml 复制代码
taos:
  datasource:
    # 最小连接数
    minimum-idle: 5
    # 最大连接数
    maximum-pool-size: 20
    # 连接超时时间 (ms)
    connection-timeout: 30000
    # 空闲连接超时时间 (ms)
    idle-timeout: 600000
    # 连接最大生命周期 (ms)
    max-lifetime: 1800000

13.4 表设计优化

  1. 合理设计 Tag: 将用于查询过滤的字段设为 Tag
  2. 子表数量控制: 建议单超级表下子表数量不超过 10 万
  3. 数据保留策略: 配置合适的数据保留时长
  4. 分区策略: 按时间自动分区

十四、常见问题

Q1: 连接失败 "Unable to load JNI libraries"

原因: TDengine JDBC 驱动依赖本地 JNI 库

解决方案:

Linux:

bash 复制代码
# 安装 TDengine 客户端
yum install -y TDengine-client

# 或设置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/taos/driver:$LD_LIBRARY_PATH

Docker:

bash 复制代码
# 确保容器已安装 TDengine 客户端
docker exec -it tdengine taos

Q2: 中文乱码

解决方案: 在 JDBC URL 中添加字符集参数

yaml 复制代码
taos:
  datasource:
    url: jdbc:TAOS://localhost:6030/iot?charset=UTF-8

Q3: 批量插入性能低

优化建议:

  1. 增加批次大小(1000-5000 条/批)
  2. 使用异步写入
  3. 增加连接池大小
  4. 检查网络延迟

Q4: 表不存在错误

解决方案:

  1. 确保 @STable(using = true) 已设置
  2. 检查表名模板 ${fieldName} 对应字段是否有值
  3. 手动创建超级表

十五、项目目录结构

复制代码
tdengine-demo/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/
│       │       └── example/
│       │           └── taos/
│       │               ├── TaosDemoApplication.java          # 启动类
│       │               ├── annotation/                        # 自定义注解
│       │               │   ├── STable.java
│       │               │   ├── IotTableId.java
│       │               │   ├── IotField.java
│       │               │   └── IotTag.java
│       │               ├── config/                            # 配置类
│       │               │   └── TaosConfig.java
│       │               ├── controller/                        # 控制器
│       │               │   └── DeviceDataController.java
│       │               ├── entity/                            # 实体类
│       │               │   └── DeviceData.java
│       │               ├── repository/                        # 数据访问层
│       │               │   ├── TaosRepository.java
│       │               │   └── TaosRepositoryImpl.java
│       │               ├── service/                           # 服务层
│       │               │   └── DeviceDataService.java
│       │               └── sql/                               # SQL 工具类
│       │                   ├── FieldMeta.java
│       │                   ├── TaosTableMeta.java
│       │                   ├── SqlExecContext.java
│       │                   ├── TaosMetaExtractor.java
│       │                   └── TableNameResolver.java
│       └── resources/
│           └── application.yml                                # 配置文件
├── pom.xml                                                    # Maven 配置
└── README.md                                                  # 项目说明

十六、总结

本文从零开始介绍如何在 Spring Boot 项目中集成 TDengine 时序数据库,包含:

  • 完整的依赖配置 - Maven 依赖和配置文件
  • 自定义注解实现 - 4 个核心注解的完整代码
  • 工具类实现 - 元数据解析、SQL 生成、表名解析
  • 数据访问层 - Repository 接口和实现
  • 完整业务代码 - Service 和 Controller
  • 可运行 Demo - 复制即可使用
  • 性能优化 - 批量写入、异步处理、连接池配置
  • 常见问题 - 连接、乱码、性能等问题解决方案

TDengine 作为国产开源时序数据库,在 IoT 场景下表现出色。通过超级表 + 子表的设计,可以轻松应对海量设备数据采集场景。

相关推荐
qq21084629531 小时前
【数据库】TDengine 清理旧数据
数据库·oracle·tdengine
郑洁文1 小时前
音乐数据分析研究与应用
大数据·数据挖掘·数据分析·音乐数据分析
成长之路5142 小时前
【实证分析】地市环境规制综合指数测算-原始数据+do代码(2011-2024年)
大数据
逸模2 小时前
AI+BIM 重构连锁公装新范式 逸模打造数字化营建核心底座
大数据·人工智能·笔记·其他·信息可视化·重构
谁似人间西林客3 小时前
工业大数据实战:看中国智造如何用数据驱动效率革命
大数据·单例模式
2501_933670793 小时前
数学成绩偏弱是否能填报大数据专业
大数据
陆水A4 小时前
【实时数仓·3】Flink多表JOIN状态爆炸——Event Time Temporal JOIN + TTL分层治理
大数据·数据仓库·数据分析·flink·数据库开发·bigdata
INGNIGHT4 小时前
Flink 的三种一致性语义
大数据·flink·linq
湘美书院--湘美谈教育4 小时前
湘美谈教育AI经验集锦:有些东西,它们很难蒸馏
大数据·人工智能·深度学习·机器学习