【Flink-scala】DataStream编程模型之水位线

news2024/12/24 9:09:28

DataStream API编程模型

1.【Flink-Scala】DataStream编程模型之 数据源、数据转换、数据输出
2.【Flink-scala】DataStream编程模型之 窗口的划分-时间概念-窗口计算程序
3.【Flink-scala】DataStream编程模型之 窗口计算-触发器-驱逐器


文章目录

  • DataStream API编程模型
  • 前言
  • 一、水位线
    • 1.1 水位线
      • 1.1.1 概念
      • 1.1.2 水位线如何发挥作用呢?
      • 1.1.3 水位线原理
        • 1.1.3.1 消息正常到达系统的时间
        • 1.1.3.2消息延迟到达系统时的情况
        • 1.1.3.3 采用事件时间时的情况
        • 1.1.3.4 引入水位线机制的情况
      • 1.1.4 水位线的设置方法
        • 1.1.4.1水位线生成策略--固定延迟生成水位线
        • 1.1.4.2 水位线生成策略-单调递增生成水位线
        • 1.1.4.3 自动义生成水位线策略
  • 总结


前言

本小节学习水位线和延迟数据处理,再学习状态编程,水位线和延迟数据处理关联性强一点,如果篇幅太长,我就再开一小节写。
开始吧!

水位线,这和实际生活中河流/水库到达哪个水位线就要有什么问题一样,就是达到水位线后做什么处理。

(我这样想:河流里或者是水龙头的水是水流,把水换成数据就是datastream[数据流]。水位线这个概念也能扯上点关系)

一、水位线

1.1 水位线

1.1.1 概念

水位线是一种衡量事件时间进展的机制,它是数据本身的一个隐藏属性,本质上就是一个时间戳。

水位线是配合事件时间来使用的,通常基于事件时间的数据,自身都包含一个水位线用于处理乱序事件。

使用处理时间来处理事件时不会有延迟,因此也不需要水位线,所以水位线只出现在事件时间窗口

正确地处理乱序事件,通常是结合窗口和水位线这两种机制来实现的。

1.1.2 水位线如何发挥作用呢?

在流处理过程中,从事件产生,到流经数据源,再到流经算子,中间是有一个过程和时间的。

虽然大部分情况下,流到算子的数据都是按照事件产生的时间顺序到达的,但是也不排除由于网络、系统等原因,导致乱序的产生和迟到数据。

但是对于迟到数据而言,我们又不能无限期地等下去,必须要有个机制来保证在经过一个特定的时间后,必须触发窗口去进行计算。

在进行窗口计算时,使用接入时间或处理时间的消息,都是以系统的墙上时间(比如现在是8:50我写的博客,那么这个事件就是8:50,就是生成时间)为准,因此事件都是按序到达的。但在实际应用中,由于网络或者系统等外部因素影响,事件数据往往不能及时到达Flink系统,从而造成数据乱序到达或者延迟到达等问题。针对这两个问题Flink主要采用以水位线为核心的机制来应对。

此时就是水位线发挥作用了,它表示当达到水位线后,在水位线之前的数据已经全部到达(即使后面还有延迟的数据),系统可以触发相应的窗口计算。

只有水位线越过窗口对应的结束时间,窗口才会关闭和进行计算。

一般而言,只有以下两个条件同时成立,才会触发窗口计算:

(1)条件T1:水位线时间 >= 窗口结束时间;

(2)条件T2:在[窗口开始时间,窗口结束时间)中有数据存在。

理想情况下,水位线应该与处理时间一致,并且处理时间与事件时间只相差常数时间甚至为零。

当水位线与处理时间完全重合时,就意味着消息产生后马上被处理,不存在消息迟到的情况。
然而,由于网络拥塞或系统原因,消息常常存在迟到的情况,

因此,在设置水位线时,总是考虑一定的延时,从而给予迟到的数据一些机会。具体的延迟大小根据水位线实现方式的不同而也有所差别

1.1.3 水位线原理

1.1.3.1 消息正常到达系统的时间

在这里插入图片描述
window1[5s-15s]
window2 [10s-20s]
window3[15s-25s]

现在假设有一个单词数据流,需要采用基于处理时间的滑动窗口进行实时的词频统计,滑动窗口大小为10s,滑动步长为5s。

假设数据源分别在第12秒、第12秒和第17秒的时候,生成3条内容为单词“a”的消息,这些消息将进入窗口中。

   时间<15s, 2条数据
   时间>15s,1条数据
   5-15s,3条数据

每个窗口提交后,最后统计值分别是 (a, 2),(a, 3) 和 (a, 1)

1.1.3.2消息延迟到达系统时的情况

在这里插入图片描述
正常是12-17s来了3条数据,现在开始有迟到数据。

假设在12s时候出现一条迟到6s的数据(18sde 数据),这条延迟的消息会落入 window2 [10s-20s] 和 window3[15s-25s]。

窗口提交后,最后统计值将分别是 (a, 1),(a, 3) 和 (a, 2)。(正常应该是(a, 2),(a, 3) 和 (a, 1))

可看出这条延迟的消息没有对window2 [10s-20s]的计算结果造成影响,但却影响了window1[5s-15s]和 window3[15s-25s]的计算结果,导致二者计算结果出错。

因为当这条消息在第18秒到达时,window1[5s-15s]计算已结束,这条消息不会被统计到window1[5s-15s]中,而会落入window3[15s-25s],导致被统计window3[15s-25s]

1.1.3.3 采用事件时间时的情况

在这里插入图片描述
采用事件时间,则当系统时间行进到第18秒时,这条迟到了6秒的消息会落入 window2 [10s-20s],

因为这条消息的事件生成时间是第12秒,所以就应该属于window1[5s-15s]和window2 [10s-20s],

但是在第18秒时,window1[5s-15s]已经关闭,所以,这条延迟的消息只会落入 window2 [10s-20s]。

最终,三个窗口的计算结果是(a,1),(a, 3) 和 (a, 1),也就是说,window2[10s-20s]和 window3[15s-25s]提交了正确的结果,但是 window1[5s-15s]的结果还是错误的

1.1.3.4 引入水位线机制的情况


就本例而言,水位线本质上就是告诉Flink一条消息可以延迟多久,

因此,这里让水位线等于系统当前时间减去5秒。由于只有水位线越过窗口对应的结束时间,窗口才会关闭和进行计算,

因此,第1个窗口window1[5s-15s]将会在第20秒的时候进行计算,第2个窗口window2[10s-20s]将会在第25秒的时候进行计算,第3个窗口window3[15s-25s]将会在第30秒的时候进行计算。

当系统时间行进到第18秒时,这条迟到了6秒的消息会落入window1[5s-15s])和 window2 [10s-20s],因为这条消息的事件生成时间是第12秒,所以就应该属于window1[5s-15s]和window2 [10s-20s]。

最终,三个窗口提交正确结果,即(a, 2),(a, 3) 和 (a, 1)

1.1.4 水位线的设置方法

水位线事关事件时间,那么就需要知道事件时间戳。

就必须为数据流中的每个元素分配一个时间戳。

在Flink系统中,分配时间戳和生成水位线这两个工作是同时进行的,前者是由TimestampAssigner来实现的,后者则是由WatermarkGenerator来实现的。

当我们构建了一个DataStream之后,可以使用assignTimestampsAndWatermarks方法来分配时间戳和生成水位线,调用该方法时,需要传入一个WatermarkStrategy对象,语法如下:

DataStream.assignTimestampsAndWatermarks(WatermarkStrategy<T>)

一般情况下,Flink要求WatermarkStrategy对象中同时包含了TimestampAssigner对象和WatermarkGenerator对象。

WatermarkStrategy是一个接口,提供了很多静态的方法,对于一些常用的水位线生成策略,我们不需要去实现这个接口,可以直接调用静态方法来生成水位线。

或者,我们也可以通过实现WatermarkStrategy接口中的createWatermarkGenerator方法和createTimestampAssigner方法,来自定义水位线策略。

说到底就是两个,分配时间戳,生成水位线(有的地方叫水印)。

1.1.4.1水位线生成策略–固定延迟生成水位线

固定延迟生成水位线的语法如下:

WatermarkStrategy.forBoundedOutOfOrderness(Duration maxOutOfOrderness)

比如,现在要实现一个延迟3秒的固定延迟水位线,并从消息中获取时间戳,具体语句如下:

val dataStream = ......
dataStream.assignTimestampsAndWatermarks(
WatermarkStrategy
.forBoundedOutOfOrderness[StockPrice](Duration.ofSeconds(3))//这里延迟3s
.withTimestampAssigner(new SerializableTimestampAssigner[StockPrice] {
    override def extractTimestamp(element: StockPrice, recordTimestamp: Long): Long = element.timeStamp
    //分配时间戳
      }
)
)

使用的是这个方法forBoundedOutOfOrderness

1.1.4.2 水位线生成策略-单调递增生成水位线

单调递增生成水位线是通过WatermarkStrategy接口的静态方法forMonotonousTimestamps提供的,语法如下:

WatermarkStrategy.forMonotonousTimestamps()

学习单词:
在这里插入图片描述
在程序中按照如下方式使用:

val dataStream = ......
dataStream.assignTimestampsAndWatermarks(
WatermarkStrategy
.forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner[StockPrice] {
    override def extractTimestamp(element: StockPrice, recordTimestamp: Long): Long = element.timeStamp
  }
)
)
1.1.4.3 自动义生成水位线策略

自定义肯定就是实现某个接口的什么方法啦,之前就说过
水位线设置就两个:分配时间戳,生成水位线
这里我们只需要实现WatermarkStrategy接口中的createWatermarkGenerator方法和createTimestampAssigner方法就可以了。

水位线策略:

createWatermarkGenerator方法需要返回一个WatermarkGenerator对象。
WatermarkGenerator是一个接口,需要实现这个接口里面的onEvent方法和onPeriodicEmit方法:

(1)onEvent:数据流中的每个元素(或事件)到达以后,都会调用这个方法,如果我们想依赖每个元素生成一个水位线,然后发射到下游,就可以实现这个方法。
(2)onPeriodicEmit:当数据量比较大的时候,为每个元素都生成一个水位线,会影响系统性能,所以Flink还提供了一个周期性生成水位线的方法。这个水位线的生成周期的设置方法是:env.getConfig.setAutoWatermarkInterval(5000L),其中5000L是间隔时间,可以由用户自定义。

在自定义水位线生成策略时,Flink提供了两种不同的方式:

1.定期水位线:在这种机制中,系统会通过onEvent方法对系统中到达的事件进行监控,然后,在系统调用onPeriodicEmit方法时,生成一个水位线。(两个方法都使用)

2.标点水位线:在这种机制中,系统会通过onEvent方法对系统中到达的事件进行监控,并等待具有特定标记的事件到达,一旦监测到特定事件到达,就立即生成一个水位线。通常,这种机制不会调用onPeriodicEmit方法来生成一个水位线。(只使用一个方法)

代码:

import java.text.SimpleDateFormat
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, TimestampAssigner, TimestampAssignerSupplier, Watermark, WatermarkGenerator, WatermarkGeneratorSupplier, WatermarkOutput, WatermarkStrategy}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
 
 
case class StockPrice(stockId:String,timeStamp:Long,price:Double)
object WatermarkTest { 
  def main(args: Array[String]): Unit = {
    //设定执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
    
    //设定时间特性为事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    
//设定程序并行度
env.setParallelism(1)
 
    //创建数据源
val source = env.socketTextStream("localhost", 9999)
 
    //指定针对数据流的转换操作逻辑
val stockDataStream = source
      .map(s => s.split(","))
      .map(s=>StockPrice(s(0).toString,s(1).toLong,s(2).toDouble))

      //为数据流分配时间戳和水位线 
val watermarkDataStream = stockDataStream.assignTimestampsAndWatermarks(new MyWatermarkStrategy)
 
    //执行窗口计算
val sumStream = watermarkDataStream
      .keyBy("stockId")
      .window(TumblingEventTimeWindows.of(Time.seconds(3)))
      .reduce((s1, s2) => StockPrice(s1.stockId,s1.timeStamp, s1.price + s2.price))
 
    //打印输出
sumStream.print("output")
 
    //指定名称并触发流计算
    env.execute("WatermarkTest")
  }
//指定水位线生成策略
  class MyWatermarkStrategy extends WatermarkStrategy[StockPrice] {
 
    override def createTimestampAssigner(context:TimestampAssignerSupplier.Context):TimestampAssigner[StockPrice]={
      new SerializableTimestampAssigner[StockPrice] {
        override def extractTimestamp(element: StockPrice, recordTimestamp: Long): Long = {
          element.timeStamp //从到达消息中提取时间戳
        }
      }
    }

   override def createWatermarkGenerator(context:WatermarkGeneratorSupplier.Context): WatermarkGenerator[StockPrice] ={
      new WatermarkGenerator[StockPrice](){
        val maxOutOfOrderness = 10000L //设定最大延迟为10秒
        var currentMaxTimestamp: Long = 0L
        var a: Watermark = null
        val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
 
        override def onEvent(element: StockPrice, eventTimestamp: Long, output:WatermarkOutput): Unit = {          
          currentMaxTimestamp = Math.max(eventTimestamp, currentMaxTimestamp)
          a = new Watermark(currentMaxTimestamp - maxOutOfOrderness)
          output.emitWatermark(a)
          println("timestamp:" + element.stockId + "," + element.timeStamp + "|" + format.format(element.timeStamp) + "," + currentMaxTimestamp + "|" + format.format(currentMaxTimestamp) + "," + a.toString)
        }
        override def onPeriodicEmit(output:WatermarkOutput): Unit = {
          // 没有使用周期性发送水印,因此这里没有执行任何操作
        }
      }
    }
  }
}

输入:

s1	stock_1,1602031567000,8.14
s2	stock_1,1602031571000,8.23
s3	stock_1,1602031577000,8.24
s4	stock_1,1602031578000,8.87
s5	stock_1,1602031579000,8.55
s6	stock_1,1602031581000,8.43
s7	stock_1,1602031582000,8.78

然后,在日志终端内,就可以看到如下输出信息:

timestamp:stock_1,1602031567000|2020-10-07 08:46:07.000,1602031567000|2020-10-07 08:46:07.000,Watermark @ 1602031557000 (2020-10-07 08:45:57.000)
timestamp:stock_1,1602031571000|2020-10-07 08:46:11.000,1602031571000|2020-10-07 08:46:11.000,Watermark @ 1602031561000 (2020-10-07 08:46:01.000)
timestamp:stock_1,1602031577000|2020-10-07 08:46:17.000,1602031577000|2020-10-07 08:46:17.000,Watermark @ 1602031567000 (2020-10-07 08:46:07.000)
timestamp:stock_1,1602031578000|2020-10-07 08:46:18.000,1602031578000|2020-10-07 08:46:18.000,Watermark @ 1602031568000 (2020-10-07 08:46:08.000)
timestamp:stock_1,1602031579000|2020-10-07 08:46:19.000,1602031579000|2020-10-07 08:46:19.000,Watermark @ 1602031569000 (2020-10-07 08:46:09.000)
output> StockPrice(stock_1,1602031567000,8.14)
timestamp:stock_1,1602031581000|2020-10-07 08:46:21.000,1602031581000|2020-10-07 08:46:21.000,Watermark @ 1602031571000 (2020-10-07 08:46:11.000)
timestamp:stock_1,1602031582000|2020-10-07 08:46:22.000,1602031582000|2020-10-07 08:46:22.000,Watermark @ 1602031572000 (2020-10-07 08:46:12.000)
output> StockPrice(stock_1,1602031571000,8.23)

为了正确理解水位线的工作原理,下面我们详细解释每个事件到达后水位线的变化情况、各个窗口中的事件分布情况以及窗口触发计算的情况。关于窗口计算,这里要再次强调,只有以下两个条件同时成立,才会触发窗口计算:
(1)条件T1:水位线时间 >= 窗口结束时间;
(2)条件T2:在[窗口开始时间,窗口结束时间)中有数据存在。

1.s1事件到达后

事件s1到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031557000(2020-10-07 08:45:57.000)。
在这里插入图片描述
s1到达后各个窗口包含事件的情况
在这里插入图片描述

水位线是在增长的,在那么增长的呢?
这是我截取上面 的部分代码。最大延迟10s,就是本次到的事件最大时间戳-10s,即为水位线。对应下代码:a = new Watermark(currentMaxTimestamp - maxOutOfOrderness)

val maxOutOfOrderness = 10000L //设定最大延迟为10秒
        var currentMaxTimestamp: Long = 0L
        var a: Watermark = null
        val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
 
        override def onEvent(element: StockPrice, eventTimestamp: Long, output:WatermarkOutput): Unit = {          
          currentMaxTimestamp = Math.max(eventTimestamp, currentMaxTimestamp)
          a = new Watermark(currentMaxTimestamp - maxOutOfOrderness)

水位线增长:每次有新事件到达时,都会检查并更新currentMaxTimestamp,然后根据这个值减去maxOutOfOrderness来生成新的水位线

2.当事件s2到达以后

s2到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031561000(2020-10-07 08:46:01.000)。
在这里插入图片描述
s2到达以后各个窗口内包含的事件的情况。
在这里插入图片描述
3.当事件s3到达以后

事件s3到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031567000(2020-10-07 08:46:07.000)。

在这里插入图片描述
s3到达以后各个窗口内包含的事件的情况。
在这里插入图片描述
4.当事件s4到达以后
事件s4到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031568000(2020-10-07 08:46:08.000)。
在这里插入图片描述
s4到达以后各个窗口内包含的事件的情况。
在这里插入图片描述
回顾一下:
触发窗口计算:
(1)条件T1:水位线时间 >= 窗口结束时间;
(2)条件T2:在[窗口开始时间,窗口结束时间)中有数据存在。

看到水位线事件8:46:08,窗口结束事件是09,那么此时还没有大于等于。
继续
5.当事件s5到达以后
事件s5到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031569000(2020-10-07 08:46:09.000)。
在这里插入图片描述

当当当,注意啦,看看触发窗口计算条件。

s5到达以后各个窗口内包含的事件的情况。
在这里插入图片描述
8:46:09水位线已经大于等于w1窗口结束时间啦,条件1满足,且窗口有数据,条件2满足,w1开始计算!!!

6.当事件s6到达以后

事件s6到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031571000(2020-10-07 08:46:11.000)。

在这里插入图片描述
s6到达以后各个窗口内包含的事件的情况:
在这里插入图片描述
此时再看看条件满足否?
7.当事件s7到达以后
事件s7到达系统以后的水位线的变化情况,可以看出,当前的水位线已经到达了1602031572000(2020-10-07 08:46:12.000)。
在这里插入图片描述
s7到达以后各个窗口内包含的事件的情况。
在这里插入图片描述
当当当,又注意啦,看看是否满足条件?

满足条件,触发计算,窗口2 计算完成!!

总结

没有想到水位线写了这么多,延迟数据处理还没有写,本小节主要学习水位线的原理和设置方法。
其中自定义的水位线生成策略稍显麻烦,代码需要着重分析。下一小节该写延迟数据处理了。

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

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

相关文章

PHP RabbitMQ连接超时问题

问题背景 Error: The connection timed out after 3 sec while awaiting incoming data 看到这个报错&#xff0c;我不以为意&#xff0c;认为是我设置的超时时间不够导致的&#xff0c;那就设置长一点 Error: The connection timed out after 300 sec while awaiting incom…

【LeetCode热题100】BFS解决FloodFill算法

这篇博客主要记录了使用BFS解决FloodFill算法的几道题目&#xff0c;包括图像渲染、岛屿数量、岛屿的最大面积、被包围的区域。 class Solution {using PII pair<int, int>; public:vector<vector<int>> floodFill(vector<vector<int>>& im…

L2G3000-LMDeploy 量化部署实践

文章目录 LMDeploy 量化部署实践闯关任务环境配置W4A16 量化 KV cacheKV cache 量化Function call LMDeploy 量化部署实践闯关任务 环境配置 conda create -n lmdeploy python3.10 -y conda activate lmdeploy conda install pytorch2.1.2 torchvision0.16.2 torchaudio2.1.…

大数据新视界 -- 大数据大厂之 Hive 临时表与视图:灵活数据处理的技巧(上)(29 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Ubuntu操作系统在Vmware中的安装、常用操作、最基础的知识、imx6ll基本开发环境配置

01-Ubuntu操作系统的安装 网盘搜索 “ubuntu18.04.zip”&#xff0c;下载下来之后用Vmware打开就行了。 我用的虚拟机是15.5.6&#xff0c;实测没有问题。 启动时用户名为book的密码为123456 提问&#xff1a;Ubuntu与Centos系统有何区别&#xff1f; 详情见 https://blog.cs…

windows2012服务器安装sqlserver2012出现NetFx3错误的解决方法。

出现以下错误: 启用 Windows 功能 NetFx3 时出错&#xff0c;错误代码: -2146498298。请尝试从 Windows 管理工具启用 Windows 功能 NetFx3&#xff0c;然后重新运行安装程序。有关如何启用 Windows 功能的详细信息&#xff0c;具体解决办法如下&#xff1a; 1、打开PowerShel…

FPGA实战篇(按键控制LDE实验)

1.按键简介 按键开关是一种电子开关&#xff0c;属于电子元器件类。我们的开发板上有两种按键开关&#xff1a;第一种是本实验所使用的轻触式按键开关&#xff0c;简称轻触开关。使用时以向开关的操作方向施加压力使内部电路闭合接通&#xff0c;当撤销压力时开关断开&#xff…

C++析构函数和构造函数

一、构造函数 1.构造函数的基本概念 1.对构造函数的理解&#xff1a; 构造函数是类的一种特殊成员函数&#xff0c;其主要功能是在创建对象时进行初始化操作。它的名字与类名相同&#xff0c;并且没有返回值类型&#xff08;不能是void&#xff09;。例如&#xff0c;对于一个…

【Axure高保真原型】数值条件分组

今天和大家分享数值条件分组的原型模板&#xff0c;效果包括&#xff1a; 点击添加分组按钮&#xff0c;可以显示添加弹窗&#xff0c;填写分组名称和数值区间后&#xff0c;可以新增该分组信息‘’ 修改分组区间&#xff0c;可以直接在输入框里修改已有的分组区间&#xff0c…

阳光电脑公司的维修服务微信小程序ssm+论文源码调试讲解

第2章 开发环境与技术 阳光电脑公司的维修服务微信小程序的编码实现需要搭建一定的环境和使用相应的技术&#xff0c;接下来的内容就是对阳光电脑公司的维修服务微信小程序用到的技术和工具进行介绍。 2.1 MYSQL数据库 本课题所开发的应用程序在数据操作方面是不可预知的&…

重生之我在异世界学编程之C语言:深入结构体篇(下)

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言结构体的自引用实现链表一、链表的基…

使用Redis Stream偶发空指针问题

问题描述&#xff1a;使用redission客户端封装的stream消息队列&#xff0c;在进行消息轮询时&#xff0c;偶发出现空指针问题。 [2024-11-13 09:59:20] [] [] [redis-stream-consumer-thread-1 ] [lambda$streamMessageListenerContainer$1] [ERROR] [c.r.c.r.s.config.Redi…

35页PDF | 元数据与数据血缘落地实施(限免下载)

一、前言 这份报告详细介绍了元数据与数据血缘的概念、重要性以及在企业数据中台中的应用。报告阐述了数据中台的核心价值在于整合和管理体系内的数据&#xff0c;以提升数据资产化能力并支持业务决策。报告还涵盖了元数据的分类&#xff08;技术元数据和业务元数据&#xff0…

方案精读:50页智慧园区数字化平台总体规划与建设方案PPT

本文介绍了智慧园区数字化平台总体规划与建设方案&#xff0c;包括智慧园区工业云平台、智慧办公平台、智能工厂、智慧能源管理、智慧政务管理等方面的建设内容。方案旨在通过技术手段加强园区内部沟通和管理能力&#xff0c;实现个性化营销、柔性化制造、高效智能协同的供应链…

深入浅出:Go语言标准库探索

深入浅出&#xff1a;Go语言标准库探索 引言 Go语言自发布以来&#xff0c;以其简洁的语法、高效的性能和强大的并发支持赢得了开发者的青睐。除了这些特性外&#xff0c;Go还拥有一个功能丰富且设计精良的标准库&#xff0c;几乎涵盖了现代应用程序开发所需的所有基本功能。…

python selenium(4+)+chromedriver最新版 定位爬取嵌套shadow-root(open)中内容

废话不多说&#xff0c;直接开始 本文以无界作为本文测试案例&#xff0c;抓取shadow-root&#xff08;open&#xff09;下的内容 shadow Dom in selenium&#xff1a; 首先先讲一下shadow Dom in selenium 版本的区别&#xff0c;链接指向这里 在Selenium 4版本 以及 chrom…

1207论文速读

1、SCN_GNN: A GNN-based fraud detection algorithm combining strong node and graph topology information 全文总结&#xff1a;本文介绍了一种基于图神经网络的欺诈检测算法——SCN_GNN。在欺诈检测中&#xff0c;由于欺诈者经常使用多种关系类型来掩盖其活动&#xff0c…

5.3_小程序渗透

小程序 行为工具小程序抓包全局代理&#xff0c;Proxifer小程序渗透小程序反编译Wxapkg 参数&#xff1a;scan 联网自动扫描&#xff0c;-r 指定 wx文件路径小程序逆向WeChatOpenDevTools 小程序抓包 工具步骤 全局代理 Burp中Proxy setings导出证书选择 Select file &a…

怎么实现邮件营销自动化?

邮件营销能够出色地帮助我们与客户建立良好关系。无论是新客户还是老客户&#xff0c;都可以通过邮件来达成较为良好的客户关系。然而&#xff0c;从消费者的角度来看&#xff0c;每个人都有自己独特的习惯和特点&#xff0c;没有人希望收到千篇一律、营销意味过重的邮件。因此…

使用ensp搭建内外互通,使用路由跨不同vlan通信。

1.网络拓扑图 2.规则 &#xff08;1&#xff09;允许 &#xff08;自己&#xff09;ping通内外网&#xff0c;内外网随便一个pc就可以. &#xff08;2&#xff09; 允许&#xff08;电信&#xff09;ping通内外网&#xff0c;内外网随便一个pc就可以 &#xff08;时间问题不做…