Flink之Watermark源码解析

news2024/9/28 7:17:32

1. WaterMark源码分析

在Flink官网中介绍watermark和数据是异步处理的,通过分析源码得知这个说法不够准确或者说不够详细,这个异步处理要分为两种情况:
  • watermark源头
  • watermark下游
这两种情况的处理方式并不相同,在watermark的源头确实是异步处理的,但是在下游只是做的判断,这里会结合源码进行说明.
  • 代码

    public class FlinkWaterMark {
        public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
            env.getConfig().setAutoWatermarkInterval(1000);
            // 获取Socket数据源
            DataStreamSource<String> socketSource = env.socketTextStream("localhost", 8888);
            // 将数据转换成UserEvent2
            SingleOutputStreamOperator<UserEvent2> mapStream = socketSource.map(s -> {
                String[] split = s.split(",");
                UserEvent2 userEvent2 = UserEvent2.builder()
                        .uId(split[0])
                        .name(split[1])
                        .event(split[2])
                        .time(split[3])
                        .build();
                return userEvent2;
            }).returns(UserEvent2.class).disableChaining(); // 这里做算子链的解绑
            // 构造Watermark策略,使用允许时间乱序策略,并设置允许时间乱序时间最大值为2000ms
            WatermarkStrategy<UserEvent2> watermark = WatermarkStrategy
                    .<UserEvent2>forBoundedOutOfOrderness(Duration.ofMillis(2000)) // 设置乱序时间
                    .withTimestampAssigner(new SerializableTimestampAssigner<UserEvent2>() {
                        @Override
                        public long extractTimestamp(UserEvent2 userEvent2, long l) {
                            /** 抽取事件时间逻辑, 根据数据中的实际情况来看 **/
                            String time = userEvent2.getTime();
                            // 将事件中携带的时间转换成毫秒值
                            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
                            LocalDateTime parse = LocalDateTime.parse(time, dateTimeFormatter);
                            Instant instant = parse.atZone(ZoneId.systemDefault()).toInstant();
                            long timestamp = instant.toEpochMilli();
                            return timestamp;
                        }
                    });
            // 将构造完成的watermark分配给数据流
            SingleOutputStreamOperator<UserEvent2> mapStream2 = mapStream.assignTimestampsAndWatermarks(watermark);
    
            // 通过process算子打印watermark信息
            mapStream2.process(new ProcessFunction<UserEvent2, UserEvent2>() {
                @Override
                public void processElement(UserEvent2 value, ProcessFunction<UserEvent2, UserEvent2>.Context ctx, Collector<UserEvent2> out) throws Exception {
                    // 获取水位线
                    long l = ctx.timerService().currentWatermark();
                    System.out.println("当前watermark: " + l);
                    // 直接输出数据
                    out.collect(value);
                }
            }).startNewChain().print(); // 这里要注意,一定要新开一个算子链,如果watermark和process算子绑定到一个算子链中就不会形成上下游的关系,后续的一些实验也就无法验证
    
            env.execute();
        }
    }
    

    下面将会围绕上面这段业务代码对watermark源码进行解析,在进行解析前说一个小技巧:当对某个框架进行源码解析时,最好是通过Debug的方式进行,就以Flink为例,如果直接通过点击查看源码是很难进行追溯的,代码执行过程中所用到的很多类通过点击的方式是查看不到的.

1.1 watermark源头源码分析
  1. 通过forBoundedOutOfOrderness(Duration.ofMillis(2000))方法点进源码,再通过源码进入到BoundedOutOfOrdernessWatermarks类中,对onEvent方法和onPeriodicEmit方法中的方法体中的内容打上断点

        @Override
        public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
            maxTimestamp = Math.max(maxTimestamp, eventTimestamp); // 断点位置
        }
    
        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1)); // 断点位置
        }
    
    • onEvent

      onEvent方法其实就是筛选最大时间的通过maxTimestampeventTimestamp进行比较来判断是否根据事件时间更新maxTimestamp.

    • onPeriodicEmit

      onPeriodicEmit方法根据名称就可以看出是周期性发射,而发射的内容就是watermark,通过方法体可以看到每次发射的watermark都是maxTimestamp - outOfOrdernessMillis - 1,maxTimestamp就是上面onEvent方法获取的最大时间戳,outOfOrdernessMillis就是在调用forBoundedOutOfOrderness(Duration.ofMillis(2000))这个方法时所给的2000的时间容错,1则是恒定减1.

  2. 通过Debug执行代码,在Debugger中是可以看到代码执行过程所调用的源码依赖顺序,如下图
    在这里插入图片描述
    通过上面的图片可以很清晰的看到代码执行过程类的调用顺序,这里不要关注每一个类的源码内容,可以直接点击onProcessingTime这个方法,在图片中就是第二行中的方法,点击这个方法后就可以定位到TimestampsAndWatermarksOperator这个类.

  3. TimestampsAndWatermarksOperator关注点

    TimestampsAndWatermarksOperator这个类中只需要关注三个方法即可openprocessElementonProcessingTime

    public class TimestampsAndWatermarksOperator<T> extends AbstractStreamOperator<T>
            implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {
        // ...
    
        // 生命周期方法,初始化各种信息
        @Override
        public void open() throws Exception {
            super.open();
    
            timestampAssigner = watermarkStrategy.createTimestampAssigner(this::getMetricGroup);
            watermarkGenerator =
                    emitProgressiveWatermarks
                            ? watermarkStrategy.createWatermarkGenerator(this::getMetricGroup)
                            : new NoWatermarksGenerator<>();
    
            wmOutput = new WatermarkEmitter(output);
            // 根据配置获取watermark发送间隔,默认200ms
            watermarkInterval = getExecutionConfig().getAutoWatermarkInterval(); 
            if (watermarkInterval > 0 && emitProgressiveWatermarks) {
                final long now = getProcessingTimeService().getCurrentProcessingTime();
                // 开启定时(200ms),执行onProcessingTime(long timestamp)方法
                getProcessingTimeService().registerTimer(now + watermarkInterval, this);
            }
        }
    
        // 由数据驱动的方法,只有数据进来时才执行这个方法
        @Override
        public void processElement(final StreamRecord<T> element) throws Exception {
            final T event = element.getValue();
            final long previousTimestamp =
                    element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE;
            final long newTimestamp = timestampAssigner.extractTimestamp(event, previousTimestamp);
    
            element.setTimestamp(newTimestamp);
            // 发送数据
            output.collect(element);
            // 根据BoundedOutOfOrdernessWatermarks类中的onEvent方法修改maxTimestamp
            watermarkGenerator.onEvent(event, newTimestamp, wmOutput);
        }
    
        // watermark定时触发器方法
        @Override
        public void onProcessingTime(long timestamp) throws Exception {
            // 执行BoundedOutOfOrdernessWatermarks类中的onPeriodicEmit方法,发射watermark
            watermarkGenerator.onPeriodicEmit(wmOutput);
    
            final long now = getProcessingTimeService().getCurrentProcessingTime();
            // 重新定时,并在定时时间到达时再次触发onProcessingTime方法
            getProcessingTimeService().registerTimer(now + watermarkInterval, this);
        }
        // ...
    }
    
    • open

      open方法就是一个线程的生命周期方法,在这个方法里面会加载各种初始化信息,其中就包含了多久发送一次watermark的信息,就是watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();这行代码,watermark的默认发送周期是200ms,这个是可以在代码中进行配置的如env.getConfig().setAutoWatermarkInterval(1000);这样就将默认的200ms修改成了1000ms,在获取到watermark发送周期后,就会启动定时器getProcessingTimeService().registerTimer(now + watermarkInterval, this)这里就第一次执行了定时器,其实就是执行的onProcessingTime方法.

    • processElement
      processElement方法只有在有数据流入的时候才会执行,这个方法中只需要关注两行代码即可output.collect(element);watermarkGenerator.onEvent(event, newTimestamp, wmOutput);在源码中可以看到,首先执行的就是output.collect(element);也就是发送数据,然后才执行的watermarkGenerator.onEvent(event, newTimestamp, wmOutput);也就是根据事件时间确定watermark,其实由这两行代码就可以看出Flink的机制其实就是数据优先,由数据驱动时间

    • onProcessingTime

      onProcessingTime方法就是watermark的定时触发器,代码中的内容也极其简单,首先就是调用BoundedOutOfOrdernessWatermarks类中的onPeriodicEmit方法,将watermark发射,然后再次通过getProcessingTimeService().registerTimer(now + watermarkInterval, this);执行定时器,可以看做类似于循环或者是递归,不断的启动定时器不断地发射watermark,这个跟是否有数据进来没有关系,这样watermark就会根据设定好的周期不断地进行发送,看到这里就说一点在watermark的源头数据本身和watermark就是异步执行的互不影响.

  4. 将源码内容拷贝到项目中,通过添加打印控制台的代码查看执行过程

    只看源码其实很难弄清楚过程到底是怎么执行的,到底是谁先谁后,这里就可以通过将源码拷贝到项目中并且根据自己的需求添加对应的代码的这种小技巧来搞清楚代码的具体执行逻辑,方法很简单首先在IDEA中查看对应的源码类,然后拷贝该类的全路径,在项目下新建一个一模一样的类即可,这样代码再执行的过程中就会优先调用项目中相同的类,这里以BoundedOutOfOrdernessWatermarksTimestampsAndWatermarksOperator为例.

    • BoundedOutOfOrdernessWatermarks

      右键BoundedOutOfOrdernessWatermarks --> Copy Reference --> 在项目的Java目录下右键 --> 新建Java Class --> 将全路径粘贴 --> 点击OK --> 再将源码中的代码全部粘贴到新建的类中即可 --> 根据自己需要添加代码

      代码

      package org.apache.flink.api.common.eventtime;
      
      import org.apache.flink.annotation.Public;
      
      import java.time.Duration;
      
      import static org.apache.flink.util.Preconditions.checkArgument;
      import static org.apache.flink.util.Preconditions.checkNotNull;
      public class BoundedOutOfOrdernessWatermarks<T> implements WatermarkGenerator<T> {
          // ...
      
          @Override
          public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
              maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
              // 在这里添加打印信息
              System.out.println(maxTimestamp);
          }
      
          @Override
          public void onPeriodicEmit(WatermarkOutput output) {
              // 在这里添加打印信息
              System.out.printf("周期性输出watermark: %d \n", maxTimestamp - outOfOrdernessMillis - 1);
              output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
          }
      }
      

      代码中部分内容省略.

    • TimestampsAndWatermarksOperator

      步骤同上,这里省略

      代码

      package org.apache.flink.streaming.runtime.operators;
      // ...
      public class TimestampsAndWatermarksOperator<T> extends AbstractStreamOperator<T>
              implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {
          // ...
          @Override
          public void open() throws Exception {
              super.open();
      
              timestampAssigner = watermarkStrategy.createTimestampAssigner(this::getMetricGroup);
              watermarkGenerator =
                      emitProgressiveWatermarks
                              ? watermarkStrategy.createWatermarkGenerator(this::getMetricGroup)
                              : new NoWatermarksGenerator<>();
      
              wmOutput = new WatermarkEmitter(output);
      
              watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();
              if (watermarkInterval > 0 && emitProgressiveWatermarks) {
                  final long now = getProcessingTimeService().getCurrentProcessingTime();
                  // 添加打印信息
                  System.out.println("第一次执行定时器");
                  getProcessingTimeService().registerTimer(now + watermarkInterval, this);
              }
          }
      
          @Override
          public void processElement(final StreamRecord<T> element) throws Exception {
              final T event = element.getValue();
              final long previousTimestamp =
                      element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE;
              final long newTimestamp = timestampAssigner.extractTimestamp(event, previousTimestamp);
      
              element.setTimestamp(newTimestamp);
              // 添加打印信息
              System.out.printf("准备发送数据: %s \n", element);
              output.collect(element);
              // 添加打印信息
              System.out.print("根据onEvent方法修改了maxTimestamp: ");
              watermarkGenerator.onEvent(event, newTimestamp, wmOutput);
          }
      
          @Override
          public void onProcessingTime(long timestamp) throws Exception {
              // 添加打印信息
              System.out.println("watermark定时器触发,开始调用onPeriodicEmit方法");
              watermarkGenerator.onPeriodicEmit(wmOutput);
      
              final long now = getProcessingTimeService().getCurrentProcessingTime();
              getProcessingTimeService().registerTimer(now + watermarkInterval, this);
          }
          // ...
      }
      
  5. 执行代码查看结果

    • 无数据进入

      [2023-10-09 09:19:57,219]-[INFO] -org.apache.flink.streaming.api.functions.source.SocketTextStreamFunction -2015 -org.apache.flink.streaming.api.functions.source.SocketTextStreamFunction.run(SocketTextStreamFunction.java:103).run(103) | Connecting to server socket localhost:8888
      第一次启动定时器
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      

      通过结果可以看出open方法只有开始的时执行了一次,后面就是一直在执行onProcessingTime方法,不断的启动定时器,然后发送watermark,而且也可以看出就算没有数据到达watermark也是根据定时周期不断地发送,数据能影响的只是watermark的值,但是不会影响watermark的发送.

    • 第一条数据到达

      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      准备发送数据: Record @ 1696648332245 : UserEvent2(uId=101, name=Tom, event=查看商品, time=2023-10-07 11:12:12.245) 
      根据onEvent方法修改了maxTimestamp: 1696648332245
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      

      当第一条数据到达后可以看到,优先执行的数据处理的方法,当数据处理的方法执行完成后,才会变更对应的maxTimestamp,然后再通过定时触发器发射watermark.

1.2 watermark下游源码分析
  1. process算子中打上断点

            mapStream2.process(new ProcessFunction<UserEvent2, UserEvent2>() {
                @Override
                public void processElement(UserEvent2 value, ProcessFunction<UserEvent2, UserEvent2>.Context ctx, Collector<UserEvent2> out) throws Exception {
    
                    out.collect(value); // 断点位置
                }
            }).print();
    
  2. 通过Debug运行,以同样的方式找到代码执行过程用到的调用栈,如下图
    在这里插入图片描述
    找到AbstractStreamTaskNetworkInput类的processElement方法

  3. processElement方法体分析

    源码内容如下:

    public abstract class AbstractStreamTaskNetworkInput<
                    T, R extends RecordDeserializer<DeserializationDelegate<StreamElement>>>
            implements StreamTaskInput<T> {
        // ..
        private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {
            if (recordOrMark.isRecord()) { // 判断是否是用户数据
                output.emitRecord(recordOrMark.asRecord());
            } else if (recordOrMark.isWatermark()) { // 判断是否是水位线数据
                statusWatermarkValve.inputWatermark(
                        recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);
            } else if (recordOrMark.isLatencyMarker()) { // 判断是否是标记性水位线数据(用以兼容以前的版本[Flink-1.12以前的版本])
                output.emitLatencyMarker(recordOrMark.asLatencyMarker());
            } else if (recordOrMark.isWatermarkStatus()) { // 判断是否是水位线状态数据
                statusWatermarkValve.inputWatermarkStatus(
                        recordOrMark.asWatermarkStatus(),
                        flattenedChannelIndices.get(lastChannel),
                        output);
            } else {
                throw new 8 ("Unknown type of StreamElement");
            }
        }
        // ...
    }
    

    源码省略了部分代码,这里只看需要的主体代码, 在processElement方法体中可以看出watermark下游的算子当数据到达后首先会判断是什么类型的数据,然后做不同的处理,这里就和watermark的处理方式不同了,这里是指在一个方法体中做了一个if else判断,然后将数据发往不同的分支.

  4. 通过同样的方式,将源码拷贝到项目中,并以打印到控制台的方式修改代码

    代码:

    public abstract class AbstractStreamTaskNetworkInput<
                    T, R extends RecordDeserializer<DeserializationDelegate<StreamElement>>>
            implements StreamTaskInput<T> {
        // ..
        private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {
            if (recordOrMark.isRecord()) {
                // 打印控制台
                System.out.println("下游process算子开始发送数据: " + recordOrMark);
                output.emitRecord(recordOrMark.asRecord());
            } else if (recordOrMark.isWatermark()) {
                // 打印控制台
                System.out.println("下游process算子开始发送watermark: " + recordOrMark);
                statusWatermarkValve.inputWatermark(
                        recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);
            } else if (recordOrMark.isLatencyMarker()) {
                output.emitLatencyMarker(recordOrMark.asLatencyMarker());
            } else if (recordOrMark.isWatermarkStatus()) {
                statusWatermarkValve.inputWatermarkStatus(
                        recordOrMark.asWatermarkStatus(),
                        flattenedChannelIndices.get(lastChannel),
                        output);
            } else {
                throw new UnsupportedOperationException("Unknown type of StreamElement");
            }
        }
        // ...
    }
    

    结果:

    # watermark源头开始打印信息
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: -9223372036854775808 
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: -9223372036854775808
    
    # 这里不需要管重复的信息,重复只是因为方法被多次调用,从这里需要关注watermark和数据的先后关系
    下游process算子开始发送数据: Record @ (undef) : 102,Jack,添加购物车,2023-10-07 11:12:14.209
    下游process算子开始发送数据: Record @ (undef) : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)
    准备发送数据: Record @ 1696648334209 : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209) 
    
    # 这里将源头的watermark修改成了事件中的时间,要注意这里只是修改了maxTimestamp但是还没有更新watermark
    根据onEvent方法修改了maxTimestamp: 1696648334209
    # 开始发送数据
    下游process算子开始发送数据: Record @ 1696648334209 : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)
    # 这里是在procees算子中打印的水位线信息,可以看到是第一条数据到达之前的watermark
    当前watermark: -9223372036854775808
    # print sink打印的数据
    UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)
    
    # 到这里就要注意,这个时候才更新watermark信息,这也就说明,一个问题随着数据达到process算子的watermark其实就是更新之前的watermark,只有当新的数据到达后,才会再次将watermark更新为前一条数据的中的事件时间
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208
    
    # 下游刚刚获取到新的watermark
    下游process算子开始发送watermark: Watermark @ 1696648332208
    
    # 在没有新数据到达时,process算子不会被触发,一直打印watermark源头的信息,虽然源头一直发送watermark,但是只有来数据时,process才会触发,而新数据到达时,proces触发拿到的其实就是上一条的watermark
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208 
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208
    

    具体信息在注释中写明,这里不做过多阐释.

1.3 watermark整体流程图

流程图的说明也是结合本章中的代码进行说明,如下图:
在这里插入图片描述

  1. 当程序刚启动时每个并行度都会执行open方法且只执行一次,这个时候的watermarkLong.MIN_VALUE + outOfOrdernessMillis + 1;Long类型的最小值 + 设置的容错时间(forBoundedOutOfOrderness(Duration.ofMillis(2000))) + 1,然后第一次启动定时器,定时器启动的周期则是通过读取配置获得,默认200ms.
  2. open执行后启动定时器,也就是onProcessingTime方法,在业务数据到达之前,定时器会根据watermark定时周期不断地自我触发,一直向下游发送watermark.
  3. 当第一条数据到达后首先会执行processElement方法,先将业务数据发送后再执行onEvent方法.
  4. onEvent方法获取maxTimestamp,就是从业务数据中获取事件时间和当前的watermark进行比较,保留最大值.
  5. 获取到maxTimestamp后,当定时触发器执行时onPeriodicEmit方法就会执行,这个时候就会用到maxTimestamp,再根据maxTimestamp - outOfOrdernessMillis - 1这个逻辑生成新的watermark.
  6. watermark生成后就会发往下游算子,这个还是根据watermark触发周期不断地发送,这个和数据是异步发送的,互不影响,只不过在发送watermark前,业务数据已经发往的下游.
  7. 下游的process算子只有接收到数据后才会执行,在没有数据到达之前是不会触发的,接收到数据后首先会判断数据类型
  8. 如果数据类型为业务数据则走output.emitRecord(recordOrMark.asRecord());方法,如果是watermark则走statusWatermarkValve.inputWatermark(...)方法,但是数据必定是先到达到达的,如果output.emitRecord(recordOrMark.asRecord())这一步阻塞了,就不会进行到接收watermark的判断,比如在process算子中通过Thread.sleep(...)就可以到达这个效果.
  9. 下游的process算子获取到的watermark并不是最新的,比如第一条数据到达后当前获取的watermark还是Long.MIN_VALUE + ...,只有下一条数据到达后才能获取到第一条数据中携带的事件时间.
  10. process处理完数据和watermark后会继续发往下游算子,以此类推.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1073714.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Monorepo实战】pnpm+turbo+vitepress构建公共组件库文档系统

Monorepo架构可以把多个独立的系统放到一起联调&#xff0c;本文记录基于pnpm > workspace功能&#xff0c;如何构建将vitepress和组件库进行联调&#xff0c;并且使用turbo进行任务顺序编排。 技术栈清单&#xff1a; pnpm 、vitepress 、turbo 一、需求分析 1、最终目标…

Maven 自动化构建

自动化构建定义了这样一种场景: 在一个项目成功构建完成后&#xff0c;其相关的依赖工程即开始构建&#xff0c;这样可以保证其依赖项目的稳定。 比如一个团队正在开发一个项目 bus-core-api&#xff0c; 并且有其他两个项目 app-web-ui 和 app-desktop-ui 依赖于这个项目。 …

面试算法22:链表中环的入口节点(1)

题目 如果一个链表中包含环&#xff0c;那么应该如何找出环的入口节点&#xff1f;从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点。 例如&#xff0c;在如图4.3所示的链表中&#xff0c;环的入口节点是节点3。 分析 第1步&#xff1a;确认是否包含环…

产线运作中如何实现sop无纸化的作业?

在车间产线的运作中&#xff0c;及时准确地获取作业指导书是至关重要的。然而&#xff0c;传统的纸质作业指导书往往需要花费大量时间和精力来查找和更新。而有了SOP电子作业指导书系统&#xff0c;车间工人们只需要通过电子设备登录系统&#xff0c;就可以轻松地找到所需的作业…

导引服务机器人 通用技术条件

声明 本文是学习GB-T 42831-2023 导引服务机器人 通用技术条件. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 6 检验规则 6.1 检验项目 检验分为型式检验和出厂检验。检验项目见表2。 表 2 检验项目 序号 检验项目 技术要求 检验方法 出厂检验 型…

【C++】哈希与布隆过滤器

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;命运给你一个低的起点&#xff0c;是想看你精彩的翻盘&#xff0c;而不是让你自甘堕落&#xff0c;脚下的路虽然难走&#xff0c;但我还能走&#xff0c;比起向阳而生&#xff0c;我更想尝试逆风…

二叉树题目:二叉树的所有路径

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树的所有路径 出处&#xff1a;257. 二叉树的所有路径 难度 4 级 题目描述 要求 给你一个二叉树的根结点 root \texttt{root} root&#xff…

Go 并发编程

并发编程 1.1 并发与并⾏ 并⾏与并发是两个不同的概念&#xff0c;普通解释&#xff1a; 并发&#xff1a;交替做不同事情的能⼒并⾏&#xff1a;同时做不同事情的能⼒ 如果站在程序员的⻆度去解释是这样的&#xff1a; 并发&#xff1a;不同的代码块交替执⾏并⾏&#xf…

慢 SQL 的致胜法宝

大促备战&#xff0c;最大的隐患项之一就是慢SQL&#xff0c;对于服务平稳运行带来的破坏性最大&#xff0c;也是日常工作中经常带来整个应用抖动的最大隐患&#xff0c;在日常开发中如何避免出现慢SQL&#xff0c;出现了慢SQL应该按照什么思路去解决是我们必须要知道的。本文主…

C++对象模型(5)-- 数据语义学:继承的对象布局(不含虚函数)

1、单继承的对象布局 (1) 在普通继承&#xff08;没有虚函数、没有继承虚基类&#xff09;的情况下&#xff0c;按父对象、子对象的顺序布局 我们来看下面的例子&#xff1a; class Base { protected:int x;int y; };class Derive : public Base { private:int z; };int mai…

vue2项目中使用element ui组件库的table,制作表格,改表格的背景颜色为透明的

el-table背景颜色变成透明_el-table背景透明_讲礼貌的博客-CSDN博客 之前是白色的&#xff0c;现在变透明了&#xff0c;背景颜色是蓝色

短视频矩阵系统源码--源头技术独立自研框架开发

目录 一、批量剪辑&#xff08;采用php语言&#xff0c;数学建模&#xff09; 短视频合成批量剪辑的算法主要有以下几种&#xff1a; 1. 帧间插值算法&#xff1a;通过对多个视频的帧进行插帧处理&#xff0c;从而合成一段平滑的短视频。 2. 特征提取算法&#xff1a;提取多…

RedissonClient中Stream流的简单使用

1、pub端 //获取一个流 RStream rStream redissonClient.getStream("testStream"); //创建一个map&#xff0c;添加数据 Map<String, Object> rr new HashMap<>(); rr.put("xx", RandomUtil.randomString(5)); //添加到流 rStream.addAll(r…

TypeScript 笔记:String 字符串

1 对象属性 length 返回字符串的长度 2 对象方法 charAt() 返回在指定位置的字符 charCodeAt() 返回在指定的位置的字符的 Unicode 编码 concat 连接两个或更多的字符串 indexOf 返回某个指定的字符串值在字符串中首次出现的位置 lastIndexOf 从后向前搜索字符串&…

c语言练习题83:#include“ “和#include<>的区别

#include" "和#include<>的区别 #include<> 默认根据环境变量的值去先搜索标准库&#xff0c;搜索系统文件会比较快。 #include“” 先搜索当前工程的路径&#xff0c;搜索自己自定义的文件会比较快。 因此自定义的头文件的名称包含在<>中的话…

LeetCode 24.两两交换链表中的结点

题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目解析 首先可以特判一下&#xff0c;如果结点数目小于等于1&#xff0c;则直接返回即可&#xff0c;因为数目小于等于1就不需要交换了。 然后我们可以创建一个虚拟的头结点&#xff0c;然…

SpringCloud源码探析(十)-Web消息推送

1.概述 消息推送在日常使用中的场景比较多&#xff0c;比如有人点赞了我的博客或者关注了我&#xff0c;这时我就会收到一条推送消息&#xff0c;以此来吸引我点击或者打开应用。消息推送的方式主要分为两种&#xff1a;web消息推送和移动端消息推送。它将所要发送的信息&…

【物联网】Arduino+ESP8266物联网开发(一):开发环境搭建 安装Arduino和驱动

ESP8266物联网开发 1.开发环境安装 开发软件下载地址&#xff1a; 链接: https://pan.baidu.com/s/1BaOY7kWTvh4Obobj64OHyA?pwd3qv8 提取码: 3qv8 1.1 安装驱动 将ESP8266连接到电脑上&#xff0c;安装ESP8266驱动CP210x 安装成功后&#xff0c;打开设备管理器&#xff0c…

抖音seo源代码开源部署----基于开放平台SaaS服务

抖音SEO搜索是什么&#xff1f; 抖音SEO搜索是指在抖音平台上进行搜索引擎优化&#xff08;Search Engine Optimization&#xff09;的一种技术手段。 通过优化抖音账号、发布内容和关键词等&#xff0c;提高抖音视频在搜索结果中的排名&#xff0c;从而增加视频曝光量和用户点…

什么是全流程的UI设计?它与单页面的视觉设计有什么区别?

在软件产品研发流程中&#xff0c;产品交互设计一般是根据项目需求&#xff0c;做出设计方案&#xff0c;以求解决某个问题。而全流程的设计不再局限于短暂或者单个页面的视觉优化&#xff0c;而是追求持续性地参与&#xff0c;以全局性整体性地提升产品体验。 在软件内的信息传…