InfluxDbTemplate使用文档

InfluxDbTemplate 使用文档

目录


简介

InfluxDbTemplate 是一个 InfluxDB 通用模板类,基于 flux-dsl 构建,提供:

  • ✅ 单条/批量写入数据
  • ✅ 基础查询和可扩展查询
  • ✅ 根据 Tag 查询(单值/多值,自动优化)
  • ✅ 多 Tag 多值组合查询
  • ✅ 超过阈值自动并行查询(阈值可配置)
  • ✅ 自动行转列(pivot)

架构设计

核心类关系图

┌─────────────────────────────────────────────────────────────────┐
│                     InfluxDbAutoConfiguration                    │
│  @Configuration                                                  │
│  @ConditionalOnProperty(prefix = "influxdb", name = "url")      │
│                                                                  │
│  负责:自动配置 InfluxDB 相关 Bean                                │
│  条件:只有配置了 influxdb.url 才会生效                           │
│                                                                  │
│  自动注入的 Bean:                                                │
│  - InfluxDBClient    (InfluxDB 客户端)                          │
│  - WriteApi          (写入 API)                                  │
│  - QueryApi          (查询 API)                                  │
│  - DeleteApi         (删除 API)                                  │
│  - ThreadPoolTaskExecutor (并行查询线程池)                       │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ 自动注入
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     InfluxDbProperties                           │
│  @ConfigurationProperties(prefix = "influxdb")                  │
│                                                                  │
│  配置项:                                                        │
│  - url          InfluxDB 地址                                   │
│  - token        认证 Token                                      │
│  - org          组织名称                                         │
│  - bucket       Bucket 名称                                     │
│  - batchSize    并行查询批次大小(默认 500)                      │
│  - readTimeout  读取超时                                         │
│  - writeTimeout 写入超时                                         │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ 依赖注入
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│               InfluxDbTemplate<Entity> (抽象类)                  │
│                                                                  │
│  功能:                                                          │
│  - add / addBatch          写入数据                             │
│  - queryByFlux             基础查询                             │
│  - queryByTag              单 Tag 查询                          │
│  - queryByTagValues        多值查询(自动优化)                   │
│  - queryByMultiTagValues   多 Tag 组合查询                       │
│  - delete                  删除数据                             │
│                                                                  │
│  特性:                                                          │
│  - 依赖通过 @Autowired 自动注入(子类无需关心)                   │
│  - 泛型类型通过反射自动获取                                       │
│  - 超过 batchSize 自动并行查询                                   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ 继承
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                  YourRepository (业务类)                         │
│  @Repository                                                     │
│  extends InfluxDbTemplate<YourEntity>                           │
│                                                                  │
│  构造方法只需传入 measurement 名称:                              │
│  public YourRepository() {                                       │
│      super("your_measurement_name");                             │
│  }                                                               │
│                                                                  │
│  可添加业务方法:                                                 │
│  - queryByIdList(...)                                           │
│  - queryByIdListSmart(...)   // 智能查询                        │
│  - ...                                                           │
└─────────────────────────────────────────────────────────────────┘

三个核心类说明

类名职责位置
InfluxDbAutoConfiguration自动配置,注册 InfluxDB 相关 Beanstarter 包
InfluxDbProperties配置属性映射(application.yml)starter 包
InfluxDbTemplate<Entity>抽象模板类,提供增删查方法starter 包

使用方式

// 1. 只需创建一个 Repository 类,继承 InfluxDbTemplate
@Repository
public class DeviceHistoryRepository extends InfluxDbTemplate<DeviceHistory> {

    // 2. 构造方法只需传入 measurement 名称
    public DeviceHistoryRepository() {
        super(DeviceHistory.MEASUREMENT);
    }

    // 3. 添加业务方法(可选)
    public List<DeviceHistory> queryByDeviceId(Instant start, Instant end, String deviceId) {
        return this.queryByTag(start, end, "deviceId", deviceId);
    }
}

无需手动注入任何依赖,InfluxDbTemplate 会自动通过 @Autowired 注入所需的:

  • WriteApi
  • QueryApi
  • DeleteApi
  • InfluxDbProperties
  • ThreadPoolTaskExecutor

快速开始

1. 添加依赖

<dependency>
    <groupId>com.tigeriot</groupId>
    <artifactId>influxdb-spring-boot3-starter</artifactId>
    <version>1.0.2</version>
</dependency>

2. 配置 application.yml

influxdb:
  url: http://localhost:8086
  token: your-token
  org: your-org
  bucket: your-bucket
  precision: ms
  read-timeout: 30s
  write-timeout: 30s
  connect-timeout: 10s
  batch-size: 500          # 并行查询批次大小(可选,默认 500)
配置项说明
配置项说明默认值
urlInfluxDB 地址必填
token认证 Token必填
org组织名称必填
bucketBucket 名称必填
precision时间精度(s/ms/us/ns)s
read-timeout读取超时10s
write-timeout写入超时10s
connect-timeout连接超时10s
batch-size并行查询批次大小,超过此数量自动分批并行500

3. 创建实体类

@Measurement(name = "device_history")
public class DeviceHistory {

    public static final String MEASUREMENT = "device_history";

    @Column(timestamp = true)
    private Instant time;

    @Column(tag = true)
    private String deviceId;

    @Column(tag = true)
    private String sensorType;

    @Column
    private Double temperature;

    @Column
    private Double humidity;

    // getter/setter...
}

4. 创建 Repository

@Repository
public class DeviceHistoryRepository extends InfluxDbTemplate<DeviceHistory> {

    public DeviceHistoryRepository() {
        // 只需传入 measurement,其他依赖自动注入,泛型类型自动获取
        super(DeviceHistory.MEASUREMENT);
    }
}

5. 使用

@Service
public class DeviceService {

    @Autowired
    private DeviceHistoryRepository repository;

    public void save(DeviceHistory history) {
        repository.add(history);
    }

    public List<DeviceHistory> query(Instant start, Instant end) {
        return repository.queryByFlux(start, end);
    }
}

查询优化原理

Flux 查询顺序最优解

from(bucket) → range(time) → filter(measurement AND tags) → [聚合] → pivot
顺序操作说明
1from(bucket)指定数据源
2range(start, end)必须紧跟 from,时间过滤优先
3filter(measurement AND tags)measurement 和 tag 放同一个 filter
4聚合操作(可选)aggregateWindow、mean、sum 等
5pivot行转列,转成实体类

为什么不能用 contains()?

// ❌ 错误:contains() 无法利用索引,全表扫描
|> filter(fn: (r) => contains(value: r["id"], set:["id1", "id2"]))

// ✅ 正确:OR 组合 equal,每个条件都能利用索引
|> filter(fn: (r) => r["id"] == "id1" or r["id"] == "id2")

多 Tag 查询顺序会影响性能吗?

答案:不会!InfluxDB 没有 MySQL 那样的"最左前缀原则"。

InfluxDB vs MySQL 索引对比
数据库索引类型最左前缀原则Tag 顺序影响
MySQL联合索引(B+树)✅ 有顺序很重要
InfluxDB每个 Tag 独立倒排索引❌ 没有顺序无所谓
原理说明
MySQL 联合索引 (a, b, c):
├── 必须先查 a,才能用 b
├── 查 b 不查 a → 索引失效
└── 最左前缀原则

InfluxDB Tag 索引:
├── Tag a → 独立倒排索引
├── Tag b → 独立倒排索引
├── Tag c → 独立倒排索引
└── 每个 Tag 都可以独立使用索引,顺序无关
示例
// 以下两种写法性能相同,InfluxDB 会自动优化
Map<String, Collection<String>> tagValuesMap = new LinkedHashMap<>();

// 写法1:先 deviceId 后 sensorType
tagValuesMap.put("deviceId", Set.of("device_001", "device_002"));
tagValuesMap.put("sensorType", Set.of("temperature"));

// 写法2:先 sensorType 后 deviceId
tagValuesMap.put("sensorType", Set.of("temperature"));
tagValuesMap.put("deviceId", Set.of("device_001", "device_002"));

// 性能一样!InfluxDB 会自动优化查询计划
总结
问题答案
Tag 顺序影响索引吗?❌ 不影响,每个 Tag 独立索引
有最左前缀原则吗?❌ 没有,和 MySQL 不同
需要关注顺序吗?❌ 不需要,InfluxDB 自动优化
什么影响性能?✅ 时间范围、Tag 值数量、是否使用聚合

基础操作

写入单条数据

DeviceHistory history = new DeviceHistory();
history.setDeviceId("device_001");
history.setTemperature(25.5);
history.setTime(Instant.now());

repository.add(history);

批量写入

List<DeviceHistory> historyList = new ArrayList<>();
// ... 添加数据
repository.addBatch(historyList);

查询操作

1. 基础时间范围查询

// 查询最近24小时
Instant start = Instant.now().minus(24, ChronoUnit.HOURS);
Instant end = Instant.now();

List<DeviceHistory> list = repository.queryByFlux(start, end);

2. 单 Tag 单值查询

// 查询指定设备
List<DeviceHistory> list = repository.queryByTag(start, end, "deviceId", "device_001");

3. 单 Tag 多值查询(自动优化)

// 查询多个设备(自动:1个用equal,多个用or,超500个并行)
Set<String> deviceIds = Set.of("device_001", "device_002", "device_003");
List<DeviceHistory> list = repository.queryByTagValues(start, end, "deviceId", deviceIds);

4. 多 Tag 多值组合查询

// 查询:(deviceId in [d1, d2]) AND (sensorType in [temp, humidity])
Map<String, Collection<String>> tagValuesMap = new LinkedHashMap<>();
tagValuesMap.put("deviceId", Set.of("device_001", "device_002"));
tagValuesMap.put("sensorType", Set.of("temperature", "humidity"));

List<DeviceHistory> list = repository.queryByMultiTagValues(start, end, tagValuesMap);

5. 自定义 Flux 查询

// 完全自定义 Flux
Flux flux = Flux.from("your-bucket")
    .range(start, end)
    .filter(Restrictions.measurement().equal("device_history"))
    .filter(Restrictions.tag("deviceId").equal("device_001"))
    .pivot(new String[]{"_time"}, new String[]{"_field"}, "_value");

List<DeviceHistory> list = repository.queryByFlux(flux);

聚合查询

⭐ 聚合分组规则(重要)

InfluxDB 聚合默认按 Series 分组,Series = Measurement + 所有 Tag 的组合

这意味着:不管你查询时传不传 Tag 条件,聚合结果都会按每个 Tag 组合独立计算!

假设实体类定义了两个 Tag:id 和 type

数据存储方式(每个 Tag 组合是一个独立的 Series):
┌─────────────────────────────────────────────────┐
│  Series 1: id=001, type=A  →  [数据点...]       │
│  Series 2: id=001, type=B  →  [数据点...]       │
│  Series 3: id=002, type=A  →  [数据点...]       │
│  Series 4: id=002, type=B  →  [数据点...]       │
└─────────────────────────────────────────────────┘

执行 aggregateWindow(20分钟, mean) 后:
→ 每个 Series 独立计算平均值,不会混在一起!
场景聚合分组方式
不传 Tag 条件按所有 Tag 组合分组(查全部设备,每个设备独立计算)
传 Tag 条件按所有 Tag 组合分组(只是数据量变少了)
想全部混在一起算需要显式 group() 取消分组
// 示例1:查询所有设备,每个设备独立计算20分钟平均值
repository.queryByFlux(start, end,
    flux -> flux.aggregateWindow(20L, ChronoUnit.MINUTES, "mean")
);
// 结果:设备A的平均值、设备B的平均值、设备C的平均值...(分开的)

// 示例2:查询指定设备,该设备独立计算20分钟平均值
repository.queryByTag(start, end, "id", "device_001",
    flux -> flux.aggregateWindow(20L, ChronoUnit.MINUTES, "mean")
);
// 结果:只有 device_001 的平均值

// 示例3:所有设备数据混在一起计算平均值(少见)
repository.queryByFlux(start, end,
    flux -> flux.group().aggregateWindow(20L, ChronoUnit.MINUTES, "mean")
);
// 结果:所有设备混合后的平均值(一个数字)

常用聚合函数

函数说明示例
mean平均值温度平均值
sum求和总流量
count计数记录数
min最小值最低温度
max最大值最高温度
first第一个值开始值
last最后一个值结束值
median中位数中间值
stddev标准差数据波动
spread极差(max-min)变化范围

1. 时间窗口聚合 - aggregateWindow

// 每10分钟取平均值
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.aggregateWindow(10L, ChronoUnit.MINUTES, "mean")
);

// 每1小时取最大值
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.aggregateWindow(1L, ChronoUnit.HOURS, "max")
);

// 每天统计数量
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.aggregateWindow(1L, ChronoUnit.DAYS, "count")
);

2. 带 Tag 条件的聚合查询

// 查询多个设备,每20分钟取平均值
Set<String> deviceIds = Set.of("device_001", "device_002");

List<DeviceHistory> list = repository.queryByTagValues(start, end, "deviceId", deviceIds,
    flux -> flux.aggregateWindow(20L, ChronoUnit.MINUTES, "mean")
);

3. 多 Tag + 聚合

Map<String, Collection<String>> tagValuesMap = new LinkedHashMap<>();
tagValuesMap.put("deviceId", Set.of("device_001", "device_002"));
tagValuesMap.put("sensorType", Set.of("temperature"));

List<DeviceHistory> list = repository.queryByMultiTagValues(start, end, tagValuesMap,
    flux -> flux.aggregateWindow(30L, ChronoUnit.MINUTES, "mean")
);

4. 降采样查询(减少数据量)

// 原始数据可能有上万条,降采样到每小时1条
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.aggregateWindow(1L, ChronoUnit.HOURS, "mean")
);

5. 获取最新数据

// 获取每个设备的最新一条数据
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.last()
);

6. 获取最早数据

// 获取每个设备的第一条数据
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.first()
);

7. 排序和限制

// 按时间降序,取前100条
List<DeviceHistory> list = repository.queryByFlux(start, end,
    flux -> flux.sort(new String[]{"_time"}, true).limit(100)
);

8. 分组说明

aggregateWindow 默认按每个设备(Tag)分组,通常不需要显式 groupBy

// 推荐:直接聚合(默认按 tag 分组)
flux -> flux.aggregateWindow(1L, ChronoUnit.HOURS, "mean")

// 特殊:取消分组,所有设备数据混在一起计算
flux -> flux.group().aggregateWindow(1L, ChronoUnit.HOURS, "mean")

// 特殊:只按某些 tag 分组(忽略其他 tag)
flux -> flux.groupBy(new String[]{"deviceId"}).aggregateWindow(1L, ChronoUnit.HOURS, "mean")

删除操作

删除时间范围内的数据

OffsetDateTime start = OffsetDateTime.now().minusDays(7);
OffsetDateTime end = OffsetDateTime.now().minusDays(1);

// 删除该 measurement 下所有数据
repository.delete(start, end);

带条件删除

// 删除指定设备的数据
repository.delete(start, end, "_measurement='device_history' AND deviceId='device_001'");

完整参数删除

repository.delete(start, end, "bucket-name", "org-name", 
    "_measurement='device_history' AND deviceId='device_001'");

高级用法

1. 并行查询原理

当查询的 Tag 值超过 500 个时,自动启用并行查询:

原始请求: 2000 个 ID
    ↓
分批: [500个] [500个] [500个] [500个]
    ↓
并行查询(使用线程池)
    ↓
合并结果

2. 批次大小说明

默认批次大小为 500,这是一个平衡值:

  • 减少分批次数,降低网络开销
  • 避免单次查询的 Flux 语句过长

3. 复杂条件组合

// 自定义复杂条件
List<DeviceHistory> list = repository.queryByFlux(start, end, flux -> {
    return flux
        .filter(Restrictions.and(
            Restrictions.tag("deviceId").equal("device_001"),
            Restrictions.column("temperature").greater(20.0)
        ))
        .aggregateWindow(10L, ChronoUnit.MINUTES, "mean");
});

4. 数据下采样策略

时间范围建议窗口说明
1小时内1分钟保持较高精度
1天内5-10分钟适中精度
1周内30分钟-1小时降低数据量
1月内1-6小时显示趋势
1年内1天长期趋势
// 根据时间范围动态选择窗口大小
public List<DeviceHistory> queryWithAutoWindow(Instant start, Instant end, String deviceId) {
    long hours = Duration.between(start, end).toHours();
    
    long windowMinutes;
    if (hours <= 1) {
        windowMinutes = 1;
    } else if (hours <= 24) {
        windowMinutes = 10;
    } else if (hours <= 168) { // 1周
        windowMinutes = 60;
    } else {
        windowMinutes = 360; // 6小时
    }
    
    return repository.queryByTag(start, end, "deviceId", deviceId,
        flux -> flux.aggregateWindow(windowMinutes, ChronoUnit.MINUTES, "mean")
    );
}

常见问题

Q1: 查询很慢怎么办?

  1. 检查是否使用了 contains():改用 queryByTagValues 方法
  2. 缩小时间范围:range 是最重要的过滤条件
  3. 使用聚合降采样:减少返回数据量
  4. 确认 Tag 是否正确:Tag 有索引,Field 没有

Q2: 查询结果为空?

  1. 检查时间范围是否正确(注意时区)
  2. 检查 measurement 名称是否匹配
  3. 检查 Tag 值是否存在
  4. 在 InfluxDB UI 中直接执行 Flux 验证

Q3: 如何查看生成的 Flux 语句?

Flux flux = Flux.from("bucket")
    .range(start, end)
    .filter(Restrictions.measurement().equal("device_history"));

System.out.println(flux.toString());
// 输出: from(bucket:"bucket") |> range(start:..., stop:...) |> filter(...)

Q4: 大量 ID 查询超时?

  1. 确保使用 queryByTagValues(自动并行)
  2. 增加线程池大小
  3. 缩小时间范围
  4. 使用聚合降采样

Q5: 时区问题?

InfluxDB 内部使用 UTC 时间,查询时注意:

// 使用 Instant(UTC)
Instant start = Instant.now().minus(24, ChronoUnit.HOURS);

// 如果有本地时间,转换为 Instant
LocalDateTime localTime = LocalDateTime.of(2024, 1, 1, 0, 0);
Instant instant = localTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant();

API 参考

方法参数返回值说明
add(entity)实体对象void写入单条
addBatch(list)实体列表void批量写入
queryByFlux(start, end)时间范围List基础查询
queryByFlux(start, end, condition)时间+条件List可扩展查询
queryByTag(start, end, tag, value)单Tag单值ListTag精确查询
queryByTagValues(start, end, tag, values)单Tag多值List自动优化查询
queryByTagValues(start, end, tag, values, condition)单Tag多值+条件List带聚合的多值查询
queryByMultiTagValues(start, end, map)多Tag多值List组合查询
queryByMultiTagValues(start, end, map, condition)多Tag多值+条件List带聚合的组合查询
delete(start, stop)时间范围void删除数据
delete(start, stop, predicate)时间+条件void条件删除

版本历史

版本日期更新内容
1.0.22024-12添加并行查询、多Tag支持、查询优化
1.0.12024-11基础功能
1.0.02024-10初始版本

附录:核心类源码

1. InfluxDbAutoConfiguration(自动配置类)

package com.tigeriot.influxdbspringboot3starter.influxdbAutoConfiguration;

import com.influxdb.annotations.Measurement;
import com.influxdb.client.*;
import com.influxdb.client.domain.WritePrecision;
import com.tigeriot.globalcommonservice.global.threadPool.DefaultThreadPoolTaskExecutorBuilder;
import io.reactivex.rxjava3.core.BackpressureOverflowStrategy;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Collections;

@ConditionalOnProperty(prefix = "influxdb",name = "url")
@EnableConfigurationProperties(InfluxDbProperties.class)
@Configuration
public class InfluxDbAutoConfiguration {

    private final InfluxDbProperties properties;

    public InfluxDbAutoConfiguration(InfluxDbProperties properties) {
        this.properties = properties;
    }

    @Bean
    public InfluxDBClient influxDBClient() {
        OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder()
                .protocols(Collections.singletonList(Protocol.HTTP_1_1))
                .readTimeout(properties.getReadTimeout())
                .writeTimeout(properties.getWriteTimeout())
                .connectTimeout(properties.getConnectTimeout());
        InfluxDBClientOptions.Builder influxBuilder = InfluxDBClientOptions.builder()
                .url(properties.getUrl())
                .bucket(properties.getBucket())
                .authenticateToken(properties.getToken().toCharArray())
                .org(properties.getOrg())
                .precision(WritePrecision.fromValue(properties.getPrecision()))
                .okHttpClient(okHttpBuilder);
        InfluxDBClientOptions build = influxBuilder.build();
        return InfluxDBClientFactory.create(build).setLogLevel(properties.getLogLevel());
    }

    @Bean
    public WriteApi writeApi(InfluxDBClient influxDBClient){
        WriteOptions options = new WriteOptions.Builder()
                .batchSize(1000)
                .flushInterval(1000)
                .jitterInterval(1000)
                .retryInterval(1000)
                .maxRetries(3)
                .maxRetryDelay(125000)
                .maxRetryTime(180000)
                .exponentialBase(2)
                .bufferLimit(10000)
                .backpressureStrategy(BackpressureOverflowStrategy.ERROR)
                .build();
        return influxDBClient.makeWriteApi(options);
    }

    @Bean
    public QueryApi queryApi(InfluxDBClient influxDBClient){
        return influxDBClient.getQueryApi();
    }

    @Bean
    public DeleteApi deleteApi(InfluxDBClient influxDBClient){
        return influxDBClient.getDeleteApi();
    }

    @Bean
    public ThreadPoolTaskExecutor influxDbThreadPoolTaskExecutor() {
        DefaultThreadPoolTaskExecutorBuilder builder = new DefaultThreadPoolTaskExecutorBuilder();
        builder.setThreadNamePrefix("influxDb-task-executor-");
        return builder.buildNew();
    }
}

2. InfluxDbProperties(配置属性类)

package com.tigeriot.influxdbspringboot3starter.influxdbAutoConfiguration;

import com.influxdb.LogLevel;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;

@Data
@ConfigurationProperties(prefix = "influxdb")
public class InfluxDbProperties {

    private static final int DEFAULT_TIMEOUT = 10_000;

    /** InfluxDB 地址 */
    private String url;
    
    /** 认证 Token */
    private String token;

    /** 组织名称 */
    private String org;

    /** Bucket 名称 */
    private String bucket;

    /** 日志级别 */
    private LogLevel logLevel = LogLevel.NONE;

    /** 读取超时 */
    private Duration readTimeout = Duration.ofMillis(DEFAULT_TIMEOUT);

    /** 写入超时 */
    private Duration writeTimeout = Duration.ofMillis(DEFAULT_TIMEOUT);

    /** 连接超时 */
    private Duration connectTimeout = Duration.ofMillis(DEFAULT_TIMEOUT);

    /** 时间精度:s/ms/us/ns */
    private String precision = "s";

    /** 
     * 每批查询的最大 Tag 值数量
     * 超过此数量将自动使用线程池并行查询
     */
    private int batchSize = 500;
}

3. InfluxDbTemplate(抽象模板类)

package com.tigeriot.influxdbspringboot3starter.template;

import com.influxdb.client.DeleteApi;
import com.influxdb.client.QueryApi;
import com.influxdb.client.WriteApi;
import com.influxdb.client.domain.WritePrecision;
import com.influxdb.query.dsl.Flux;
import com.influxdb.query.dsl.functions.FilterFlux;
import com.influxdb.query.dsl.functions.restriction.Restrictions;
import com.tigeriot.globalcommonservice.global.utils.ListUtils;
import com.tigeriot.influxdbspringboot3starter.influxdbAutoConfiguration.InfluxDbProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * InfluxDB 通用抽象模板类
 * <p>
 * 功能特性:
 * <ul>
 *     <li>单条/批量写入数据</li>
 *     <li>基础查询和可扩展查询</li>
 *     <li>单/多 Tag 查询(自动优化:1个值用equal,多个值用or)</li>
 *     <li>超过阈值(500个)自动并行查询</li>
 *     <li>自动行转列(pivot)</li>
 * </ul>
 * <p>
 * 查询优化:from(bucket) → range(time) → filter(measurement AND tags) → pivot
 * <p>
 * 详细使用文档请参考:resources/InfluxDbTemplate使用文档.md
 *
 * @param <Entity> 实体类型,需要使用 @Measurement 注解
 * @see com.influxdb.annotations.Measurement
 */
public abstract class InfluxDbTemplate<Entity> {

    /**
     * 行转列语句(Flux pivot)
     */
    public static final String ROW_TO_COLUMN_FLUX = "|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")";

    /**
     * 获取批次大小(从配置中读取)
     */
    private int getBatchSize() {
        return influxDbProperties.getBatchSize();
    }

    @Autowired
    private WriteApi writeApi;

    @Autowired
    private InfluxDbProperties influxDbProperties;

    @Autowired
    private QueryApi queryApi;

    @Autowired
    private DeleteApi deleteApi;

    @Autowired
    private ThreadPoolTaskExecutor influxDbThreadPoolTaskExecutor;

    private final String measurement;
    private final Class<Entity> entityClass;

    /**
     * 构造方法
     * <p>
     * 子类只需传入 measurement,entityClass 通过反射自动获取
     *
     * @param measurement InfluxDB measurement 名称
     */
    @SuppressWarnings("unchecked")
    public InfluxDbTemplate(String measurement) {
        this.measurement = measurement;
        // 通过反射获取泛型类型
        this.entityClass = (Class<Entity>) getGenericType();
    }

    /**
     * 获取泛型的实际类型
     */
    private Class<?> getGenericType() {
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType parameterizedType) {
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class) {
                return (Class<?>) actualTypeArguments[0];
            }
        }
        throw new IllegalStateException("无法获取泛型类型,请确保子类正确指定泛型参数");
    }

    // ==================== 写入操作 ====================

    /**
     * 写入单条数据
     *
     * @param entity 实体对象
     */
    public void add(Entity entity) {
        writeApi.writeMeasurement(WritePrecision.MS, entity);
    }

    /**
     * 批量写入数据
     *
     * @param entityList 实体列表
     */
    public void addBatch(List<Entity> entityList) {
        if (entityList == null || entityList.isEmpty()) {
            return;
        }
        writeApi.writeMeasurements(WritePrecision.MS, entityList);
    }

    // ==================== 基础查询 ====================

    /**
     * 使用自定义 Flux 查询
     *
     * @param flux Flux 查询对象
     * @return 查询结果列表
     */
    public List<Entity> queryByFlux(Flux flux) {
        return queryApi.query(flux.toString(), entityClass);
    }

    /**
     * 核心查询方法(内部使用)
     * <p>
     * 优化:将 measurement 和 tag 条件合并到同一个 filter 中,减少扫描次数
     * <pre>
     * 优化前(两个 filter):
     * |> filter(fn: (r) => r["_measurement"] == "xxx")
     * |> filter(fn: (r) => r["id"] == "yyy")
     *
     * 优化后(一个 filter):
     * |> filter(fn: (r) => r["_measurement"] == "xxx" and r["id"] == "yyy")
     * </pre>
     *
     * @param start          开始时间
     * @param end            结束时间
     * @param tagRestriction Tag 过滤条件(可为 null)
     * @param condition      自定义条件函数(如聚合,可为 null)
     * @return 查询结果列表
     */
    private List<Entity> executeQuery(Instant start, Instant end, Restrictions tagRestriction, Function<Flux, Flux> condition) {
        // 构建基础查询:from → range
        Flux flux = Flux.from(influxDbProperties.getBucket()).range(start, end);

        // 优化:将 measurement 和 tag 条件合并到同一个 filter
        Restrictions measurementRestriction = Restrictions.measurement().equal(measurement);
        Restrictions combinedRestriction = tagRestriction != null
                ? Restrictions.and(measurementRestriction, tagRestriction)
                : measurementRestriction;

        flux = ((FilterFlux) flux.filter(combinedRestriction));

        // 应用自定义条件(如聚合)
        if (condition != null) {
            flux = condition.apply(flux);
        }

        // 行转列
        flux = flux.pivot(new String[]{"_time"}, new String[]{"_field"}, "_value");

        return queryByFlux(flux);
    }

    /**
     * 按时间范围查询(自动行转列)
     *
     * @param start 开始时间
     * @param end   结束时间
     * @return 查询结果列表
     */
    public List<Entity> queryByFlux(Instant start, Instant end) {
        return queryByFlux(start, end, null);
    }

    /**
     * 按时间范围查询,支持自定义条件(自动行转列)
     * <p>
     * 查询顺序优化:from → range → filter(measurement) → [自定义条件] → pivot
     *
     * @param start     开始时间
     * @param end       结束时间
     * @param condition 自定义条件函数(可为 null)
     * @return 查询结果列表
     */
    public List<Entity> queryByFlux(Instant start, Instant end, Function<Flux, Flux> condition) {
        return executeQuery(start, end, null, condition);
    }

    // ==================== 单 Tag 查询 ====================

    /**
     * 根据单个 Tag 的单个值查询
     *
     * @param start    开始时间
     * @param end      结束时间
     * @param tagName  Tag 名称
     * @param tagValue Tag 值
     * @return 查询结果列表
     */
    public List<Entity> queryByTag(Instant start, Instant end, String tagName, String tagValue) {
        return queryByTag(start, end, tagName, tagValue, null);
    }

    /**
     * 根据单个 Tag 的单个值查询,带自定义条件
     *
     * @param start     开始时间
     * @param end       结束时间
     * @param tagName   Tag 名称
     * @param tagValue  Tag 值
     * @param condition 自定义条件函数
     * @return 查询结果列表
     */
    public List<Entity> queryByTag(Instant start, Instant end, String tagName, String tagValue, Function<Flux, Flux> condition) {
        Restrictions tagRestriction = Restrictions.tag(tagName).equal(tagValue);
        return executeQuery(start, end, tagRestriction, condition);
    }

    // ==================== 单 Tag 多值查询(自动优化) ====================

    /**
     * 根据单个 Tag 的多个值查询
     * <p>
     * 自动优化策略:
     * <ul>
     *     <li>1个值:使用 equal</li>
     *     <li>多个值:使用 OR 组合 equal</li>
     *     <li>超过 getBatchSize():并行查询后合并</li>
     * </ul>
     *
     * @param start   开始时间
     * @param end     结束时间
     * @param tagName Tag 名称
     * @param values  Tag 值集合
     * @return 查询结果列表
     */
    public List<Entity> queryByTagValues(Instant start, Instant end, String tagName, Collection<String> values) {
        return queryByTagValues(start, end, tagName, values, null);
    }

    /**
     * 根据单个 Tag 的多个值查询,带自定义条件
     *
     * @param start     开始时间
     * @param end       结束时间
     * @param tagName   Tag 名称
     * @param values    Tag 值集合
     * @param condition 自定义条件函数
     * @return 查询结果列表
     */
    public List<Entity> queryByTagValues(Instant start, Instant end, String tagName, Collection<String> values, Function<Flux, Flux> condition) {
        if (values == null || values.isEmpty()) {
            return new ArrayList<>();
        }

        List<String> valueList = new ArrayList<>(values);

        // 智能优化:1个值直接用 equal
        if (valueList.size() == 1) {
            return queryByTag(start, end, tagName, valueList.get(0), condition);
        }

        // 不超过批次大小,直接查询
        if (valueList.size() <= getBatchSize()) {
            return queryByTagValuesBatch(start, end, tagName, valueList, condition);
        }

        // 超过批次大小,并行查询
        return queryByTagValuesParallel(start, end, tagName, valueList, condition);
    }

    /**
     * 单批次查询(内部方法)
     */
    private List<Entity> queryByTagValuesBatch(Instant start, Instant end, String tagName, List<String> values, Function<Flux, Flux> condition) {
        Restrictions tagOrCondition = buildOrRestrictions(tagName, values);
        return executeQuery(start, end, tagOrCondition, condition);
    }

    /**
     * 并行查询(内部方法)
     */
    private List<Entity> queryByTagValuesParallel(Instant start, Instant end, String tagName, List<String> values, Function<Flux, Flux> condition) {
        // 分批
        List<List<String>> batches = ListUtils.divideList(values, getBatchSize());

        // 并行查询
        List<CompletableFuture<List<Entity>>> futures = batches.stream()
                .map(batch -> CompletableFuture.supplyAsync(
                        () -> queryByTagValuesBatch(start, end, tagName, batch, condition),
                        influxDbThreadPoolTaskExecutor
                ))
                .collect(Collectors.toList());

        // 等待所有任务完成并合并结果
        return futures.stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }

    // ==================== 多 Tag 多值组合查询 ====================

    /**
     * 根据多个 Tag 的多个值组合查询
     * <p>
     * 生成的查询条件:(tag1 == v1 or tag1 == v2) AND (tag2 == v3 or tag2 == v4)
     * <p>
     * 自动优化策略:
     * <ul>
     *     <li>单个值:使用 equal</li>
     *     <li>多个值:使用 OR 组合</li>
     *     <li>任意 Tag 值超过 getBatchSize():对该 Tag 并行查询</li>
     * </ul>
     *
     * @param start        开始时间
     * @param end          结束时间
     * @param tagValuesMap Tag 名称 → 值集合 的映射
     * @return 查询结果列表
     */
    public List<Entity> queryByMultiTagValues(Instant start, Instant end, Map<String, Collection<String>> tagValuesMap) {
        return queryByMultiTagValues(start, end, tagValuesMap, null);
    }

    /**
     * 根据多个 Tag 的多个值组合查询,带自定义条件
     * <p>
     * 优化策略:如果有多个 Tag 超过阈值,选择值最多的 Tag 进行分批并行查询
     *
     * @param start        开始时间
     * @param end          结束时间
     * @param tagValuesMap Tag 名称 → 值集合 的映射
     * @param condition    自定义条件函数
     * @return 查询结果列表
     */
    public List<Entity> queryByMultiTagValues(Instant start, Instant end, Map<String, Collection<String>> tagValuesMap, Function<Flux, Flux> condition) {
        if (tagValuesMap == null || tagValuesMap.isEmpty()) {
            return queryByFlux(start, end, condition);
        }

        // 找出值最多的 Tag(如果超过阈值,对其进行分批并行查询)
        String largestTagName = null;
        int largestSize = 0;

        for (Map.Entry<String, Collection<String>> entry : tagValuesMap.entrySet()) {
            if (entry.getValue() != null && entry.getValue().size() > largestSize) {
                largestSize = entry.getValue().size();
                largestTagName = entry.getKey();
            }
        }

        // 如果最大的 Tag 超过阈值,对其进行并行查询
        if (largestSize > getBatchSize()) {
            return queryByMultiTagValuesParallel(start, end, tagValuesMap, largestTagName, condition);
        }

        // 否则直接查询
        return queryByMultiTagValuesBatch(start, end, tagValuesMap, condition);
    }

    /**
     * 多 Tag 单批次查询(内部方法)
     */
    private List<Entity> queryByMultiTagValuesBatch(Instant start, Instant end, Map<String, Collection<String>> tagValuesMap, Function<Flux, Flux> condition) {
        // 构建所有 Tag 的 AND 条件
        List<Restrictions> allTagRestrictions = new ArrayList<>();

        for (Map.Entry<String, Collection<String>> entry : tagValuesMap.entrySet()) {
            String tagName = entry.getKey();
            Collection<String> values = entry.getValue();

            if (values == null || values.isEmpty()) {
                continue;
            }

            List<String> valueList = new ArrayList<>(values);
            if (valueList.size() == 1) {
                // 单个值用 equal
                allTagRestrictions.add(Restrictions.tag(tagName).equal(valueList.get(0)));
            } else {
                // 多个值用 OR
                allTagRestrictions.add(buildOrRestrictions(tagName, valueList));
            }
        }

        if (allTagRestrictions.isEmpty()) {
            return executeQuery(start, end, null, condition);
        }

        // 组合成 AND 条件
        Restrictions combinedRestriction = allTagRestrictions.size() == 1
                ? allTagRestrictions.get(0)
                : Restrictions.and(allTagRestrictions.toArray(new Restrictions[0]));

        // 使用 executeQuery,将 measurement 和所有 tag 条件合并到同一个 filter
        return executeQuery(start, end, combinedRestriction, condition);
    }

    /**
     * 多 Tag 并行查询(内部方法)
     * <p>
     * 对超大 Tag 进行分批并行查询。
     * 如果其他 Tag 也超过阈值,会递归处理(继续分批)
     */
    private List<Entity> queryByMultiTagValuesParallel(Instant start, Instant end, Map<String, Collection<String>> tagValuesMap, String largeTagName, Function<Flux, Flux> condition) {
        Collection<String> largeTagValues = tagValuesMap.get(largeTagName);
        List<List<String>> batches = ListUtils.divideList(new ArrayList<>(largeTagValues), getBatchSize());

        // 构建其他 Tag 的条件 Map
        Map<String, Collection<String>> otherTagsMap = new LinkedHashMap<>();
        for (Map.Entry<String, Collection<String>> entry : tagValuesMap.entrySet()) {
            if (!entry.getKey().equals(largeTagName)) {
                otherTagsMap.put(entry.getKey(), entry.getValue());
            }
        }

        // 并行查询(只对最大 tag 分批,不递归,减少查询次数以提高响应速度)
        List<CompletableFuture<List<Entity>>> futures = batches.stream()
                .map(batch -> {
                    // 每批次创建新的 Map
                    Map<String, Collection<String>> batchTagMap = new LinkedHashMap<>(otherTagsMap);
                    batchTagMap.put(largeTagName, batch);

                    return CompletableFuture.supplyAsync(
                            // 直接执行批次查询,不递归(避免查询次数爆炸)
                            () -> queryByMultiTagValuesBatch(start, end, batchTagMap, condition),
                            influxDbThreadPoolTaskExecutor
                    );
                })
                .collect(Collectors.toList());

        // 等待所有任务完成并合并结果
        return futures.stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }

    // ==================== 删除操作 ====================

    /**
     * 删除指定时间范围内的数据
     *
     * @param start 开始时间
     * @param stop  结束时间
     */
    public void delete(OffsetDateTime start, OffsetDateTime stop) {
        delete(start, stop, "_measurement='" + measurement + "'");
    }

    /**
     * 删除指定时间范围内符合条件的数据
     *
     * @param start     开始时间
     * @param stop      结束时间
     * @param predicate 删除条件(如:_measurement='xxx' AND tagName='value')
     */
    public void delete(OffsetDateTime start, OffsetDateTime stop, String predicate) {
        deleteApi.delete(start, stop, predicate, influxDbProperties.getBucket(), influxDbProperties.getOrg());
    }

    // ==================== 工具方法 ====================

    /**
     * 构建 OR 条件:tag == v1 or tag == v2 or tag == v3
     */
    private Restrictions buildOrRestrictions(String tagName, List<String> values) {
        if (values.size() == 1) {
            return Restrictions.tag(tagName).equal(values.get(0));
        }

        Restrictions[] restrictions = values.stream()
                .map(value -> Restrictions.tag(tagName).equal(value))
                .toArray(Restrictions[]::new);

        return Restrictions.or(restrictions);
    }
}

4. 业务 Repository 示例

package com.tigeriot.uicomponentcommon.customEcharts.historyCurveEcharts.rundevparam.influxRepository;

import com.tigeriot.globalcommonservice.model.valuemanage.field.SysFieldConst;
import com.tigeriot.influxdbspringboot3starter.template.InfluxDbTemplate;
import com.tigeriot.productmanagerserviceapiclient.model.iotdev.rundev.entity.MeasureCtlDevice;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Repository
public class RunDevParamInfluxDbRepository extends InfluxDbTemplate<MeasureCtlDevice> {

    public RunDevParamInfluxDbRepository() {
        super(MeasureCtlDevice.MEASUREMENT);
    }

    /** 查询最近24小时数据,每20分钟取平均值 */
    public List<MeasureCtlDevice> queryBy24HourMean() {
        return this.queryByFlux(
                Instant.now().minus(24, ChronoUnit.HOURS),
                Instant.now(),
                flux -> flux.aggregateWindow(20L, ChronoUnit.MINUTES, "mean")
        );
    }

    /** 根据单个 ID 查询 */
    public List<MeasureCtlDevice> queryById(Instant start, Instant end, String id) {
        return this.queryByTag(start, end, SysFieldConst.ID, id);
    }

    /** 根据多个 ID 查询(自动优化,超过阈值并行查询) */
    public List<MeasureCtlDevice> queryByIdList(Date start, Date end, Set<String> idSet) {
        return this.queryByTagValues(start.toInstant(), end.toInstant(), SysFieldConst.ID, idSet);
    }

    /** 智能查询:超过7天自动使用每小时平均值聚合 */
    public List<MeasureCtlDevice> queryByIdListSmart(Date start, Date end, Set<String> idSet) {
        Instant startInstant = start.toInstant();
        Instant endInstant = end.toInstant();
        long days = java.time.Duration.between(startInstant, endInstant).toDays();

        if (days > 7) {
            return this.queryByTagValues(startInstant, endInstant, SysFieldConst.ID, idSet,
                    flux -> flux.aggregateWindow(1L, ChronoUnit.HOURS, "mean")
            );
        } else {
            return this.queryByTagValues(startInstant, endInstant, SysFieldConst.ID, idSet);
        }
    }

    /** 根据多个 ID 查询,带聚合条件 */
    public List<MeasureCtlDevice> queryByIdListWithAggregation(Date start, Date end, Set<String> idSet,
                                                               long windowDuration, ChronoUnit unit, String aggregateFn) {
        return this.queryByTagValues(
                start.toInstant(),
                end.toInstant(),
                SysFieldConst.ID,
                idSet,
                flux -> flux.aggregateWindow(windowDuration, unit, aggregateFn)
        );
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值