Flink Sliding Window 详解及代码实现:从窗口重叠到状态爆炸防控

上一篇讲了 TimeWindow,把事件时间、处理时间、滚动窗口和滑动窗口的基本用法过了一遍。这篇专门把滑动窗口(Sliding Window)拎出来深讲------它是 Flink 窗口里最容易用错、也最容易出性能问题的一种。

很多人第一次用滑动窗口,就是想做一个"最近 5 分钟的平均响应时间,每 30 秒更新一次"的监控曲线。写出来代码很简单,但跑着跑着就发现状态越来越大、Checkpoint 越来越慢、甚至直接 OOM。问题出在哪?这篇把滑动窗口的原理、内部机制、代码实现、性能优化和常见坑一次性讲透。


一、滑动窗口是什么:一个监控场景引入

先看一个真实场景。你负责一个微服务集群的监控系统,需要在大屏上展示"最近 5 分钟的平均接口响应时间",并且每 30 秒刷新一次数字。

如果用滚动窗口,5 分钟窗口意味着每 5 分钟才出一个数字,大屏上的数字 5 分钟才变一次,看起来像卡死了。你需要的是:窗口大小还是 5 分钟(看最近 5 分钟的数据),但每 30 秒就计算一次(数字频繁更新)。这就是滑动窗口。

滑动窗口有两个核心参数:

  • 窗口大小(Size):窗口覆盖的时间范围,比如 5 分钟
  • 滑动步长(Slide):每隔多久触发一次窗口计算,比如 30 秒

当滑动步长小于窗口大小时,窗口之间会重叠。一个元素可能同时属于多个窗口。比如 5 分钟窗口、30 秒滑动,一个时间戳为 10:02:30 的元素,会属于从 9:58:00 到 10:03:00 之间开始的所有 5 分钟窗口,一共 10 个窗口。

这就是滑动窗口最核心的特点:窗口重叠,元素复制,状态倍数 = 窗口大小 / 滑动步长


二、滑动窗口核心概念:Size、Slide、重叠、状态倍数

2.1 窗口大小(Size)

窗口大小决定了每个窗口覆盖多长时间范围。比如 Size = 10 分钟,每个窗口就覆盖 10 分钟的数据。窗口大小是固定的,所有窗口的大小都一样。

2.2 滑动步长(Slide)

滑动步长决定了每隔多久创建一个新窗口、触发一次计算。比如 Slide = 5 分钟,每 5 分钟就有一个新窗口开始、一个旧窗口结束触发计算。

2.3 窗口重叠

滑动步长和窗口大小的关系决定了窗口是否重叠:

  • Slide < Size:窗口重叠,一个元素属于多个窗口。这是最常用的情况,比如 10 分钟窗口、5 分钟滑动。
  • Slide = Size:窗口不重叠,等于滚动窗口。比如 5 分钟窗口、5 分钟滑动,和 TumblingEventTimeWindows.of(5 分钟) 完全一样。
  • Slide > Size:窗口之间有间隙,部分元素不属于任何窗口,会被丢弃。这种情况很少用,除非你明确需要跳过某些时间段。

2.4 状态倍数

滑动窗口下,一个元素被复制到多个窗口,每个窗口都需要独立维护状态。状态倍数 = 窗口大小 / 滑动步长

举几个例子:

  • 10 分钟窗口、5 分钟滑动:状态倍数 = 2,每个 key 同时维护 2 个窗口状态
  • 1 小时窗口、5 分钟滑动:状态倍数 = 12,每个 key 同时维护 12 个窗口状态
  • 1 小时窗口、1 分钟滑动:状态倍数 = 60,每个 key 同时维护 60 个窗口状态

状态倍数是滑动窗口性能的关键。滑动步长越小,状态倍数越大,内存占用越高,Checkpoint 越慢。后面会详细讲怎么优化。


三、滑动窗口 vs 滚动窗口:一张图看懂区别

下面这张图把滚动窗口和滑动窗口放在一起对比,同时展示了滑动窗口的时间轴可视化和状态倍数的计算。

从图里可以看到几个关键点:

第一,滚动窗口不重叠,每个元素只属于一个窗口,状态是 1 倍。滑动窗口在 Slide < Size 时重叠,一个元素属于多个窗口,状态是 Size/Slide 倍。

第二,滑动窗口的结果更平滑。滚动窗口的结果是离散的,窗口切换时数字可能突然跳变。滑动窗口因为相邻窗口共享大部分数据,结果是连续变化的,适合做趋势曲线展示。

第三,滑动步长有三种情况:小于窗口大小(重叠,最常用)、等于窗口大小(等于滚动)、大于窗口大小(有间隙,元素丢失)。除非明确需要,否则 Slide 应该 ≤ Size。


四、滑动窗口 API 及代码实现

Flink 提供了两种滑动窗口:事件时间滑动窗口(SlidingEventTimeWindows)和处理时间滑动窗口(SlidingProcessingTimeWindows)。API 很简单,两个参数:窗口大小和滑动步长。

4.1 事件时间滑动窗口:最近 5 分钟平均响应时间,每 30 秒更新

这是最常用的场景。用事件时间保证结果准确可重现,用滑动窗口实现高频更新。

java 复制代码
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;

import java.time.Duration;

public class SlidingWindowEventTimeExample {

    // 访问日志事件
    public static class AccessLog {
        public String api;        // 接口名
        public long timestamp;    // 请求时间戳(毫秒)
        public long responseTime; // 响应时间(毫秒)

        public AccessLog() {}
        public AccessLog(String api, long timestamp, long responseTime) {
            this.api = api;
            this.timestamp = timestamp;
            this.responseTime = responseTime;
        }
    }

    // 平均响应时间累加器
    public static class AvgAccumulator {
        public long sum = 0;   // 响应时间总和
        public long count = 0; // 请求次数
    }

    // 增量聚合函数:求平均响应时间
    public static class AvgResponseTimeAgg 
        implements AggregateFunction<AccessLog, AvgAccumulator, Double> {

        @Override
        public AvgAccumulator createAccumulator() {
            return new AvgAccumulator();
        }

        @Override
        public AvgAccumulator add(AccessLog value, AvgAccumulator accumulator) {
            accumulator.sum += value.responseTime;
            accumulator.count += 1;
            return accumulator;
        }

        @Override
        public Double getResult(AvgAccumulator accumulator) {
            if (accumulator.count == 0) return 0.0;
            return accumulator.sum * 1.0 / accumulator.count;
        }

        @Override
        public AvgAccumulator merge(AvgAccumulator a, AvgAccumulator b) {
            a.sum += b.sum;
            a.count += b.count;
            return a;
        }
    }

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();

        // 开启 Checkpoint,每 1 分钟一次
        env.enableCheckpointing(60000);

        // 模拟数据源:访问日志
        DataStream<AccessLog> source = env.addSource(new AccessLogSource())
            .assignTimestampsAndWatermarks(
                WatermarkStrategy.<AccessLog>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                    .withTimestampAssigner((event, timestamp) -> event.timestamp)
            );

        // 按接口名分组,滑动窗口:5 分钟窗口,30 秒滑动
        DataStream<String> result = source
            .keyBy(log -> log.api)
            .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)))
            .aggregate(new AvgResponseTimeAgg())
            .map(avg -> String.format("平均响应时间: %.2f ms", avg));

        result.print();

        env.execute("Sliding Window Event Time Example");
    }
}

这段代码有几个关键点:

第一,用 SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)) 创建滑动窗口,第一个参数是窗口大小,第二个是滑动步长。

第二,用 AggregateFunction 做增量聚合。累加器只存 (sum, count) 两个 long,状态大小与窗口内元素数量无关。如果用全量 ProcessWindowFunction,会把窗口内所有元素存到状态里,5 分钟窗口、30 秒滑动 = 10 个窗口同时存元素,很快就 OOM 了。

第三,用 forBoundedOutOfOrderness(Duration.ofSeconds(5)) 分配 Watermark,允许 5 秒乱序。事件时间窗口必须分配时间戳和 Watermark,否则窗口永远不触发。

4.2 处理时间滑动窗口:实时告警,低延迟

对延迟要求极高的场景(比如告警检测),可以用处理时间滑动窗口。不需要 Watermark,到点就触发,延迟最低。

java 复制代码
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;

public class SlidingWindowProcessingTimeExample {

    // 错误率统计
    public static class ErrorStats {
        public long totalCount;  // 总请求数
        public long errorCount;  // 错误请求数

        public ErrorStats() {}
        public ErrorStats(long total, long error) {
            this.totalCount = total;
            this.errorCount = error;
        }

        public double getErrorRate() {
            if (totalCount == 0) return 0.0;
            return errorCount * 1.0 / totalCount;
        }
    }

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();

        // 模拟数据源
        DataStream<ErrorStats> source = env.addSource(new MetricsSource());

        // 按服务名分组,处理时间滑动窗口:1 分钟窗口,10 秒滑动
        DataStream<String> alertStream = source
            .keyBy(stats -> stats.serviceName)
            .window(SlidingProcessingTimeWindows.of(Time.minutes(1), Time.seconds(10)))
            .reduce((ReduceFunction<ErrorStats>) (a, b) -> 
                new ErrorStats(a.totalCount + b.totalCount, a.errorCount + b.errorCount))
            .filter(stats -> stats.getErrorRate() > 0.05) // 错误率超过 5% 告警
            .map(stats -> String.format("告警: 错误率 %.2f%% (总请求 %d, 错误 %d)",
                stats.getErrorRate() * 100, stats.totalCount, stats.errorCount));

        alertStream.addSink(new AlertSink());

        env.execute("Sliding Window Processing Time Example");
    }
}

处理时间窗口的特点:不需要分配时间戳,不需要 Watermark,到点就触发。但结果不可重现,系统负载高时窗口内数据可能不完整。告警场景可以接受这种不精确,因为告警的目标是快速发现异常,而不是精确统计。

4.3 增量聚合 + ProcessWindowFunction:需要窗口元数据时

有时候你需要在输出中包含窗口的开始和结束时间,这时候纯增量聚合做不到(增量聚合的 getResult 拿不到窗口元数据)。Flink 提供了组合方式:增量聚合函数 + ProcessWindowFunction。

java 复制代码
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

// 增量聚合 + ProcessWindowFunction 组合
DataStream<String> result = source
    .keyBy(log -> log.api)
    .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)))
    .aggregate(
        new AvgResponseTimeAgg(),  // 增量聚合,省状态
        new ProcessWindowFunction<Double, String, String, TimeWindow>() {
            @Override
            public void process(String key, Context context, 
                                Iterable<Double> elements, Collector<String> out) {
                double avg = elements.iterator().next();
                long windowStart = context.window().getStart();
                long windowEnd = context.window().getEnd();
                out.collect(String.format("[%tT ~ %tT] %s 平均响应时间: %.2f ms",
                    windowStart, windowEnd, key, avg));
            }
        }
    );

这种组合方式两全其美:增量聚合负责省状态(状态只存累加器),ProcessWindowFunction 负责提供窗口元数据(窗口开始/结束时间)。aggregate 的第一个参数是增量聚合函数,第二个参数是 ProcessWindowFunction,它的输入是增量聚合的结果(这里是 Double),而不是全量元素。


五、滑动窗口内部机制:WindowAssigner 分配、状态管理、增量聚合

理解滑动窗口的内部机制,才能搞清楚为什么状态会爆炸、为什么必须用增量聚合。下面这张图展示了滑动窗口的完整内部机制。

5.1 WindowAssigner 分配逻辑

当一个元素到达时,SlidingEventTimeWindows.assignWindows() 方法会计算这个元素属于哪些窗口。核心逻辑是:

  1. 计算窗口数量:num = size / slide,比如 10 分钟窗口、5 分钟滑动 = 2 个窗口
  2. 计算最后一个窗口的起始时间:lastStart = timestamp - (timestamp + offset) % slide
  3. 从 lastStart 开始,往前每隔 slide 生成一个窗口,直到窗口开始时间 < timestamp - size
  4. 返回窗口列表

比如时间戳 10:07:00,10 分钟窗口、5 分钟滑动:

  • lastStart = 10:05:00(对齐到 5 分钟边界)
  • 第一个窗口:[10:05, 10:15)
  • 第二个窗口:[10:00, 10:10)
  • 元素属于这 2 个窗口

关键点:一个元素被复制到多个窗口,每个窗口独立维护状态。这就是状态倍数的来源。

5.2 状态管理

每个 (key, window) 组合独立维护一份 WindowState。状态内容取决于聚合方式:

  • 全量聚合(ProcessWindowFunction):状态存窗口内所有元素的列表。元素越多,状态越大。
  • 增量聚合(ReduceFunction/AggregateFunction):状态只存累加器(如 sum、count),与元素数量无关。

滑动窗口下,每个 key 同时存在 size/slide 个窗口状态。比如 1 小时窗口、1 分钟滑动 = 60 个窗口同时存在。如果用全量聚合,每个窗口都存所有元素,状态是滚动窗口的 60 倍,很容易 OOM。

5.3 为什么必须用增量聚合

增量聚合的原理是:元素到达时立即更新累加器,不保存原始元素。状态只存累加器(如一个 Long 或一个小对象),与窗口内元素数量无关。

比如求平均响应时间,累加器只存 (sum, count) 两个 long,16 字节。不管窗口里有 100 个元素还是 100 万个元素,状态都是 16 字节。滑动窗口下 60 个窗口也只有 960 字节,完全可控。

如果用全量聚合,每个元素存一份(假设 100 字节),100 万个元素就是 100MB,60 个窗口就是 6GB,直接 OOM。

结论:滑动窗口必须用增量聚合,这是硬性要求,不是优化建议。


六、滑动窗口触发机制

滑动窗口的触发机制和滚动窗口一样,区别只在于滑动窗口有多个窗口,每个窗口独立注册定时器、独立触发。

6.1 事件时间触发(EventTimeTrigger)

SlidingEventTimeWindows 默认使用 EventTimeTrigger。每个窗口注册一个事件时间定时器,时间 = window.maxTimestamp() = end - 1。当 Watermark 推进到 ≥ end - 1 时,定时器触发,返回 FIRE,窗口计算输出。

因为滑动窗口有多个窗口,每个窗口的 end 不同,所以定时器分别注册、分别触发。比如 10 分钟窗口、5 分钟滑动,窗口 end 分别是 10:10、10:15、10:20... 每 5 分钟有一个窗口触发。

6.2 处理时间触发(ProcessingTimeTrigger)

SlidingProcessingTimeWindows 默认使用 ProcessingTimeTrigger。每个窗口注册一个处理时间定时器,时间 = window.end。当系统当前时间 ≥ window.end 时,定时器触发。

处理时间触发不需要 Watermark,到点就触发,延迟最低。但结果不可重现,系统负载高时窗口内数据可能不完整。

6.3 触发频率

滑动窗口的触发频率 = 每滑动步长触发一次。比如 5 分钟滑动,每 5 分钟有一个窗口触发计算。窗口大小不影响触发频率,只影响每个窗口覆盖的数据范围。


七、实战案例:监控、告警、趋势分析

滑动窗口在生产环境中有很多典型应用。下面这张图总结了四大实战案例和性能优化策略。

7.1 案例一:实时监控指标

场景:监控系统展示最近 5 分钟的平均 CPU 使用率、内存使用率,每 30 秒更新一次。

配置:SlidingEventTimeWindows.of(5min, 30s),状态倍数 10 倍。

要点:用 AggregateFunction 增量聚合,累加器存 (sum, count)。10 倍状态但增量聚合后状态很小,完全可控。

7.2 案例二:实时告警检测

场景:检测最近 1 分钟内的错误率是否超过阈值,每 10 秒检测一次,快速发现异常。

配置:SlidingProcessingTimeWindows.of(1min, 10s),状态倍数 6 倍。

要点:用处理时间保证低延迟,到点就触发。用 ReduceFunction 增量聚合,累加器存 (错误数, 总数)。告警场景可接受结果不精确。

7.3 案例三:趋势分析预测

场景:分析最近 1 小时的销售额趋势,每 5 分钟更新一次,用于趋势预测和异常检测。

配置:SlidingEventTimeWindows.of(1h, 5min),状态倍数 12 倍。

要点:窗口大(1 小时),滑动频繁(5 分钟),状态较大。必须用 RocksDBStateBackend,支持大状态和增量 Checkpoint。用 AggregateFunction + ProcessWindowFunction 组合,增量聚合省状态,ProcessWindowFunction 提供窗口元数据。

7.4 案例四:数据质量评估

场景:评估最近 10 分钟的数据质量(完整率、准确率、及时率),每 1 分钟更新一次。

配置:SlidingEventTimeWindows.of(10min, 1min),状态倍数 10 倍。

要点:多指标聚合,累加器存多个指标(总数、缺失数、错误数、延迟数)。累加器较复杂,但增量聚合后状态仍然可控。每次触发输出一个质量评估报告。


八、性能优化六大策略

滑动窗口的性能优化核心是控制状态大小。以下六个策略按重要性排序:

8.1 必须用增量聚合

这是最重要的一条,没有之一。ReduceFunction 或 AggregateFunction 增量聚合,状态只存累加器,与元素数量无关。全量 ProcessWindowFunction 会存所有元素,滑动窗口下状态爆炸。

如果需要窗口元数据,用 aggregate(aggFunc, windowFunc) 组合,增量聚合 + ProcessWindowFunction,两全其美。

8.2 用 RocksDB 状态后端

大状态(窗口大、滑动小)必须用 RocksDBStateBackend。状态存在本地磁盘,支持增量 Checkpoint,内存占用可控。MemoryStateBackend 只适合小状态(状态全部在堆内存里),滑动窗口大状态下会频繁 GC 甚至 OOM。

8.3 合理设置滑动步长

滑动步长越小,窗口数越多,状态越大,计算越频繁。根据业务需求选择最大可接受的滑动步长。比如监控曲线,30 秒更新和 1 分钟更新视觉差别不大,但状态减少一半、计算频率降低一半。

不要盲目追求高频更新,先问自己:业务真的需要这么高的更新频率吗?

8.4 增量聚合 + ProcessWindowFunction 组合

需要窗口元数据(窗口开始/结束时间)时,用 aggregate(aggFunc, windowFunc) 组合。增量聚合负责省状态,ProcessWindowFunction 负责提供元数据。不要为了拿窗口元数据就退回到全量 ProcessWindowFunction。

8.5 合理配置 allowedLateness

事件时间窗口触发后,迟到数据会被丢弃。设置 allowedLateness 允许迟到数据触发窗口重新计算。但这会延长状态生命周期(窗口触发后不立即清理,等 allowedLateness 时间后才清理),增加状态大小。

根据数据迟到情况合理设置,不要设太大。如果数据基本不迟到,不设置 allowedLateness 也可以。

8.6 监控状态大小

通过 Flink Web UI 监控 State Size 指标。滑动窗口的状态应该是稳定的(窗口数固定 = size/slide)。如果状态持续增长,可能是:

  • allowedLateness 太大,窗口触发后状态没及时清理
  • 状态泄漏,某个算子的状态没正常清理
  • key 基数持续增长(比如按用户 ID 分组,新用户不断加入)

发现状态异常增长要及时排查,不要等 OOM 了才处理。


九、五大常见坑与解决方案

9.1 坑一:用全量聚合导致 OOM

现象:滑动窗口作业跑一段时间后 OOM,日志报 OutOfMemoryError: Java heap space

原因:用了 ProcessWindowFunction 全量聚合,状态存窗口内所有元素。滑动窗口下多个窗口同时存元素,状态是滚动窗口的 N 倍。

解决方案:必须用 ReduceFunctionAggregateFunction 增量聚合。需要窗口元数据时用 aggregate(aggFunc, windowFunc) 组合。

9.2 坑二:滑动步长太小状态爆炸

现象:1 天窗口、1 分钟滑动,状态巨大,Checkpoint 很慢,作业不稳定。

原因:状态倍数 = 24h / 1min = 1440 倍,即使增量聚合,1440 个窗口的累加器也不小,加上 RocksDB 的开销,状态会很大。

解决方案:根据业务需求选择合理的滑动步长。1 天窗口用 5 分钟或 10 分钟滑动就够了,状态减少 5-10 倍。不要盲目追求高频更新。

9.3 坑三:忘了分配时间戳

现象:用 SlidingEventTimeWindows,但窗口一直不触发,数据卡在状态里越来越多,最终 OOM。

原因:事件时间窗口需要 Watermark 推进才能触发。如果没调用 assignTimestampsAndWatermarks,Watermark 永远是 Long.MIN_VALUE,所有窗口都不触发,数据不断加入状态但永远不计算不清理。

解决方案:事件时间窗口必须分配时间戳和 Watermark。用 forBoundedOutOfOrderness 或自定义 WatermarkStrategy。监控 Watermark 推进情况,确保它在正常前进。

9.4 坑四:处理时间结果不可重现

现象:用 SlidingProcessingTimeWindows 做统计报表,每次跑结果都不一样,同一个时间段的数字每次都不同。

原因:处理时间窗口按系统当前时间触发,窗口内包含哪些数据取决于系统处理速度。系统负载高时,有些数据还没到窗口就触发了,结果不完整。每次跑的处理速度不同,结果就不同。

解决方案:报表、统计类场景必须用事件时间,结果可重现。处理时间只适合对延迟要求极高、可接受结果不精确的场景(如告警)。

9.5 坑五:滑动步长大于窗口大小有间隙

现象:SlidingEventTimeWindows.of(5min, 10min),有些数据莫名其妙丢失了。

原因:Slide > Size 时,窗口之间有间隙(5 分钟窗口、10 分钟滑动,窗口 [0,5)、[10,15)、[20,25)...,中间 [5,10)、[15,20) 的数据不属于任何窗口,被丢弃)。

解决方案:除非明确需要跳过某些时间段,否则 Slide 应该 ≤ Size。如果需要不重叠的窗口,用滚动窗口 TumblingEventTimeWindows 更直观。


十、总结与下一篇预告

滑动窗口是 Flink 窗口中最灵活、也最容易出问题的一种。核心要点回顾:

第一,滑动窗口有两个参数:窗口大小(Size)和滑动步长(Slide)。Slide < Size 时窗口重叠,一个元素属于多个窗口,状态倍数 = Size / Slide。

第二,滑动窗口必须用增量聚合(ReduceFunction/AggregateFunction),全量聚合会导致状态爆炸。需要窗口元数据时用增量聚合 + ProcessWindowFunction 组合。

第三,大状态必须用 RocksDBStateBackend,合理设置滑动步长,监控状态大小,及时发现异常。

第四,事件时间窗口必须分配时间戳和 Watermark,否则窗口不触发。报表类场景用事件时间保证结果可重现,告警类场景用处理时间保证低延迟。

相关推荐
jnrjian1 小时前
DRG-50857 ORA-30576 drixmd.PurgeKGL CTXSYS
数据库·oracle
志栋智能2 小时前
集成是关键:让巡检超自动化融入现有工具链
运维·服务器·数据库·架构·自动化
IvorySQL2 小时前
PostgreSQL 日报|逻辑解码竞态条件修复(9 月 20 日)
数据库·人工智能·postgresql
AI智能从业者2 小时前
数码家电客服机器人怎么选?客户问参数问题机器人能不能接住
运维·人工智能·自动化
Gl�ria3 小时前
Yarn NM 常驻Flink任务下线:stop/savepoint 释放容器
flink·yarn
捷烽3 小时前
光纤熔接机是什么?工作原理与马达结构(2026)
运维·网络·信息与通信
当下新鲜事3 小时前
空调机房水泵常见问题解答:赛莱默B&G冷冻泵与冷却泵技术说明
大数据·运维·物联网·业界资讯
adinnet20264 小时前
为什么 RAG 需要 Milvus?向量数据库到底存了什么
大数据·数据库
Elastic 中国社区官方博客4 小时前
使用 Elasticsearch 和 Jina 进行 AI 视频搜索:精准找到你需要的视频片段秒数
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索