SpringBoot集成InfluxDB 2.x 以及常用方法封装

一、InfluxDB简介

InfluxDB 是一款高性能的时序数据库(Time Series Database, TSDB),专为处理时间戳数据的高效写入、存储和查询而设计。其核心优势在于针对时间序列数据的特性(如时间戳对齐、数据压缩、高频采样等)进行了深度优化,广泛应用于物联网(IoT)、监控系统、实时分析、金融交易等场景。

核心特性‌

1、时序数据优化‌

按时间戳自动分区存储,支持高效范围查询(如“过去1小时的数据”)。

内置数据压缩算法,显著降低存储成本(压缩比可达10:1以上)。

支持降采样(Downsampling)和连续查询(Continuous Query),减少冗余数据。

2、高性能写入与查询‌

写入吞吐量高(每秒数百万数据点),适合高频采集场景。

查询语言(Flux)支持复杂的时间序列聚合、过滤和转换。

3、灵活的数据模型‌

基于“测量(Measurement)”“标签(Tag)”“字段(Field)”和“时间戳”构建数据模型,支持多维数据组织。

标签(Tag)用于索引和分组,字段(Field)存储实际值,提升查询效率。

4、集群与高可用‌

支持分布式部署,通过分片(Shard)和副本(Replica)实现水平扩展和容灾。

提供数据一致性保证(强一致性或最终一致性)。

5、生态集成‌

兼容Telegraf(数据采集代理)、Chronograf(可视化工具)、Kapacitor(流处理引擎)等组件,形成完整时序数据栈。

支持HTTP API、Golang/Java/Python等客户端库。

典型应用场景‌

物联网(IoT)‌:传感器数据存储与分析(如温度、湿度、设备状态)。

监控系统‌:服务器性能指标(CPU、内存、网络)、应用日志聚合。

金融领域‌:股票价格、交易量、风险指标的时间序列分析。

工业互联网‌:生产线设备状态监控、预测性维护。

与传统数据库的对比‌

特性

InfluxDB

关系型数据库

数据模型

时序数据(时间戳+标签+字段

表格(行+列)

写入性能

极高(针对时间序列优化)

较低(事务开销大)

查询语言

Flux(专为时序设计)

SQL(通用但非时序优化)

存储效率

高压缩比

较低(无时序优化)

二、SpringBoot集成

1、添加依赖

在SpringBoot项目的pom.xml文件中添加以下依赖:

<dependency>
	<groupId>com.influxdb</groupId>
	<artifactId>influxdb-client-java</artifactId>
	<version>6.9.0</version>
</dependency>

2、配置InfluxDB连接

在application.xml中添加InfluxDB的连接配置信息:

spring:
  data:
    influxdb:
      url: http://127.0.0.1:8086
      token: testtoken
      org: testorg
      bucket: testbucket

3、初始化InfluxDB连接

创建一个配置类来初始化InfluxDB连接:

import com.influxdb.client.BucketsApi;
import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.domain.Bucket;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * 初始化InfluxDB配置类
 * @author 大凌青年
 * @version 1.0
 */
@Data
@Component
@ConfigurationProperties("spring.data.influxdb")
public class InfluxDBConfig {
    /**
     * 连接地址
     */
    private String url;
    /**
     * 认证token
     */
    private String token;
    /**
     *  组织
     */
    private String org;
    /**
     * 数据库
     */
    private String bucket;

    @Bean
    public InfluxDBClient influxDBClient() {
        return InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);
    }

    @PostConstruct
    public void initializeBucket() {
        InfluxDBClient client = influxDBClient();
        BucketsApi bucketsApi = client.getBucketsApi();
        // 获取组织信息以获得正确的orgID
        String orgId = client.getOrganizationsApi()
                .findOrganizations().stream()
                .filter(o -> o.getName().equals(org))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Organization not found: " + org))
                .getId();

        // 检查bucket是否存在
        Optional<Bucket> existingBucket = bucketsApi.findBuckets().stream()
                .filter(b -> b.getName().equals(bucket) && b.getOrgID().equals(orgId))
                .findFirst();

        // 如果bucket不存在,则创建
        if (!existingBucket.isPresent()) {
            // 创建Bucket对象并设置名称和orgID
            Bucket newBucket = new Bucket();
            newBucket.setName(bucket);
            newBucket.setOrgID(orgId); 
            bucketsApi.createBucket(newBucket);
        }
    }
}

三、常用方法封装

1、接口类定义:


import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.write.Point;
import com.influxdb.query.FluxRecord;
import com.influxdb.query.FluxTable;
import com.kind.ies.common.core.pojo.PaginationResult;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * InfluxDB 2.x 服务接口
 *
 * 提供对InfluxDB数据库的完整操作支持,包括数据写入、查询、聚合统计等功能
 * 主要面向时序数据处理场景,支持Flux查询语法
 *
 * @author 大凌青年
 * @version 1.0
 */
public interface InfluxDBService {

    /**
     * 获取InfluxDB客户端对象
     *
     * @return InfluxDBClient InfluxDB客户端对象
     */
    InfluxDBClient getClient();

    /**
     * 创建数据点对象,用于后续写入操作
     *
     * @param measurement 表名/测量名称,不能为空
     * @param time        时间戳,可为null(使用服务器当前时间)
     * @param tags        标签数据(key-value对),用于索引和查询过滤
     * @param fields      字段数据(key-value对),存储实际的时序数值
     * @return Point 数据点对象,可用于写入操作
     * @throws IllegalArgumentException 当measurement为空时抛出
     */
    Point createPoint(String measurement, Instant time, Map<String, String> tags, Map<String, Object> fields);

    /**
     * 写入单条数据点到默认bucket
     *
     * @param measurement 表名/测量名称
     * @param time        时间戳
     * @param tags        标签数据,用于高效查询和分组
     * @param fields      字段数据,存储实际业务数值
     */
    void writePoint(String measurement, Instant time, Map<String, String> tags, Map<String, Object> fields);

    /**
     * 写入数据点对象到默认bucket
     *
     * @param point 已构建的数据点对象,不能为null
     * @see #createPoint(String, Instant, Map, Map)
     */
    void writePoint(Point point);

    /**
     * 批量写入数据点到默认bucket
     *
     * @param points 数据点列表,不能为null
     */
    void writePoints(List<Point> points);

    /**
     * 执行自定义Flux查询语句
     *
     * @param query 完整的Flux查询语句,必须包含对bucket的引用
     * @return List<FluxTable> 查询结果表列表,每个表代表一个数据流
     * @throws com.influxdb.exceptions.InfluxException 查询执行失败时抛出
     */
    List<FluxTable> query(String query);

    /**
     * 处理查询结果,提取所有记录
     *
     * @param tables 查询结果表
     * @return List<FluxRecord> 记录列表
     */
    List<FluxRecord> getRecordsFromTables(List<FluxTable> tables);

    /**
     * 按时间范围查询指定measurement的数据
     *
     * @param measurement 表名/测量名称
     * @param start       查询开始时间(包含)
     * @param end         查询结束时间(包含)
     * @return List<FluxTable> 符合时间范围的数据结果
     */
    List<FluxTable> queryByTimeRange(String measurement, LocalDateTime start, LocalDateTime end);

    /**
     * 查询最近N分钟的数据记录
     *
     * @param measurement 表名/测量名称
     * @param minutes     查询时间范围(分钟),必须大于0
     * @return List<FluxTable> 最近N分钟内的数据记录
     * @throws IllegalArgumentException 当minutes小于等于0时抛出
     */
    List<FluxTable> queryLastMinutes(String measurement, int minutes);

    /**
     * 按标签过滤查询
     *
     * @param measurement 表名/测量名称
     * @param tags        标签过滤条件
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> queryByTags(String measurement, Map<String, String> tags);

    /**
     * 按时间窗口进行数据聚合统计
     *
     * 支持常见的聚合函数如mean(平均值)、max(最大值)、min(最小值)、sum(求和)等
     *
     * @param measurement 表名/测量名称
     * @param window      时间窗口大小,如"1h"(1小时)、"30m"(30分钟)、"1d"(1天)
     * @param aggFunction 聚合函数名称,如"mean"、"max"、"min"、"sum"
     * @param tags        标签过滤条件,可为null表示不过滤
     * @param fields      参与聚合的字段名列表,可为null表示所有字段
     * @return List<FluxTable> 聚合统计结果
     */
    List<FluxTable> queryWithAggregation(String measurement, String window, String aggFunction, Map<String, String> tags, List<String> fields);

    /**
     * 按时间范围和聚合函数查询数据
     *
     * @param measurement 表名/测量名称
     * @param tags        标签过滤条件
     * @param fields      字段名列表
     * @param aggFunction 聚合函数 (max, min, mean等)
     * @param startTime   开始时间
     * @param endTime     结束时间
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> queryAggregationWithTimeRange(String measurement, Map<String, String> tags,
                                                  List<String> fields, String aggFunction,
                                                  Instant startTime, Instant endTime);

    /**
     * 查询最新值记录
     *
     * @param measurement 表名/测量名称
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> queryLast(String measurement);

    /**
     * 查询指定字段的最新值
     *
     * @param measurement 表名/测量名称
     * @param field       字段名
     * @param tags        标签过滤条件
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> queryLastFieldsWithTags(String measurement, String field, Map<String, String> tags);

    /**
     * 查询指定字段的最新值
     *
     * @param measurement 表名/测量名称
     * @param fields      字段名列表
     * @param tags        标签过滤条件
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> queryLastFieldsWithTags(String measurement, List<String> fields, Map<String, String> tags);

    /**
     * 分页查询数据
     *
     * @param measurement 表名/测量名称
     * @param tags        tags过滤条件
     * @param fields      字段列表
     * @param page        页码(从1开始)
     * @param size        每页数量
     * @return PaginationResult<FluxTable> 查询结果
     */
    PaginationResult<FluxTable> queryWithPagination(String measurement, Map<String, String> tags, List<String> fields, int page, int size);

    /**
     * 将查询结果转换为易于处理的Map列表格式
     *
     * 自动合并具有相同时间戳和标签的记录,将多个字段整合到单个Map中
     *
     * @param tables 查询结果表列表
     * @return List<Map<String, Object>> 转换后的数据列表,每个Map代表一条记录
     *         包含_time(时间戳)、_measurement(表名)等系统字段及所有用户字段
     */
    List<Map<String, Object>> convertToMapList(List<FluxTable> tables);

    /**
     * 从查询结果中提取单一数值
     *
     * 适用于只关注特定字段最新值或聚合值的场景
     *
     * @param tables 查询结果表列表
     * @return Object 提取的单一值,当无结果时返回null
     */
    Object getSingleValue(List<FluxTable> tables);

    /**
     * 查询指定measurement的总记录数
     *
     * @param measurement 表名/测量名称
     * @return long 记录数
     */
    long count(String measurement);

    /**
     * 按时间窗口统计记录数
     *
     * @param measurement 表名/测量名称
     * @param field       字段名
     * @param range       时间范围
     * @param window      时间窗口 (如 "10m"表示10分钟)
     * @return List<FluxTable> 查询结果
     */
    List<FluxTable> countWithFieldByWindow(String measurement, String field, String range, String window);

    /**
     * 获取窗口统计结果的时间和值信息
     *
     * @param tables 查询结果表
     * @return List<Map<String, Object>> 包含时间窗口和统计值的列表
     */
    List<Map<String, Object>> getWindowCountResult(List<FluxTable> tables);

    /**
     * 按指定时间单位统计指定时间范围内某字段的数量
     *
     * 用于生成按时间维度的统计数据,如每日/每月/每年的数据量统计
     *
     * @param measurement 表名/测量名称
     * @param tags        标签过滤条件,用于筛选特定数据
     * @param field       统计的目标字段名
     * @param duration    时间跨度数量
     * @param unit        时间单位类型: "d"(天)、"M"(月)、"y"(年)
     * @return List<Map<String, Long>> 按时间单位分组的统计结果
     */
    List<Map<String, Long>> countByTimeUnit(String measurement, Map<String, String> tags, String field, Integer duration, String unit);
}

2、接口类实现:


import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.QueryApi;
import com.influxdb.client.WriteApi;
import com.influxdb.client.domain.WritePrecision;
import com.influxdb.client.write.Point;
import com.influxdb.query.FluxRecord;
import com.influxdb.query.FluxTable;
import com.kind.framework.core.db.bean.Pagination;
import com.kind.ies.common.core.pojo.PaginationResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * InfluxDB 2.x 服务实现类
 * @author 大凌青年
 * @version 1.0
 */
@Service
public class InfluxDBServiceImpl implements InfluxDBService{

    @Autowired
    private InfluxDBClient influxDBClient;

    @Value("${spring.data.influxdb.bucket}")
    private String bucket;

    /**
     * 获取InfluxDB客户端对象
     *
     * @return InfluxDBClient InfluxDB客户端对象
     */
    @Override
    public InfluxDBClient getClient() {
        return influxDBClient;
    }

    @Override
    public Point createPoint(String measurement, Instant time, Map<String, String> tags, Map<String, Object> fields) {
        Point point = Point.measurement(measurement)
                .time(time, WritePrecision.MS);

        // 添加标签
        if (tags != null) {
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                point.addTag(entry.getKey(), entry.getValue());
            }
        }

        // 添加字段
        if (fields != null) {
            for (Map.Entry<String, Object> entry : fields.entrySet()) {
                Object value = entry.getValue();
                String key = entry.getKey();

                // 根据值的类型调用相应的addField方法
                if (value instanceof String) {
                    point.addField(key, (String) value);
                } else if (value instanceof Long) {
                    point.addField(key, (Long) value);
                } else if (value instanceof Integer) {
                    point.addField(key, (Integer) value);
                } else if (value instanceof Double) {
                    point.addField(key, (Double) value);
                } else if (value instanceof Boolean) {
                    point.addField(key, (Boolean) value);
                } else if (value != null) {
                    // 对于其他类型,转换为字符串
                    point.addField(key, value.toString());
                }
            }
        }

        return point;
    }

    @Override
    public void writePoint(String measurement, Instant time, Map<String, String> tags, Map<String, Object> fields) {
        Point point = createPoint(measurement, time, tags, fields);
        writePoint(point);
    }

    @Override
    public void writePoint(Point point) {
        try (WriteApi writeApi = influxDBClient.makeWriteApi()) {
            writeApi.writePoint(point);
        }
    }

    @Override
    public void writePoints(List<Point> points) {
        try (WriteApi writeApi = influxDBClient.makeWriteApi()) {
            writeApi.writePoints(points);
        }
    }

    @Override
    public List<FluxTable> query(String query) {
        QueryApi queryApi = influxDBClient.getQueryApi();
        return queryApi.query(query);
    }

    @Override
    public List<FluxRecord> getRecordsFromTables(List<FluxTable> tables) {
        return tables.stream()
                .flatMap(table -> table.getRecords().stream())
                .collect(java.util.stream.Collectors.toList());
    }

    @Override
    public List<FluxTable> queryByTimeRange(String measurement, LocalDateTime start, LocalDateTime end) {
        String startTime = start.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
        String endTime = end.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);

        String query = String.format("from(bucket: \"%s\") " +
                        "|> range(start: %s, stop: %s) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\")",
                bucket, startTime, endTime, measurement);
        return query(query);
    }

    @Override
    public List<FluxTable> queryLastMinutes(String measurement, int minutes) {
        String query = String.format("from(bucket: \"%s\") " +
                        "|> range(start: -%dm) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\")",
                bucket, minutes, measurement);
        return query(query);
    }

    @Override
    public List<FluxTable> queryByTags(String measurement, Map<String, String> tags) {
        StringBuilder filterBuilder = new StringBuilder();
        filterBuilder.append(String.format("from(bucket: \"%s\") " +
                "|> range(start: 0) " +
                "|> filter(fn: (r) => r._measurement == \"%s\")", bucket, measurement));

        for (Map.Entry<String, String> entry : tags.entrySet()) {
            filterBuilder.append(String.format(" " +
                            "|> filter(fn: (r) => r.%s == \"%s\")",
                    entry.getKey(), entry.getValue()));
        }

        return query(filterBuilder.toString());
    }

    @Override
    public List<FluxTable> queryWithAggregation(String measurement, String window, String aggFunction,
                                                Map<String, String> tags, List<String> fields) {
        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append(String.format("from(bucket: \"%s\") " +
                        "|> range(start: 0) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\")",
                bucket, measurement));

        // 添加标签过滤条件
        if (tags != null && !tags.isEmpty()) {
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                queryBuilder.append(String.format(" " +
                                "|> filter(fn: (r) => r.%s == \"%s\")",
                        entry.getKey(), entry.getValue()));
            }
        }

        // 添加字段过滤条件
        if (fields != null && !fields.isEmpty()) {
            StringBuilder fieldCondition = new StringBuilder();
            for (int i = 0; i < fields.size(); i++) {
                if (i > 0) {
                    fieldCondition.append(" or ");
                }
                fieldCondition.append(String.format("r._field == \"%s\"", fields.get(i)));
            }
            queryBuilder.append(String.format(" |> filter(fn: (r) => %s)", fieldCondition));
        }

        // 添加数值类型过滤和转换
        queryBuilder.append(" |> filter(fn: (r) => exists r._value)");
        // 将值转换为浮点数,这样可以处理整数和浮点数,同时过滤掉无法转换的字符串
        queryBuilder.append(" |> toFloat()");
        // 过滤掉转换后为null的记录
        queryBuilder.append(" |> filter(fn: (r) => exists r._value)");

        return query(queryBuilder.toString());
    }

    @Override
    public List<FluxTable> queryAggregationWithTimeRange(String measurement, Map<String, String> tags,
                                                         List<String> fields, String aggFunction,
                                                         Instant startTime, Instant endTime) {
        // 将Instant时间转换为RFC3339格式字符串
        String startStr = startTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
        String endStr = endTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);

        // 构建基础查询
        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append(String.format("from(bucket: \"%s\") " +
                        "|> range(start: %s, stop: %s) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\")",
                bucket, startStr, endStr, measurement));

        // 添加标签过滤条件
        if (tags != null && !tags.isEmpty()) {
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                queryBuilder.append(String.format(" " +
                                "|> filter(fn: (r) => r.%s == \"%s\")",
                        entry.getKey(), entry.getValue()));
            }
        }

        // 添加字段过滤条件
        if (fields != null && !fields.isEmpty()) {
            StringBuilder fieldCondition = new StringBuilder();
            for (int i = 0; i < fields.size(); i++) {
                if (i > 0) {
                    fieldCondition.append(" or ");
                }
                fieldCondition.append(String.format("r._field == \"%s\"", fields.get(i)));
            }
            queryBuilder.append(String.format(" |> filter(fn: (r) => %s)", fieldCondition));
        }

        // 添加数值类型过滤和转换
        queryBuilder.append(" |> filter(fn: (r) => exists r._value)");
        // 将值转换为浮点数,这样可以处理整数和浮点数,同时过滤掉无法转换的字符串
        queryBuilder.append(" |> toFloat()");

        // 添加聚合函数
        queryBuilder.append(String.format(" |> %s()", aggFunction));

        // 执行查询并返回结果
        return query(queryBuilder.toString());
    }

    @Override
    public List<FluxTable> queryLast(String measurement) {
        String query = String.format("from(bucket: \"%s\") " +
                        "|> range(start: 0) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\") " +
                        "|> last()",
                bucket, measurement);
        return query(query);
    }

    @Override
    public List<FluxTable> queryLastFieldsWithTags(String measurement, String field, Map<String, String> tags) {
        return queryLastFieldsWithTags(measurement, Collections.singletonList(field), tags);
    }

    @Override
    public List<FluxTable> queryLastFieldsWithTags(String measurement, List<String> fields, Map<String, String> tags) {
        StringBuilder filterBuilder = new StringBuilder();
        filterBuilder.append(String.format("from(bucket: \"%s\") " +
                "|> range(start: 0) " +
                "|> filter(fn: (r) => r._measurement == \"%s\")", bucket, measurement));

        // 添加标签过滤条件
        for (Map.Entry<String, String> entry : tags.entrySet()) {
            filterBuilder.append(String.format(" " +
                            "|> filter(fn: (r) => r.%s == \"%s\")",
                    entry.getKey(), entry.getValue()));
        }

        // 构造字段的或关系过滤条件
        if (fields != null && !fields.isEmpty()) {
            StringBuilder fieldCondition = new StringBuilder();
            for (int i = 0; i < fields.size(); i++) {
                if (i > 0) {
                    fieldCondition.append(" or ");
                }
                fieldCondition.append(String.format("r._field == \"%s\"", fields.get(i)));
            }
            filterBuilder.append(String.format(" |> filter(fn: (r) => %s)", fieldCondition));
        }

        filterBuilder.append("|> last()");

        return query(filterBuilder.toString());
    }

    @Override
    public PaginationResult<FluxTable> queryWithPagination(String measurement, Map<String, String> tags,
                                                           List<String> fields, int page, int size) {
        // 构建基础查询
        StringBuilder baseQuery = new StringBuilder();
        baseQuery.append(String.format("from(bucket: \"%s\") " +
                        "|> range(start: 0) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\") ",
                bucket, measurement));

        // 添加标签过滤条件
        if (tags != null && !tags.isEmpty()) {
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                baseQuery.append(String.format(" " +
                                "|> filter(fn: (r) => r.%s == \"%s\")",
                        entry.getKey(), entry.getValue()));
            }
        }

        // 添加字段过滤条件
        if (fields != null && !fields.isEmpty()) {
            StringBuilder fieldCondition = new StringBuilder();
            for (int i = 0; i < fields.size(); i++) {
                if (i > 0) {
                    fieldCondition.append(" or ");
                }
                fieldCondition.append(String.format("r._field == \"%s\"", fields.get(i)));
            }
            baseQuery.append(String.format(" |> filter(fn: (r) => %s)", fieldCondition));
        }

        // 查询总数 - 使用手动计数方式
        long totalCount = 0;
        try {
            String uniqueQuery = baseQuery.toString() +
                    " |> keep(columns: [\"_time\"]) " +
                    " |> unique(column: \"_time\")";

            List<FluxTable> uniqueResult = query(uniqueQuery);

            // 手动计算唯一时间点数量
            int count = 0;
            for (FluxTable table : uniqueResult) {
                count += table.getRecords().size();
            }
            totalCount = count;
        } catch (Exception e) {
            System.err.println("Error calculating total count: " + e.getMessage());
            e.printStackTrace();
            totalCount = 0;
        }

        // 如果总数为0,直接返回空结果
        if (totalCount == 0) {
            return PaginationResult.<FluxTable>builder()
                    .data(new ArrayList<>())
                    .totalCount(0)
                    .totalPages(0)
                    .currentPage(page)
                    .pageSize(size)
                    .build();
        }

        // 第一步:获取当前页的时间点
        StringBuilder timePointsQuery = new StringBuilder(baseQuery);
        timePointsQuery.append(" |> keep(columns: [\"_time\"])")
                .append(" |> unique(column: \"_time\")")
                .append(" |> sort(columns: [\"_time\"], desc: true)");

        // 分页获取时间点
        int offset = (page - 1) * size;
        timePointsQuery.append(String.format(" |> limit(n: %d, offset: %d)", size, offset));

        List<FluxTable> timePointTables = query(timePointsQuery.toString());

        // 提取时间点
        List<Instant> timePoints = timePointTables.stream()
                .flatMap(table -> table.getRecords().stream())
                .map(FluxRecord::getTime)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());

        // 如果没有时间点,返回空结果
        if (timePoints.isEmpty()) {
            return PaginationResult.<FluxTable>builder()
                    .data(new ArrayList<>())
                    .totalCount(totalCount)
                    .totalPages((int)((totalCount + size - 1) / size))
                    .currentPage(page)
                    .pageSize(size)
                    .build();
        }

        // 第二步:根据时间点查询详细数据
        StringBuilder dataQuery = new StringBuilder(baseQuery);
        dataQuery.append(" |> sort(columns: [\"_time\"], desc: true)");

        // 构建时间点过滤条件
        StringBuilder timeFilter = new StringBuilder();
        timeFilter.append(" |> filter(fn: (r) => ");
        for (int i = 0; i < timePoints.size(); i++) {
            if (i > 0) {
                timeFilter.append(" or ");
            }
            String timeStr = timePoints.get(i).atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
            timeFilter.append(String.format("r._time == %s", timeStr));
        }
        timeFilter.append(")");
        dataQuery.append(timeFilter.toString());

        List<FluxTable> data = query(dataQuery.toString());

        return PaginationResult.<FluxTable>builder()
                .data(data)
                .totalCount(totalCount)
                .totalPages((int)((totalCount + size - 1) / size))
                .currentPage(page)
                .pageSize(size)
                .build();
    }

    @Override
    public List<Map<String, Object>> convertToMapList(List<FluxTable> tables) {
        // 按时间戳和标签分组记录
        Map<String, Map<String, Object>> resultMap = new LinkedHashMap<>();

        // 先收集所有记录并按时间排序
        List<FluxRecord> allRecords = tables.stream()
                .flatMap(table -> table.getRecords().stream())
                .sorted((r1, r2) -> r2.getTime().compareTo(r1.getTime())) // 按时间倒序排序
                .collect(Collectors.toList());

        allRecords.forEach(record -> {
            // 创建唯一键,基于measurement、时间戳和标签
            StringBuilder keyBuilder = new StringBuilder();
            keyBuilder.append(record.getMeasurement()).append("|");
            keyBuilder.append(record.getTime().toString()).append("|");

            // 添加标签到键中
            record.getValues().entrySet().stream()
                    .filter(entry -> entry.getValue() instanceof String && !entry.getKey().startsWith("_"))
                    .sorted(Map.Entry.comparingByKey())
                    .forEach(entry -> keyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append(","));

            String key = keyBuilder.toString();

            // 如果已存在相同键的Map,则添加字段;否则创建新的Map
            Map<String, Object> map = resultMap.computeIfAbsent(key, k -> {
                Map<String, Object> newMap = new LinkedHashMap<>();
                newMap.put("_measurement", record.getMeasurement());
                newMap.put("_time", record.getTime());

                // 添加标签
                record.getValues().entrySet().stream()
                        .filter(entry -> entry.getValue() instanceof String && !entry.getKey().startsWith("_"))
                        .forEach(entry -> newMap.put(entry.getKey(), entry.getValue()));

                return newMap;
            });

            // 添加字段值
            map.put(record.getField(), record.getValue());
        });

        return new ArrayList<>(resultMap.values());
    }

    @Override
    public Object getSingleValue(List<FluxTable> tables) {
        return tables.stream()
                .flatMap(table -> table.getRecords().stream())
                .findFirst()
                .map(FluxRecord::getValue)
                .orElse(null);
    }

    @Override
    public long count(String measurement) {
        String query = String.format("from(bucket: \"%s\") " +
                        "|> range(start: 0) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\") " +
                        "|> count()",
                bucket, measurement);

        List<FluxTable> result = query(query);
        return result.stream()
                .flatMap(table -> table.getRecords().stream())
                .findFirst()
                .map(record -> ((Number) record.getValue()).longValue())
                .orElse(0L);
    }

    @Override
    public List<FluxTable> countWithFieldByWindow(String measurement, String field, String range, String window) {
        String query = String.format("from(bucket: \"%s\") " +
                        "|> range(start: %s) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\") " +
                        "|> filter(fn: (r) => r._field == \"%s\") " +
                        "|> window(every: %s) " +
                        "|> count()" +
                        "|> group(columns: [\"_measurement\", \"_field\"], mode: \"except\")",
                bucket, range, measurement, field, window);
        return query(query);
    }

    @Override
    public List<Map<String, Object>> getWindowCountResult(List<FluxTable> tables) {
        List<Map<String, Object>> result = new ArrayList<>();

        for (FluxTable table : tables) {
            List<FluxRecord> records = table.getRecords();

            // 合并同一窗口的记录
            Instant windowStart = null;
            Instant windowStop = null;
            Object value = null;

            for (FluxRecord record : records) {
                // 获取窗口时间信息
                Object startObj = record.getValueByKey("_start");
                Object stopObj = record.getValueByKey("_stop");

                if (startObj instanceof Instant && windowStart == null) {
                    windowStart = (Instant) startObj;
                }

                if (stopObj instanceof Instant && windowStop == null) {
                    windowStop = (Instant) stopObj;
                }

                // 获取统计值
                if (record.getValue() != null && value == null) {
                    value = record.getValue();
                }
            }

            // 只有当有完整数据时才添加
            if (windowStart != null && value != null) {
                Map<String, Object> map = new HashMap<>();
                // 将Instant时间转换为时间戳(毫秒)
                map.put("start", windowStart.toEpochMilli());
                map.put("stop", windowStop != null ? windowStop.toEpochMilli() : null);
                map.put("value", value);
                // 使用窗口结束时间作为代表时间,并转换为时间戳
                map.put("time", windowStop != null ? windowStop.toEpochMilli() : null);
                result.add(map);
            }
        }

        return result;
    }

    @Override
    public List<Map<String, Long>> countByTimeUnit(String measurement, Map<String, String> tags, String field, Integer duration, String unit) {
        if (duration == null || duration <= 0) {
            return new ArrayList<>();
        }

        ZoneId systemZone = ZoneId.systemDefault();
        Map<String, Long> timeUnitCountMap = new LinkedHashMap<>();
        LocalDateTime now = LocalDateTime.now(systemZone);
        DateTimeFormatter formatter;
        LocalDateTime startTime;

        // 根据时间单位类型初始化时间和格式化器
        switch (unit) {
            case "1":
                formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                LocalDateTime today = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
                startTime = today.minusDays(duration - 1);
                // 初始化日期映射
                for (int i = duration - 1; i >= 0; i--) {
                    LocalDateTime targetDay = today.minusDays(i);
                    String dateStr = targetDay.format(formatter);
                    timeUnitCountMap.put(dateStr, 0L);
                }
                break;
            case "2":
                formatter = DateTimeFormatter.ofPattern("yyyy-MM");
                LocalDateTime currentMonth = now.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
                startTime = currentMonth.minusMonths(duration - 1);
                // 初始化月份映射
                for (int i = duration - 1; i >= 0; i--) {
                    LocalDateTime targetMonth = currentMonth.minusMonths(i);
                    String monthStr = targetMonth.format(formatter);
                    timeUnitCountMap.put(monthStr, 0L);
                }
                break;
            case "3":
                formatter = DateTimeFormatter.ofPattern("yyyy");
                LocalDateTime currentYear = now.withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
                startTime = currentYear.minusYears(duration - 1);
                // 初始化年份映射
                for (int i = duration - 1; i >= 0; i--) {
                    LocalDateTime targetYear = currentYear.minusYears(i);
                    String yearStr = targetYear.format(formatter);
                    timeUnitCountMap.put(yearStr, 0L);
                }
                break;
            default:
                throw new IllegalArgumentException("Unsupported time unit: " + unit);
        }

        // 构建查询时间范围
        LocalDateTime endTime = now;
        String startStr = startTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
        String endStr = endTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);

        // 构建Flux查询语句
        StringBuilder query = new StringBuilder();
        query.append(String.format("from(bucket: \"%s\") " +
                        "|> range(start: %s, stop: %s) " +
                        "|> filter(fn: (r) => r._measurement == \"%s\") " +
                        "|> filter(fn: (r) => r._field == \"%s\")",
                bucket, startStr, endStr, measurement, field));

        // 添加标签过滤条件
        if (tags != null && !tags.isEmpty()) {
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                query.append(String.format(" " +
                                "|> filter(fn: (r) => r.%s == \"%s\")",
                        entry.getKey(), entry.getValue()));
            }
        }

        try {
            List<FluxTable> queryResult = query(query.toString());

            // 手动按时间单位分组计数
            Map<String, Long> tempCountMap = new HashMap<>();

            if (queryResult != null && !queryResult.isEmpty()) {
                for (FluxTable table : queryResult) {
                    if (table != null && table.getRecords() != null) {
                        for (FluxRecord record : table.getRecords()) {
                            Instant time = record.getTime();
                            if (time != null) {
                                // 使用系统默认时区将时间转换为相应的时间单位字符串
                                LocalDateTime localDateTime = LocalDateTime.ofInstant(time, systemZone);
                                String timeUnitStr = localDateTime.format(formatter);

                                // 计数
                                tempCountMap.put(timeUnitStr, tempCountMap.getOrDefault(timeUnitStr, 0L) + 1);
                            }
                        }
                    }
                }
            }

            // 更新主映射
            for (Map.Entry<String, Long> entry : tempCountMap.entrySet()) {
                if (timeUnitCountMap.containsKey(entry.getKey())) {
                    timeUnitCountMap.put(entry.getKey(), entry.getValue());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 转换为List<Map<String, Long>>格式
        List<Map<String, Long>> result = new ArrayList<>();
        for (Map.Entry<String, Long> entry : timeUnitCountMap.entrySet()) {
            Map<String, Long> countMap = new HashMap<>();
            countMap.put(entry.getKey(), entry.getValue());
            result.add(countMap);
        }

        return result;
    }
}

### 如何在Spring Boot项目中集成和使用InfluxDB #### 集成步骤概述 为了实现Spring BootInfluxDB集成,可以采用以下方式完成配置和操作。以下是详细的说明: #### 添加依赖项 首先,在`pom.xml`文件中引入必要的Maven依赖项来支持InfluxDB客户端库。通常推荐使用的库是`influxdb-java`[^4]。 ```xml <dependency> <groupId>org.influxdb</groupId> <artifactId>influxdb-java</artifactId> <version>2.17</version> </dependency> ``` 上述代码片段展示了如何通过Maven构建工具导入所需的依赖包[^4]。 #### 创建连接实例 创建用于管理数据库交互的服务层组件时,需初始化`InfluxDB`对象并设置其基本属性,例如URL地址以及认证信息(如果适用)。下面是一个简单的例子展示如何建立到远程服务器上的链接[^5]: ```java import org.influxdb.InfluxDB; import org.influxdb.InfluxDBFactory; public class InfluxDbService { private final String influxUrl = "http://localhost:8086"; private final String username = "admin"; private final String password = "password"; public InfluxDB connectToDatabase() { return InfluxDBFactory.connect(influxUrl, username, password); } } ``` 此部分实现了基础的功能——即获取一个可用的`InfluxDB`实例以便后续调用写入或查询数据等功能[^5]。 #### 编写自定义Repository接口 对于更复杂的业务逻辑处理需求,则可以通过扩展标准JPA repository模式来自定义repository接口,并利用Spring Data框架简化开发流程。然而由于目前官方尚未提供针对Influx Time Series Database的支持版本,因此我们仍需手动编写SQL语句来进行CRUD操作[^6]。 尽管如此,仍然建议封装这些底层细节至专门的服务类当中去,从而提高程序结构清晰度及维护便利程度[^6]。 #### 数据模型设计 当涉及到时间序列数据分析场景下,合理规划实体关系显得尤为重要。考虑到这一点之后再决定是否有必要映射具体的Java Bean类对应每张表字段还是仅仅传递原始JSON字符串形式给API端点消费即可满足实际应用中的大部分情况[^7]。 例如,假设我们要记录传感器读数随时间变化的趋势图谱的话,那么可能只需要关心几个核心维度参数就足够了: ```java @Data @AllArgsConstructor @NoArgsConstructor @Builder(toString = false) public class SensorReading { @JsonProperty("time") private LocalDateTime timestamp; @JsonProperty("sensor_id") private int sensorId; @JsonProperty("value") private double value; } ``` 这里定义了一个名为SensorReading的数据传输对象DTO用来表示单次测量的结果集[^7]。 #### 实现服务功能 最后一步就是把前面提到的所有要素结合起来形成完整的解决方案啦!比如我们可以新增加这样一个方法负责向指定measurement里批量追加新纪录条目[^8]: ```java @Service public class MeasurementService { private static final String MEASUREMENT_NAME = "sensor_readings"; private final InfluxDB influxDB; public MeasurementService(InfluxDB influxDB){ this.influxDB=influxDB; } public void saveBatch(List<SensorReading> readingsList) throws Exception{ BatchPoints batchPoints=BatchPoints.database("mydb").build(); for(SensorReading reading :readingsList ){ Point point=Point.measurement(MEASUREMENT_NAME). time(reading.getTimestamp(), TimeUnit.MILLISECONDS).addField("sensor_id",reading.getSensorId()) .addField("value",reading.getValue()).build(); batchPoints.point(point); } influxDB.write(batchPoints); } } ``` 以上便是关于怎样将InfluxDB融入现有的Spring Boot应用程序环境下的全过程介绍完毕[^8]!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大凌青年

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

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

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

打赏作者

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

抵扣说明:

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

余额充值