【Flink精讲】Flink数据延迟处理

面试题:Flink数据延迟怎么处理?

  1. 将迟到数据直接丢弃【默认方案】
  2. 将迟到数据收集起来另外处理(旁路输出)
  3. 重新激活已经关闭的窗口并重新计算以修正结果(Lateness)

Flink数据延迟处理方案

用一个案例说明三种处理方式

举个例子:左流跟右流按照5秒的时间窗口进行coGroup操作(按单词进行关联),超过5秒进行丢弃。

结果说明:在Socket数据源输入 "1005000 java" 后,会统计1005000时间戳之前的数据,而在1005000时间戳之后输入的hello就没有被统计输出。当输入 "1010000 xixi" 后,触发了第2个窗口,只输出了java,还是没有后输入的hello统计结果,这也更明确了1005000时间戳之后输入的hello被丢弃了。

object MyCoGroupJoin {
  def main(args: Array[String]): Unit = {
    //创建环境变量
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //指定事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    //创建Socket源数据流,内容格式 "时间戳 单词"
    val s1 = env.socketTextStream("127.0.0.1", 9000)
      // 设置事件时间戳字段
      .assignAscendingTimestamps(_.split(" ")(0).toLong)
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })

    //创建Socket源数据流,内容格式 "时间戳 单词"
    val s2 = env.socketTextStream("127.0.0.1", 8888)
      // 设置事件时间戳字段
      .assignAscendingTimestamps(_.split(" ")(0).toLong)
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })

    //将两个数据流进行合并统计,这里是将两数据流利用窗口进行单词拼串处理
    s1.coGroup(s2)
      .where(_._2)
      .equalTo(_._2)
      .window(TumblingEventTimeWindows.of(Time.seconds(5))) //滚动窗口,窗口大小5秒
      .apply(new CoGroupFunction[(Long, String), (Long, String), String] {

        override def coGroup(first: lang.Iterable[(Long, String)], second: lang.Iterable[(Long, String)], out: Collector[String]): Unit = {
          //对两数据流中的数据进行循环遍历,并拼串下发
          first.forEach(r1 => {
            second.forEach(r2 => {
              println(s"${r1}::${r2}")
              val str = r1._2 + r2._2
              out.collect(str)
            })
          })
        }
      })
      .print()

    env.execute("cogroupjoin")
  }
}

 运行效果

========左流========
nc -l 9999

1001000 hello
1005000 java
1003000 hello [这条被丢弃了]
1010000 xixi

========右流========
nc -l 8888

1002000 hello
1005000 java
1001000 hello [这条被丢弃了]
1010000 xixi

========程序控制台输出结果========
(1001000,hello)::(1002000,hello)
4> hellohello
(1005000,java)::(1005000,java)
2> javajava

设置Watermark

时间语义:

  • Event Time(事件时间):每条数据或事件自带的时间属性。由于时间属性依附于数据本身,在高并发的情况下可能存在Event Time的到达为乱序的,即一个较早发生的数据延迟到达
  • Process Time(处理时间):对于某个算子来说,Processing Time指算子使用当前机器的系统时钟时间
  • Ingestion Time(接入时间):事件到达Flink Source的时间

Flink的三种时间语义中,Processing Time和Ingestion Time都是基于Flink本身所产生的时间,可以不用设置时间字段和Watermark。如果要使用Event Time,以下两项配置缺一不可:第一,使用一个时间戳为数据流中每个事件的Event Time赋值;第二,生成Watermark。

Event Time是每个事件的元数据,如果不设置,Flink并不知道每个事件的发生时间,我们必须要为每个事件的Event Time赋值一个时间戳。关于时间戳,包括Flink在内的绝大多数系统都使用Unix时间戳系统(Unix time或Unix epoch)。Unix时间戳系统以1970-01-01 00:00:00.000 为起始点,其他时间记为距离该起始时间的整数差值,一般是毫秒(millisecond)精度。

有了Event Time时间戳,我们还必须生成Watermark。Watermark是Flink插入到数据流中的一种特殊的数据结构,它包含一个时间戳,并假设后续不会有小于该时间戳的数据,如果后续数据存在小于该时间戳的数据则视为延迟数据,需另外处理。下图展示了一个乱序数据流,其中方框是单个事件,方框中的数字是其对应的Event Time时间戳,圆圈为Watermark,圆圈中的数字为Watermark对应的时间戳。

 将之前的例子进行处理说明

object MyCoGroupJoin {

  def main(args: Array[String]): Unit = {

    // 创建环境变量
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    // 指定事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 创建Socket源数据流,内容格式 "时间戳 单词"
    val s1 = env.socketTextStream("127.0.0.1", 9999)
      // 设置事件时间戳字段
      // .assignAscendingTimestamps(_.split(" ")(0).toLong)
      // 这里可以指定周期性产生WaterMark 或 间歇性产生WaterMark,分别使用AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks来实现
      // 这里使用周期性产生WaterMark,延长2秒
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(2)) {
        
        override def extractTimestamp(element: String): Long = {
          val strs = element.split(" ")
          strs(0).toLong
        }
      })
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })

    // 创建Socket源数据流,内容格式 "时间戳 单词"
    val s2 = env.socketTextStream("127.0.0.1", 8888)
      // 设置事件时间戳字段
      // .assignAscendingTimestamps(_.split(" ")(0).toLong)
      // 这里可以指定周期性产生WaterMark 或 间歇性产生WaterMark,分别使用AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks来实现
      // 这里使用周期性产生WaterMark,延长2秒
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(2)) {
        override def extractTimestamp(element: String): Long = {
          val strs = element.split(" ")
          strs(0).toLong
        }
      })
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })
    //将两个数据流进行合并统计,这里是将两数据流利用窗口进行单词拼串处理
    s1.coGroup(s2)
      .where(_._2)
      .equalTo(_._2)
      .window(TumblingEventTimeWindows.of(Time.seconds(5))) //滚动窗口,窗口大小5秒
      .apply(new CoGroupFunction[(Long, String), (Long, String), String] {
        
        override def coGroup(first: lang.Iterable[(Long, String)], second: lang.Iterable[(Long, String)], out: Collector[String]): Unit = {
          // 对两数据流中的数据进行循环遍历,并拼串下发
          first.forEach(r1 => {
            second.forEach(r2 => {
              println(s"${r1}::${r2}")
              val str = r1._2 + r2._2
              out.collect(str)
            })
          })
        }
      })
      .print()
    
    env.execute("cogroupjoin")
  }
}

执行效果

=========左流=========
nc -l 9999
1001000 hello
1005000 java
1003000 hello
1007000 java

=========右流=========
nc -l 8888
1002000 hello
1005000 java
1001000 hello
1007000 java

=========程序控制台输出=========
(1001000,hello)::(1002000,hello)
4> hellohello
(1001000,hello)::(1001000,hello)
4> hellohello
(1003000,hello)::(1002000,hello)
4> hellohello
(1003000,hello)::(1001000,hello)
4> hellohello

当我们使用Watermark后,我们可以发现在两个Socket终端输入"1005000 java"时,控制台并没有立刻统计输出信息。而是在两个Socket终端输入 "1007000 java"后,控制台才将统计结果输出出来,且在时间戳"1005000"之后输入的hello也同时给统计出来了,上面的问题可以解决了,但是 "1007000 java" 之后我们再输入 hello ,你会发现还是存在问题,没有输出又给丢弃了。继续测试如下。


======左流======
nc -l 9999
1001000 hello
1005000 java
1003000 hello
1007000 java
1003000 hello
1012000 spark

======右流======
nc -l 8888
1002000 hello
1005000 java
1001000 hello
1007000 java
1004000 hello
1012000 spark

=======程序执行控制台输出结果=======
(1001000,hello)::(1002000,hello)
4> hellohello
(1001000,hello)::(1001000,hello)
4> hellohello
(1003000,hello)::(1002000,hello)
4> hellohello
(1003000,hello)::(1001000,hello)
4> hellohello
(1005000,java)::(1005000,java)
2> javajava
(1005000,java)::(1007000,java)
2> javajava
(1007000,java)::(1005000,java)
2> javajava
(1007000,java)::(1007000,java)
2> javajava

所以waterMark只能在一定程度上解决这种问题。我们再来看看allowedLateness机制。

设置Lateness

object MyCoGroupJoin {
  def main(args: Array[String]): Unit = {
    // 创建环境变量
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    // 指定事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 创建Socket源数据流,内容格式 "时间戳 单词"
    val s1 = env.socketTextStream("127.0.0.1", 9999)
      // 设置事件时间戳字段
      // .assignAscendingTimestamps(_.split(" ")(0).toLong)
      // 这里可以指定周期性产生WaterMark 或 间歇性产生WaterMark,分别使用AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks来实现
      // 这里使用周期性产生WaterMark,延长2秒
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(2)) {
        override def extractTimestamp(element: String): Long = {
          val strs = element.split(" ")
          strs(0).toLong
        }
      })
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })
    // 创建Socket源数据流,内容格式 "时间戳 单词"
    val s2 = env.socketTextStream("127.0.0.1", 8888)
      // 设置事件时间戳字段
      // .assignAscendingTimestamps(_.split(" ")(0).toLong)
      // 这里可以指定周期性产生WaterMark 或 间歇性产生WaterMark,分别使用AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks来实现
      // 这里使用周期性产生WaterMark,延长2秒
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(2)) {
        override def extractTimestamp(element: String): Long = {
          val strs = element.split(" ")
          strs(0).toLong
        }
      })
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })
    //将两个数据流进行合并统计,这里是将两数据流利用窗口进行单词拼串处理
    s1.coGroup(s2)
      .where(_._2)
      .equalTo(_._2)
      .window(TumblingEventTimeWindows.of(Time.seconds(5))) //滚动窗口,窗口大小5秒
      // 允许数据迟到2秒,窗口触发后2秒内过来的数据还可以重新被计算
      .allowedLateness(Time.seconds(2))
      .apply(new CoGroupFunction[(Long, String), (Long, String), String] {
        override def coGroup(first: lang.Iterable[(Long, String)], second: lang.Iterable[(Long, String)], out: Collector[String]): Unit = {
          // 对两数据流中的数据进行循环遍历,并拼串下发
          first.forEach(r1 => {
            second.forEach(r2 => {
              println(s"${r1}::${r2}")
              val str = r1._2 + r2._2
              out.collect(str)
            })
          })
        }
      })
      .print()
    env.execute("cogroupjoin")
  }
}

执行效果

======左流======
nc -l 9999
1001000 hello
1007000 java

======右流======
nc -l 8888
1002000 hello
1007000 java
1003000 hello

=======程序执行控制台输出结果=======
(1001000,hello)::(1002000,hello)
4> hellohello
(1001000,hello)::(1002000,hello)
4> hellohello
(1001000,hello)::(1003000,hello)
4> hellohello

到这里估计有朋友又有疑问了,allowedLateness机制解决数据延迟设置的时间段,那之后再来的延迟数据呢,还是被丢弃了并没有彻底解决问题。别慌,针对allowedLateness机制之后来的延迟数据Flink还提供了另一种方案就是sideOutput机制。 

旁路输出

Side Output简单来说就是在程序执行过程中,将主流stream流中的不同的业务类型或者不同条件的数据分别输出到不同的地方。如果我们想对没能及时在Flink窗口计算的延迟数据专门处理,也就是窗口已经计算了,但后面才来的数据专门处理,我们可以使用旁路输出到侧流中去处理。

object MyCoGroupJoin {

  def main(args: Array[String]): Unit = {
    // 创建环境变量
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    // 指定事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 创建Socket源数据流,内容格式 "时间戳 单词"
    val s1 = env.socketTextStream("127.0.0.1", 9999)
      // 这里可以指定周期性产生WaterMark 或 间歇性产生WaterMark,分别使用AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks来实现
      // 这里使用周期性产生WaterMark,延长2秒
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(2)) {
        override def extractTimestamp(element: String): Long = {
          val strs = element.split(" ")
          strs(0).toLong
        }
      })
      .map(line => {
        val strs = line.split(" ")
        (strs(0).toLong, strs(1))
      })
    // 定义一个侧输出流
    val lateData: OutputTag[(Long, String)] = new OutputTag[(Long, String)]("late")
    val s2 = s1.timeWindowAll(Time.seconds(5))
      //允许迟到2秒数据
      .allowedLateness(Time.seconds(2))
      //迟到长于2秒的数据将被保存到lateData侧数据流中
      .sideOutputLateData(lateData)
      .process(new ProcessAllWindowFunction[(Long, String), (Long, String), TimeWindow] {
        override def process(context: Context, elements: Iterable[(Long, String)], out: Collector[(Long, String)]): Unit = {
          elements.foreach(ele => {
            out.collect(ele)
          })
        }
      })
    s2.print("主流")
    s2.getSideOutput(lateData).print("侧流")
    env.execute("cogroupjoin")
  }
}
=======数据流======
nc -l 9999
1001000 hello
1005000 java
1007000 python
1002000 hello
1009000 java
1001000 xixi
1002000 haha

=======程序执行控制台输出结果=====
主流:10> (1001000,hello)
主流:11> (1001000,hello)
主流:12> (1002000,hello)
侧流:1> (1001000,xixi)
侧流:2> (1002000,haha)

通过上面测试可以发现晚于allowedLateness机制的延迟数据,Flink没有丢弃而是输出到了侧输出流中等待处理了,这样延迟数据就完美解决了。

双流Join中的数据延迟处理

  1. 数据质量问题:流式数据到达计算引擎的时间不一定:比如 A 流的数据先到了,A 流不知道 B 流对应同 key 的数据什么时候到,没法关联;
  2. 数据时效问题:流式数据不知何时、下发怎样的数据:A 流的数据到达后,如果 B 流的数据永远不到,那么 A 流的数据在什么时候以及是否要填充一个 null 值下发下去。

Window Join

Flink Window Join。就是将两条流的数据从无界数据变为有界数据,即划分出时间窗口,然后将同一时间窗口内的两条流的数据做 Join(这里的时间窗口支持 Tumbling、Sliding、Session)。

解法说明:

  1. 流式数据到达计算引擎的时间不一定:数据已经被划分为窗口,无界数据变为有界数据,就和离线批处理的方式一样了,两个窗口的数据简单的进行关联即可
  2. 流式数据不知何时、下发怎样的数据:窗口结束就把数据下发下去,关联到的数据就下发 [A, B],没有关联到的数据取决于是否是 outer join 然后进行数据下发

Tumbling

 Sliding

Session

Flink SQL(Flink 1.14 版本 Window TVF 中支持):

SELECT 
    L.num as L_Num
    , L.id as L_Id
    , R.num as R_Num
    , R.id as R_Id
    , L.window_start
    , L.window_end
FROM (
    SELECT * 
    FROM TABLE(TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) L
FULL JOIN (
    SELECT * 
    FROM TABLE(TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) R
ON L.num = R.num 
AND L.window_start = R.window_start 
AND L.window_end = R.window_end;
方案特点

⭐ 产出数据质量:低
⭐ 产出数据时效性:中
当我们的窗口大小划分的越细时,在窗口边缘关联不上的数据就会越多,数据质量就越差。窗口大小划分的越宽时,窗口内关联上的数据就会越多,数据质量越好,但是产出时效性就会越差。所以要注意取舍。

举个例子:以曝光关联点击来说,如果我们划分的时间窗口为 1 分钟,那么一旦出现曝光在 0:59,点击在 1:01 的情况,就会关联不上,当我们的划分的时间窗口 1 小时时,只有在每个小时的边界处的数据才会出现关联不上的情况。

适用场景

该种解决方案适用于可以评估出窗口内的关联率高的场景,如果窗口内关联率不高则不建议使用。注意:这种方案由于上面说到的数据质量和时效性问题在实际生产环境中很少使用。

Interval Join

Interval Join。其也是将两条流的数据从无界数据变为有界数据,但是这里的有界和Window Join 的有界的概念是不一样的,这里的有界是指两条流之间的有界。

以 A 流 join B 流举例,interval join 可以让 A 流可以关联 B 流一段时间区间内的数据,比如 A 流关联 B 流前后 5 分钟的数据。

解法说明:

  1. 流式数据到达计算引擎的时间不一定:数据已经被划分为窗口,无界数据变为有界数据,就和离线批处理的方式一样了,两个窗口的数据简单的进行关联即可
  2. 流式数据不知何时、下发怎样的数据:窗口结束(这里的窗口结束是指 interval 区间结束,区间的结束是利用 watermark 来判断的)就把数据下发下去,关联到的数据就下发 [A, B],没有关联到的数据取决于是否是 outer join 然后进行数据下发
CREATE TABLE show_log_table (
     log_id BIGINT,
     show_params STRING,
     row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
     WATERMARK FOR row_time AS row_time
 ) WITH (
   'connector' = 'datagen',
   'rows-per-second' = '1',
   'fields.show_params.length' = '1',
   'fields.log_id.min' = '1',
   'fields.log_id.max' = '10'
 );
 
 CREATE TABLE click_log_table (
     log_id BIGINT,
     click_params STRING,
     row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
     WATERMARK FOR row_time AS row_time
 )
 WITH (
   'connector' = 'datagen',
   'rows-per-second' = '1',
   'fields.click_params.length' = '1',
   'fields.log_id.min' = '1',
   'fields.log_id.max' = '10'
 );
 
 CREATE TABLE sink_table (
     s_id BIGINT,
     s_params STRING,
     c_id BIGINT,
     c_params STRING
 ) WITH (
   'connector' = 'print'
 );
 
 INSERT INTO sink_table
 SELECT
     show_log_table.log_id as s_id,
     show_log_table.show_params as s_params,
     click_log_table.log_id as c_id,
     click_log_table.click_params as c_params
 FROM show_log_table FULL JOIN click_log_table ON show_log_table.log_id = click_log_table.log_id
 AND show_log_table.row_time BETWEEN click_log_table.row_time - INTERVAL '5' SECOND AND click_log_table.row_time
方案特点

⭐ 产出数据质量:中
⭐ 产出数据时效性:中
interval join 的方案比 window join 方案在数据质量上好很多,但是其也是存在 join 不到的情况的。并且如果为 outer join 的话,outer 一测的流数据需要要等到区间结束才能下发。

适用场景

该种解决方案适用于两条流之间可以明确评估出相互延迟的时间是多久的,这里我们可以使用离线数据进行评估,使用离线数据的两条流的时间戳做差得到一个分布区间。

比如在 A 流和 B 流时间戳相差在 1min 之内的有 95%,在 1-4 min 之内的有 4.5%,则我们就可以认为两条流数据时间相差在 4 min 之内的有 99.5%,这时我们将上下界设置为 4min 就是一个能保障 0.5% 误差的合理区间。

注意:这种方案在生产环境中还是比较常用的。

Regular Join

Regular Join。上面两节说的两种 Join 都是基于划分窗口,将无界数据变为有界数据进行关联机制,但是本节说的 regular join 则还是基于无界数据进行关联。

以 A 流 left join B 流举例,A 流数据到来之后,直接去尝试关联 B 流数据。
1. 如果关联到了则直接下发关联到的数据
2. 如果没有关联到则也直接下发没有关联到的数据,后续 B 流中的数据到来之后,会把之前下发下去的没有关联到数据撤回,然后把关联到的数据数据进行下发。由此可以看出这是基于 Flink SQL 的 retract 机制,则也就说明了其目前只支持 Flink SQL。

解法说明:

  1.  流式数据到达计算引擎的时间不一定:两条流的数据会尝试关联,能关联到直接下发,关联不到先下发一个目前的结果数据
  2.  流式数据不知何时、下发怎样的数据:两条流的数据会尝试关联,能关联到直接下发,关联不到先下发一个目前的结果数据
CREATE TABLE show_log_table (
    log_id BIGINT,
    show_params STRING
) WITH (
  'connector' = 'datagen',
  'rows-per-second' = '1',
  'fields.show_params.length' = '3',
  'fields.log_id.min' = '1',
  'fields.log_id.max' = '10'
);
 
CREATE TABLE click_log_table (
  log_id BIGINT,
  click_params     STRING
)
WITH (
  'connector' = 'datagen',
  'rows-per-second' = '1',
  'fields.click_params.length' = '3',
  'fields.log_id.min' = '1',
  'fields.log_id.max' = '10'
);
 
CREATE TABLE sink_table (
    s_id BIGINT,
    s_params STRING,
    c_id BIGINT,
    c_params STRING
) WITH (
  'connector' = 'print'
);
 
INSERT INTO sink_table
SELECT
    show_log_table.log_id as s_id,
    show_log_table.show_params as s_params,
    click_log_table.log_id as c_id,
    click_log_table.click_params as c_params
FROM show_log_table
LEFT JOIN click_log_table ON show_log_table.log_id = click_log_table.log_id;
方案特点

产出数据质量:高
产出数据时效性:高
数据质量和时效性高的原因都是因为 regular join 会保障目前 Flink 任务已经接收到的数据中能关联的一定是关联上的,即使关联不上,数据也会下发,完完全全保障了当前数据的客观性和时效性。

适用场景

该种解决方案虽然是目前在产出质量、时效性上最好的一种解决方案,但是在实际场景中使用时,也存在一些问题:

基于 retract 机制,所有的数据都会存储在 state 中以判断能否关联到,所以我们要设置合理的 state ttl 来避免大 state 问题导致的任务不稳定
基于 retract 机制,所以在数据发生更新时,会下发回撤数据、最新数据 2 条消息,当我们的关联层级越多,则下发消息量的也会放大
 sink 组件要支持 retract,我们不要忘了最终数据是要提供数据服务给需求方进行使用的,所以我们最终写入的数据组件也需要支持 retract,比如 MySQL。如果写入的是 Kafka,则下游消费这个 Kafka 的引擎也需要支持回撤\更新机制。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在 sealos 上使用 redisinsight 完美管理 redis

先起一个 redis 集群,在 sealos 上可以点点鼠标就搞定: 简单两步,redis 集群搞定。 再启动 RedisInsight, 是一个 redis 的可视化管理工具。 就可以看到部署后的地址了。进去之后填写 redis 的链接信息即可: 链接信息在数据库的…

算法第十二天-最大整除子集

最大整除子集 题目要求 解题思路 来自[宫水三叶] 根据题意:对于符合要求的[整除子集]中的任意两个值,必然满足[较大数]是[较小数]的倍数 数据范围是 1 0 3 10^3 103,我们不可能采取获取所有子集,再检查子集是否合法的暴力搜解法…

C# 自定义配置文件序列化生成+文件格式错误自动回档

文章目录 前言选择Xml简单的Xml使用测试用例简单的写简单的读简单的生成配置修改配置类测试用例运行结果对比 代码逻辑封装逻辑示意封装好的代码测试生成配置文件格式错误测试使用默认值覆盖来解决问题 配置文件人为修改错误如何解决解决方案代码测试用例运行结果 代码封装总结…

Swift爬虫使用代理IP采集唯品会商品详情

目录 一、准备工作 二、代理IP的选择与使用 三、使用Swift编写唯品会商品爬虫 四、数据解析与处理 五、注意事项与优化建议 六、总结 一、准备工作 在开始编写爬虫之前,需要准备一些工具和库,以确保数据抓取的顺利进行。以下是所需的工具和库&…

第14课 利用openCV快速数豆豆

除了检测运动,openCV还能做许多有趣且实用的事情。其实openCV和FFmpeg一样都是宝藏开源项目,貌似简单的几行代码功能实现背后其实是复杂的算法在支撑。有志于深入学习的同学可以在入门后进一步研究算法的实现,一定会受益匪浅。 这节课&#…

opencv003图像裁剪(应用NumPy矩阵的切片)

这一部分相对于马上要学习的二值化是要更更更简单一些的,只需三行,便能在opencv上裁剪图像啦(顺便云吸猫,太可爱了!) 出处见水印! 1、复习图像的显示 前几天期末考试,太久没有看…

docker安装nodejs,并更改为淘宝源

拉取官方 Node.js 镜像 docker pull node:latest创建 Dockerfile,并更改 NPM 下载源为淘宝源,设置为全局持久化 # 使用最新版本的Node.js作为基础镜像 FROM node:latest# 设置工作目录为/app WORKDIR /app # 更改 NPM 下载源为淘宝源,并设置…

限制选中指定个数CheckBox控件(1/2)

限制选中指定个数CheckBox控件(1/2) 实例需求:工作表中有8个CheckBox控件(下文中简称为控件),现在需要实现限制用户最多只能勾选4个控件。 Dim OnDic As Object Sub CheckboxeEvent()Dim oCB As CheckBox…

test mutation-01-变异测试 PITest PIT 是一种先进的变异测试系统,为 Java 和 JVM 提供黄金标准的测试覆盖率。

拓展阅读 test 系统学习-04-test converate 测试覆盖率 jacoco 原理介绍 test 系统学习-05-test jacoco 测试覆盖率与 idea 插件 test 系统学习-06-test jacoco SonarQube Docker learn-29-docker 安装 sonarQube with mysql Ubuntu Sonar PITest 实际应用的变异测试 …

Linux的基本指令(5)

目录 bc指令 uname指令 压缩解压相关的指令 zip指令 unzip指令 tar打包压缩指令 tar解压解包指令 ​编辑​编辑sz&rz 热键 关机命令 安装:yum install -y 指令 bc指令 bc命令可以很方便的进行浮点运算 Linux中的计算器 uname指令 语法:un…

QtApplets-SystemInfo

QtApplets-SystemInfo ​ 今天是2024年1月3日09:18:44,这也是2024年的第一篇博客,今天我们主要两件事,第一件,获取系统CPU使用率,第二件,获取系统内存使用情况。 ​ 这里因为写博客的这个本本的环境配置不…

高性能NVMe Host Controller IP

NVMe Host Controller IP 介绍 NVMe Host Controller IP可以连接高速存储PCIe SSD,无需CPU和外部存储器,自动加速处理所有的NVMe协议命令,具备独立的数据写入AXI4-Stream/FIFO接口和数据读取AXI4-Stream/FIFO接口,非常适合于超高…

Python爬虫中的协程

协程 基本概念 协程:当程序执行的某一个任务遇到了IO操作时(处于阻塞状态),不让CPU切换走(就是不让CPU去执行其他程序),而是选择性的切换到其他任务上,让CPU执行新的任务&#xff…

C++ 虚函数virtual的引入和应用

来回顾一下使用引用或指针调用方法的过程。请看下面的代码: BrassPlus ophelia; // 子类对象 Brass * bp; // 基类指针 bp &ophelia; // 让基类指针指向子类对象 bp->ViewAcct(); // ViewAcct() 如果基类和子类都有这个函…

tp8/6 插件PhpOffice\PhpSpreadsheet导入表格

一、安装 composer require phpoffice/phpspreadsheet 官网&#xff1a;phpoffice/phpspreadsheet - Packagist 二、代码 <?php namespace app\services\upload\model; use app\services\BaseServices; use \PhpOffice\PhpSpreadsheet\Spreadsheet; use \PhpOffice\Php…

计算机Java项目|基于SpringBoot+Vue的图书个性化推荐系统

项目编号&#xff1a;L-BS-GX-10 一&#xff0c;环境介绍 语言环境&#xff1a;Java: jdk1.8 数据库&#xff1a;Mysql: mysql5.7 应用服务器&#xff1a;Tomcat: tomcat8.5.31 开发工具&#xff1a;IDEA或eclipse 二&#xff0c;项目简介 图片管理系统是一个为学生和…

Mac打包Unix可执行文件为pkg

Mac打包Unix可执行文件为pkg 方式一&#xff1a;通过packages页面打包 1.下载packages app Distribution&#xff1a;自定义化更高&#xff0c;包括修改安装页面的内容提示 我这里主要演示Distribution模式的项目&#xff1a;通过unix可执行文件postinstall.sh脚本实现通过ma…

浅谈冒泡排序

手写一个冒泡排序的代码。 1.数组 let arr [10, 2, 50, 23, 30, 56, 3]; 2.排序的思路 里层的循环: for (var i 0; i < arr.length; i) {if (arr[i] < arr[i 1]) {var temp arr[i];arr[i] arr[i 1];arr[i 1] temp;} 用途&#xff1a; [2, 10, 23, 30, 50, 3, …

腾讯云取消免费10G CDN流量包:免费CDN时代结束

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 免费送了7-8年的腾讯云10G免费流量包&#xff0c;从2024年开始&#xff0c;停止赠送了!自此&#xff0c;国内绝大多数互联网大厂的CDN都开收费了! 大概从2016年开始&#xff0c;腾讯云为了抢夺CDN客户&#xff0…

基于JavaWeb+SSM+Vue四六级词汇微信小程序系统的设计和实现

基于JavaWebSSMVue四六级词汇微信小程序系统的设计和实现 源码获取入口KaiTi 报告Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 KaiTi 报告 &#xff08;1&#xff09;课题背景 伴随着社会的快速发展, 现代社…