influxDB基础

一、简单对比

InfluxDB 是一个由 InfluxData 开发的开源时序型数据库。使用 GO 语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据。

那么相比于关系型数据库有何优势?

1、写入性能提升
2、冷热数据分离
3、时间不可倒流,数据只写不改
二、基本概念
  1. Bucket(桶): Bucket 是 InfluxDB 中的基本存储单元,用于组织和存储时间序列数据。它是数据的逻辑容器,用于区分和隔离不同类型或来源的数据。
  2. Organization(组织): Organization 是 InfluxDB 中的组织概念,用于隔离和管理用户、应用程序或项目。每个用户可以属于一个或多个组织,Bucket 也属于一个组织。
  3. Measurement(测量值): Measurement 是 InfluxDB 中的数据表的概念。它类似于关系型数据库中的表,用于存储时间序列数据。每个 Measurement 包含一组字段和标签。
  4. Field(字段): Field 是 Measurement 中的一种数据类型,用于存储实际的数值数据。例如,温度、湿度等是字段的例子。
  5. Tag(标签): Tag 是 Measurement 中的一种索引类型,用于标识和过滤数据。标签通常用于存储维度信息,例如地理位置、设备 ID 等。
  6. Point(数据点): Point 是 InfluxDB 中时间序列数据的基本单位,它包含了时间戳、字段和标签。每个数据点都关联到一个 Measurement。
  7. API 密钥和访问令牌: 为了进行安全的访问和操作,InfluxDB 2.x 引入了 API 密钥和访问令牌。API 密钥用于身份验证,而访问令牌用于授权对数据的访问。
  8. Flux 查询语言: Flux 是 InfluxDB 2.x 中的查询语言,用于执行强大的数据处理和分析操作。它支持更灵活的查询、过滤、聚合和转换操作。
  9. 仪表板和可视化: InfluxDB 2.x 提供了仪表板和可视化工具,允许用户创建、定制和共享图表和仪表板,以直观地展示时间序列数据。
三、数据的存储
通过行协议写入数据

例如

occupancy_rate,cpu=core4 use=75,free=25

influxDB基础_第1张图片

influxDB使用序列的方式去管理数据

influxDB基础_第2张图片

图中的foodships为influxdb的measurement,相当于关系型数据库中的表

tags(图中的park_id,planet)相当于关系型数据库中的索引项

图中的#_foodships相当于关系型数据中的未建索引的列


在influxDB中,唯一的measurement、tag_set和filed(一个字段)组合是一个序列

比如tag有手机号,地块id,属性有经度、纬度、设备时间

那么该手机号地块号下的序列应该有三个,分别是经度序列,纬度序列,设备时间序列

influxDB基础_第3张图片


假如我们有这样一个场景,我们需要查询一段时间的数据,对于传统的关系型数据库来说,我们很有可能需要多次寻址找到多个元组才能完成查询,而时序数据库是吧索引打到了一批次的数据上,在这种场景下的读写,时序数据库性能是远强于B+树数据库的


四、双索引设计

现在已经说明了基本的概念和数据的存储,但是还没有提到最重要的时间。在influxdb中,时间也是索引,数据在入库时,会按照时间戳进行排序。这样,我们在进行查询时,一般遵循下面的思路

(1)先指定要从哪个存储桶查询数据

(2)指定数据的时间范围

(3)指定measurement、tag_set和field说明我要查询哪个序列

五、flux查询语言

【示例】

from(bucket: "example_java")
  |> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
  |> filter(fn: (r) => r["cpu"] == "core1")

influxDB基础_第4张图片

在上面的示例中我们看到了第一行指定了从哪个存储桶中查找,第二行指定了查询的数据的时间范围,第三行指定了查询的measurement,tag为cpu,但是因为没有过滤tag条件,我们查询出来了两条序列,而且每一行都是通过|> 符号将上一个函数的输出传递给下一个函数,类似于linux的管道

在range函数中,需要接收两个参数,start和stop,其中stop可以省略,省略的话默认为当前时间

start和stop既可以使用相对的时间区间也可以使用绝对的时间戳,在上面例子中使用的是绝对的时间戳,接下来我们将其换成相对的时间,其格式为时间间隔,如h/m/s

from(bucket: "example_java")
  |> range(start:-3d)
  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")

influxDB基础_第5张图片

我们可以添加上tag的过滤条件,指定我们想要查询的序列

from(bucket: "example_java")
  |> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
  |> filter(fn: (r) => r["cpu"] == "core1")

influxDB基础_第6张图片

这次只查询出了cpu为core1的序列


但是假如我们有多个属性,我们用这种方式查询到的会是这种形式

from(bucket: "example_java")
  |> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
  |> filter(fn: (r) => r["cpu"] == "core3")

influxDB基础_第7张图片

可以看到,现在是长数据的格式,那么如果我们想让数据变得成宽数据格式,可以使用pivot函数

from(bucket: "example_java")
  |> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
  |> filter(fn: (r) => r["cpu"] == "core3")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")

在这里插入图片描述

其中 rowKey: [“time”]指定哪些列将用来标识唯一的行,通常使用时间戳列 “_time”-可以指定多个列

columnKey: [“_field”]指定哪些列的值将被转换为新的列名 这里指定 “field”,意味着 “_field” 列中的值将变成新的列名

valueColumn: "_value"指定哪一列包含实际的数据值,这些值将填充到新生成的列中

还有一些常见的函数

min(最小值) max(最大值), mean(平均值)

例如

from(bucket: "example_java")
  |> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
  |> filter(fn: (r) => r["cpu"] == "core3")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> max(column: "free")

在这里插入图片描述

六、spring boot应用

1、引入依赖

<dependency>
	<groupId>com.influxdbgroupId>
    <artifactId>influxdb-client-javaartifactId>
    <version>7.2.0version>
dependency>

2、在配置文件中配置参数

spring:
	influxdb:
    	url: 
    	token:
    	bucket: 
    	measurement: 
    	org: 

3、配置类

@Configuration
public class InfluxConfig {


    @Value("${spring.influxdb.url}")
    private String url;
    @Value("${spring.influxdb.token}")
    private String token;
    @Value("${spring.influxdb.org}")
    private String org;
    @Value("${spring.influxdb.bucket}")
    private String bucket;
    @Bean
    public InfluxDBClient influxDBClient() {
        return InfluxDBClientFactory.create(influxDBClientOptions());
    }

    @Bean
    public InfluxDBClientOptions influxDBClientOptions() {
        return InfluxDBClientOptions.builder()
                .url(url)
                .authenticateToken(token.toCharArray())
                .org(org)
                .bucket(bucket)
                .build();
    }

    @Bean
    public WriteApi writeApi(InfluxDBClient influxDBClient) {
        // 异步写的API
        return influxDBClient.getWriteApi();
    }
    @Bean
    public QueryApi queryApi(InfluxDBClient influxDBClient){
        return influxDBClient.getQueryApi();
    }
    @Bean
    public DeleteApi deleteApi(InfluxDBClient influxDBClient){
        return influxDBClient.getDeleteApi();
    }

    public void closeInfluxDBClient() {
        if (influxDBClient() != null) {
            influxDBClient().close();
        }
    }
}
异步写入的三种方式
  • 行协议
public void test(){
    String flux = "occupancy_rate,cpu=core4 use=75,free=25";
    writeApi.writeRecord(WritePrecision.NS,flux);
}
  • 通过POJO
@Measurement(name = "occupancy_rate")
public class OccupancyRate {
    @Column(name = "use")
    private Double use;
    @Column(name = "free")
    private Double free;

    public Double getUse() {
        return use;
    }

    public void setUse(Double use) {
        this.use = use;
    }

    public Double getFree() {
        return free;
    }

    public void setFree(Double free) {
        this.free = free;
    }
}
public void insertByPOJO(@RequestBody OccupancyRate occupancyRate){
    writeApi.writeMeasurement(WritePrecision.NS,occupancyRate);
}
  • 通过Point插入
public void insertByPoint(@RequestBody OccupancyRate occupancyRate){
    Point point = Point.measurement("occupancy_rate")
            .addTag("cpu","core4")
            .addField("use",occupancyRate.getUse())
            .addField("free",occupancyRate.getFree())
            .time(Instant.now(), WritePrecision.NS);
    writeApi.writePoint(point);
}

如果想得到同步阻塞写入,可以通过下面方式获取阻塞写入API

WriteApiBlocking writeApiBlocking = influxDBClient.getWriteApiBlocking();
查询
  • 使用FluxTable
public List<OccupancyRate> query(){
    String flux = """
            from(bucket: "example_java")
              |> range(start:2024-11-02T12:30:40Z, stop:2024-11-03T00:00:00Z)
              |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
              |> filter(fn: (r) => r["cpu"] == "core4")
              |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
            """;
    List<FluxTable> fluxTables = queryApi.query(flux);
    System.out.println(fluxTables);
    List<OccupancyRate> occupancyRates = new ArrayList<>();
    fluxTables.forEach(table -> {
        table.getRecords().forEach(record -> {
            OccupancyRate occupancyRate = new OccupancyRate();
            occupancyRate.setFree(Double.valueOf(record.getValueByKey("free").toString()));
            occupancyRate.setUse(Double.valueOf(record.getValueByKey("use").toString()));
            occupancyRates.add(occupancyRate);
        });

    });
    return occupancyRates;
}
  • 返回POJO
public List<OccupancyRate> query(){
        String flux = """
                from(bucket: "example_java")
                  |> range(start:2024-11-02T12:30:40Z, stop:2024-11-03T00:00:00Z)
                  |> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
                  |> filter(fn: (r) => r["cpu"] == "core4")
                  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
                """;
        List<OccupancyRate> result = queryApi.query(flux, OccupancyRate.class);
        return  result;
    }
删除数据

InfluxDB 2.7 不支持通过字段删除数据。

支持通过以下方式删除数据

  • 时间范围
  • measurement
  • 标签

当删除请求成功完成后,被删除的数据将不再可查询,但会保留在磁盘上直到压缩服务运行

public void deleteByPhoneNumber(LocalDateTime startTime, LocalDateTime endTime) {
    //开始时间和结束时间转换为OffsetDateTime类型
    OffsetDateTime start = startTime.atOffset(OffsetDateTime.now().getOffset());
    OffsetDateTime stop = endTime.atOffset(OffsetDateTime.now().getOffset());
    //删除条件
    String predicate = "cpu=core4";
    DeletePredicateRequest request = new DeletePredicateRequest()
            .predicate(predicate)
            .start(start)
            .stop(stop);
    deleteApi.delete(request, bucket, org);
}
七、数据压缩与存储优化技术

大数据存储下要解决的问题

1、时序数据在降采样后会存在大批量的数据删除

2、单机环境存放大量数据时不能占用过多文件句柄

3、数据存储需要热备份

4、大数据场景下写吞吐量要跟得上

5、存储需要具备良好的压缩性能

数据写入的流程

influxDB基础_第8张图片

7.1 serieskey

InfluxDB中的SeriesKey的概念就是通常在时序数据库领域被称为 时间线 的概念

每个SeriesKey代表一条唯一时间线,可以追踪特定指标的历史变化

SeriesKey的组成:
measurement名称 + tag集合(经过排序的tag key-value对)

influxDB基础_第9张图片

7.2 分片(Shard)

在influxDB中,能且只能对一个Bucket指定一个Rentention Policy(保留策略),通过RP可以对指定的bucket中保存的时序数据的保留时间(duration)进行设置。一但一个bucket的duration去顶后,那么在该bucket的时序数据将会在这个duration范围内进一步按时间进行分片从而数据分成一个一个的shard为单位进行保存,每个分片是独立的TSM存储

例如:如果在新建bucket在未显式指定保留策略的情况下,默认的Duration为永久,Shard分片时间为7天

7.3 预写日志(WAL)

WAL是一种数据库常用的持久化机制:

  • 所有修改在写入实际数据前,先记录到日志文件
  • 确保系统崩溃时数据不丢失
  • 支持数据库恢复

在influxDB中存在两种WAL,一种是WriteWAL,一种是DeleteWAL

  1. WriteWAL

    • 记录写入操作
    • 数据点批量追加
    • 定期刷新到TSM
  2. DeleteWAL

    • 记录删除操作
    • 标记删除范围
    • 支持数据清理
7.4 缓存(Cache)

Cache 是 WAL 中当前存储的所有数据点的内存副本。这些点按键(即测量值、标签集和唯一字段进行组织。每个字段都按其自己的时间顺序范围保存。Cache 数据在内存中时不进行压缩。

对存储引擎的查询将合并来自缓存的数据和来自 TSM 文件的数据。查询在查询处理时对缓存中的数据副本执行。这样,查询运行时的写入不会影响结果。

(举个例子,比如我图书馆有藏书区和临时防书区,临时放书区存放刚购入的书,假如我查询图书馆某部分的图书,会将临时借书区和藏书区的图书结果合并)

发送到缓存的删除将清除给定的键或给定键的特定时间范围。

Cache 公开了一些快照行为控件。两个最重要的控件是内存限制。有一个下限,cache-snapshot-memory-size当超过该下限时,将触发 TSM 文件的快照并删除相应的 WAL 段。还有一个上限,cache-max-memory-size当超过该上限时,将导致 Cache 拒绝新的写入。这些配置有助于防止内存不足的情况,并对写入数据速度快于实例持久保存数据的客户端施加背压。每次写入时都会检查内存阈值。

比如有一个水箱,有一个下限水位,当水量达到下限水位的时候就往水箱里面注水,当水位达到上限水位后就拒绝水的进入

其他快照控制是基于时间的。空闲阈值,cache-snapshot-write-cold-duration如果在指定的时间间隔内没有收到写入,则强制缓存将快照保存到 TSM 文件。

定时执行

通过重新读取磁盘上的 WAL 文件,在重启时重新创建内存缓存。

7.5 存储引擎和时间结构合并树(TSM)

influxDB基础_第10张图片

influxDB基础_第11张图片

7.6 数据压缩
  • 快照,将缓存和WAL中的值转换成TSM文件,释放WAL段使用的内存和磁盘空间

  • 级别压缩(分为1-4级):随着TSM文件的增长,TSM 文件从快照压缩为级别 1 文件。多个级别 1 文件被压缩以生成级别 2 文件。该过程持续进行,直到文件达到级别 4(完全压缩)和 TSM 文件的最大大小。

  • 索引优化:当许多 4 级 TSM 文件累积起来时,内部索引会变得更大,访问成本也会更高。索引优化压缩会将系列和索引拆分到一组新的 TSM 文件中,将给定系列的所有点排序到一个 TSM 文件中。在索引优化之前,每个 TSM 文件都包含大多数或所有系列的点,因此每个文件都包含相同的系列索引。索引优化之后,每个 TSM 文件都包含来自最少系列的点,并且文件之间的系列重叠很少。

  • 完全压缩:当分片长时间处于写入冷状态或分片上发生删除时,将运行完全压缩(4 级压缩)。完全压缩会生成一组最佳的 TSM 文件,并包括来自级别和索引优化压缩的所有优化。一旦分片完全压缩,除非存储了新的写入或删除,否则不会在其上运行任何其他压缩。

7.7 删除操作

删除操作的执行过程

1、WAL记录要删除的measurement或series信息,确保系统崩溃后仍能够恢复删除操作

2、立即清除所有与删除操作相关的缓存中的数据

3、对于包含被删除数据的TSM文件创建对应的墓碑文件,墓碑文件记录了哪些数据需要被忽略,删除

在完全压缩发生前触发查询时

1、读取TSM文件数据

2、检查墓碑文件

3、过滤掉被标记删除的数据

4、返回结果

在压缩发生之前,删除的数据是不会从磁盘上物理删除的,等待压缩发生之后,重写TSM文件时会过滤掉删除的数据

你可能感兴趣的:(influxdb,数据库)