《Flink学习笔记》——第七章 处理函数

为了让代码有更强大的表现力和易用性,Flink 本身提供了多层 API

在更底层,我们可以不定义任何具体的算子(比如 map,filter,或者 window),而只是提炼出一个统一的“处理”(process)操作——它是所有转换算子的一个概括性的表达,可以自定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function)。是整个DataStream API的基础

7.1 基本处理函数

处理函数主要是定义数据流的转换操作,Flink提供的处理函数类接口ProcessFunction

7.1.1 处理函数的功能和使用

我们之前讲过的MapFunction(一一处理,仅仅拿到数据)、AggregateFunction(窗口聚合,除了数据还可以拿到当前的状态)

另外,RichMapFunction提供了获取上下文的方法——getRuntimeContext(),可以拿到状态,并行度、任务名等运行时信息

但上面这些无法拿到事件的时间戳或者当前水位线。

而在很多应用需求中,要求我们对时间有更精细的控制,需要能够获取水位线,甚至要“把控时间”、定义什么时候做什么事,这就不是基本的时间窗口能够实现的了,所以这个时候就要用到底层的API——处理函数ProcessFunction了

  • 提供“定时服务”,可以通过它访问事件流中的事件、时间戳、水位线,甚至可以注册“定时事件”
  • 继承了AbstractRichFunction,拥有富函数所有特性
  • 可以直接将数据输出到侧输出流

使用:

​ 直接基于 DataStream 调用.process()方法就可以了。方法需要传入一个 ProcessFunction 作为参数,用来定义处理逻辑。

stream.process(new MyProcessFunction())

7.1.2 ProcessFunction解析

public abstract class ProcessFunction<I, O> extends AbstractRichFunction{
    public abstract void processElement(I var1, ProcessFunction<I, O>.Context var2, Collector<O> var3);
    public void onTimer(long timestamp, ProcessFunction<I, O>.OnTimerContext ctx, Collector<O> out);
}

1.抽象方法.processElement()

  • var1:正在处理的数据
  • var2:上下文
  • var3:“收集器”,用于返回数据

2.非抽象方法.onTimer()

  • 用于定义定时触发的操作

7.1.3 处理函数的分类

Flink 中的处理函数其实是一个大家族,ProcessFunction 只是其中一员

Flink 提供了 8 个不同的处理函数:

(1) ProcessFunction

​ 最基本的处理函数,基于 DataStream 直接调用.process()时作为参数传入

(2) KeyedProcessFunction

​ 对流按键分区后的处理函数,基于 KeyedStream 调用.process()时作为参数传入。要想使用定时器,比如基于 KeyedStream

(3) ProcessWindowFunction

​ 开窗之后的处理函数,也是全窗口函数的代表。基于 WindowedStream 调用.process()时作为参数传入

(4)ProcessAllWindowFunction

​ 同样是开窗之后的处理函数,基于 AllWindowedStream 调用.process()时作为参数传入

(5) CoProcessFunction

​ 合并(connect)两条流之后的处理函数,基于 ConnectedStreams 调用.process()时作为参数传入

(6) ProcessJoinFunction

​ 间隔连接(interval join)两条流之后的处理函数,基于 IntervalJoined 调用.process()时作为参数传入

(7)BroadcastProcessFunction

​ 广播连接流处理函数,基于 BroadcastConnectedStream 调用.process()时作为参数传入。这里的“广播连接流”BroadcastConnectedStream,是一个未 keyBy 的普通 DataStream 与一个广播流(BroadcastStream)做连接(conncet)之后的产物

(8) KeyedBroadcastProcessFunction

​ 按键分区的广播连接流处理函数,同样是基于 BroadcastConnectedStream 调用.process()时作为参数传入。与 BroadcastProcessFunction 不同的是,这时的广播连接流,是一个 KeyedStream 与广播流(BroadcastStream)做连接之后的产物

7.2 按键分区处理函数

public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction

只有在 KeyedStream 中才支持使用 TimerService 设置定时器的操作,所以一般情况下,我们都是先做了 keyBy 分区之后,再去定义处理操作;代码中更加常见的处理函数是 KeyedProcessFunction,最基本的 ProcessFunction 反而出镜率没那么高。KeyedProcessFunction 可以说是处理函数中的“嫡系部队”,可以认为是 ProcessFunction 的一个扩展。

7.2.1 定时器(Timer)和定时服务(TimerService)

首先通过定时服务注册一个定时器,ProcessFunction 的上下文(Context)中提供了.timerService()方法,可以直接返回一个 TimerService 对象。

TimerService 是 Flink 关于时间和定时器的基础服务接口,包含以下六个方法:

// 获取当前的处理时间
long currentProcessingTime();

// 获取当前的水位线(事件时间)
long currentWatermark();

// 注册处理时间定时器,当处理时间超过 time 时触发
void registerProcessingTimeTimer(long time);

// 注册事件时间定时器,当水位线超过 time 时触发
void registerEventTimeTimer(long time);

// 删除触发时间为 time 的处理时间定时器
void deleteProcessingTimeTimer(long time);

// 删除触发时间为 time 的处理时间定时器
void deleteEventTimeTimer(long time);

7.2.2 KeyedProcessFunction的使用

与 ProcessFunction 的定义几乎完全一样,区别只是在于类型参数多了一个 K, 这是当前按键分区的 key 的类型。在KeyedProcessFunction中可以注册定时器,定义定时器触发逻辑。

KeyedProcessFunction是个抽象类,继承了AbstractRichFunction。

public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction

主要有两个核心的方法:

// 定义处理每个元素的逻辑
public abstract void processElement(I value, Context ctx, Collector<O> out)

// 定时器触发时处理逻辑
public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out)

从上面可以看到,参数里面都有Context(这里OnTimerContext继承了Context),所以都可以通过

ctx.timerService().registerEventTimeTimer(long time);

去注册定时器。

示例:

自定义数据源

public class CustomSource implements SourceFunction<Event> {
    @Override
    public void run(SourceContext<Event> ctx) throws Exception {
        // 直接发出一条数据
        ctx.collect(new Event("Mark", "./hhhh.com", 1000L));

        // 中间停顿5秒
        Thread.sleep(5000L);

        // 发出10秒后的数据
        ctx.collect(new Event("Mark", "/home", 11000L));
        Thread.sleep(5000L);

        // 发出 10 秒+1ms 后的数据
        ctx.collect(new Event("Alice", "./cart", 11001L));
        Thread.sleep(5000L);

    }

    @Override
    public void cancel() {

    }
}

创建一个KeyedProcessFunction实现类

public class MyKeyedProcessFunction extends KeyedProcessFunction<Boolean, Event, String> {
    @Override
    public void processElement(Event value, KeyedProcessFunction<Boolean, Event, String>.Context ctx, Collector<String> out) throws Exception {
        out.collect("数据到达,时间戳为:" + ctx.timestamp());
        out.collect("数据到达,水位线为:" + ctx.timerService().currentWatermark());
        // 注册一个 1 秒后的定时器
        ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 1000L);
        out.collect(String.format("注册定时器:%d%n-------分割线-------", ctx.timestamp() + 1000L));
    }

    @Override
    public void onTimer(long timestamp, KeyedProcessFunction<Boolean, Event, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
        out.collect("定时器触发,触发时间:" + timestamp);
    }
}

主函数

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

        SingleOutputStreamOperator<Event> stream = env.addSource(new CustomSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.getTimestamp();
                            }
                        }));

        stream.keyBy(data->true).process(new MyKeyedProcessFunction()).print();

        env.execute();

    }
}

输出结果:

数据到达,时间戳为:1000
数据到达,水位线为:-9223372036854775808
注册定时器:2000
-------分割线-------
数据到达,时间戳为:11000
数据到达,水位线为:999
注册定时器:12000
-------分割线-------
定时器触发,触发时间:2000
数据到达,时间戳为:11001
数据到达,水位线为:10999
注册定时器:12001
-------分割线-------
定时器触发,触发时间:12000
定时器触发,触发时间:12001

输出结果解释:

当第一条数据 Event(“Mark”, “./hhhh.com”, 1000L) 过来,由于水位线生成的周期是默认(200ms)一次,所以第一次数据过来时,水位线没有更新,为默认值Long.MIN_VALUE,此时注册一个以事件时间为准加1000ms的定时器。所以输出就是:

数据到达,时间戳为:1000
数据到达,水位线为:-9223372036854775808
注册定时器:2000
-------分割线-------

过了200ms后,到了水位线生成时间,此时最大时间戳为1000,由于没有设置水位线延迟,所以默认减1ms。此时水位线为:1000-1=999.并未达到定时器触发时间(2000)

过了5秒钟第二条数据 Event(“Mark”, “/home”, 11000L) 过来,输出并注册了一个12000的定时器:

数据到达,时间戳为:11000
数据到达,水位线为:999
注册定时器:12000
-------分割线-------

达到水位线生成时间后,更新为11000-1=10999,此时达到(注册定时器:2000)触发时间,所以输出:

定时器触发,触发时间:2000

过了5秒,数据 Event(“Alice”, “./cart”, 11001L) 过来,输出并注册了一个12001的定时器

数据到达,时间戳为:11001
数据到达,水位线为:10999
注册定时器:12001
-------分割线-------

达到水位线生成时间后,更新为11001-1=11000

过了5秒,数据发送执行完毕,第三条数据发出后再过 5 秒,没有更多的数据生成了,整个程序运行结束将要退出,此时 Flink 会自动将水位线推进到长整型的最大值(Long.MAX_VALUE)。于是所有尚未触发的定时器这时就统一触发了,输出

定时器触发,触发时间:12000
定时器触发,触发时间:12001

7.3 窗口处理函数

除了按键分区的处理,还有就是窗口数据的处理,常用的有:

  • ProcessWindowFunction
  • ProcessAllWindowFunction

7.3.1 窗口处理函数的使用

进行窗口计算,我们可以直接调用现成的简单聚合方法(sum/max/min),也可以通过调用.reduce()或.aggregate()来自定义一般的增量聚合函数(ReduceFunction/AggregateFucntion);而对于更加复杂、需要窗口信息和额外状态的一些场景,我们还可以直接使用全窗口函数、把数据全部收集保存在窗口内,等到触发窗口计算时再统一处理。窗口处理函数就是一种典型的全窗口函数。

窗口处理函数 ProcessWindowFunction 的使用与其他窗口函数类似,也是基于WindowedStream 直接调用方法就可以,只不过这时调用的是.process()

stream.keyBy( t -> t.f0 )
    .window( TumblingEventTimeWindows.of(Time.seconds(10)) )
    .process(new MyProcessWindowFunction())

7.3.2 ProcessWindowFunction 解析

public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window>
        extends AbstractRichFunction

/*
IN: 输入数据类型
OUT:输出数据类型
KEY:数据中key的类型
W:窗口类型
*/

方法:

// 窗口数据的处理
public abstract void process(
            KEY key, Context context, Iterable<IN> elements, Collector<OUT> out);
/*
key: 键
context: 上下文
elements: 窗口收集到用来计算的所有数据,这是一个可迭代的集合类型
out: 发送输出结果的收集器
*/


// 这主要是方便我们进行窗口的清理工作。如果我们自定义了窗口状态,那么必须在.clear()方法中进行显式地清除,避免内存溢出
public void clear(Context context);

还定义了一个抽象类

public abstract class Context implements java.io.Serializable
// 我们之前可以看到,processFunction用的都是Context,但是这里ProcessWindowFunction 自己定义了一个Context,他是没有定时器的。为什么呢?因为本身窗口操作已经起到了一个触发计算的时间点,一般情况下是没有必要去做定时操作的。如果非要这么做,可以使用窗口触发器Trigger,里面有一个TriggerContext

ProcessAllWindowFunction的用法相似

stream.windowAll( TumblingEventTimeWindows.of(Time.seconds(10)) )
	.process(new MyProcessAllWindowFunction())

7.4 应用案例——TopN

窗口的计算处理,在实际应用中非常常见。对于一些比较复杂的需求,如果增量聚合函数无法满足,我们就需要考虑使用窗口处理函数这样的“大招”了。

网站中一个非常经典的例子,就是实时统计一段时间内的热门 url。例如,需要统计最近

10 秒钟内最热门的两个 url 链接,并且每 5 秒钟更新一次。我们知道,这可以用一个滑动窗口来实现,而“热门度”一般可以直接用访问量来表示。于是就需要开滑动窗口收集 url 的访问数据,按照不同的 url 进行统计,而后汇总排序并最终输出前两名。这其实就是著名的“Top N” 问题。

很显然,简单的增量聚合可以得到 url 链接的访问量,但是后续的排序输出 Top N 就很难实现了。所以接下来我们用窗口处理函数进行实现。

7.4.1 使用 ProcessAllWindowFunction

一种最简单的想法是,我们干脆不区分 url 链接,而是将所有访问数据都收集起来,统一进行统计计算。所以可以不做 keyBy,直接基于 DataStream 开窗,然后使用全窗口函数ProcessAllWindowFunction 来进行处理。

在窗口中可以用一个 HashMap 来保存每个 url 的访问次数,只要遍历窗口中的所有数据, 自然就能得到所有 url 的热门度。最后把 HashMap 转成一个列表 ArrayList,然后进行排序、取出前两名输出就可以了

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

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource()).assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps().withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
            @Override
            public long extractTimestamp(Event event, long l) {
                return event.getTimestamp();
            }
        }));

        SingleOutputStreamOperator<String> urlStream = stream.map(new MapFunction<Event, String>() {
            @Override
            public String map(Event event) throws Exception {
                return event.getUrl();
            }
        });

        SingleOutputStreamOperator<String> result = urlStream.windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                .process(new ProcessAllWindowFunction<String, String, TimeWindow>() {
                    @Override
                    public void process(ProcessAllWindowFunction<String, String, TimeWindow>.Context context,
                                        Iterable<String> elements, Collector<String> out) throws Exception {
                        HashMap<String, Long> urlCountMap = new HashMap<>(10);
                        for (String url : elements) {
                            if (urlCountMap.containsKey(url)) {
                                long count = urlCountMap.get(url);
                                urlCountMap.put(url, count + 1);
                            } else {
                                urlCountMap.put(url, 1L);
                            }
                        }

                        // 转存为ArrayList
                        ArrayList<Tuple2<String, Long>> mapList = new ArrayList<Tuple2<String, Long>>();
                        for (String key : urlCountMap.keySet()) {
                            mapList.add(Tuple2.of(key, urlCountMap.get(key)));
                        }
                        mapList.sort(new Comparator<Tuple2<String, Long>>() {
                            @Override
                            public int compare(Tuple2<String, Long> o1, Tuple2<String, Long> o2) {
                                return o2.f1.intValue() - o1.f1.intValue();
                            }
                        });

                        // 取排序后的前两名,构建输出结果
                        StringBuilder result = new StringBuilder();
                        result.append("========================================\n");
                        for (int i = 0; i < 2; i++) {
                            Tuple2<String, Long> temp = mapList.get(i);
                            String info = "浏览量 No." + (i + 1) +
                                    "  url:" + temp.f0 +
                                    "  浏览量:" + temp.f1 +
                                    "  窗口结束时间:" + new Timestamp(context.window().getEnd()) + "\n";
                            result.append(info);
                        }
                        result.append("========================================\n");
                        out.collect(result.toString());
                    }
                });
        result.print();
        env.execute();
    }
}

7.4.2 使用KeyedProcessFunction

直接将所有数据放在一个分区上进行开窗操作。这相当于将并行度强行设置为 1,在实际应用中是要尽量避免的。

思路:

(1)读取数据源

(2)提取时间戳并生成水位线

(3)按照url进行keyBy分区

(4)开长度为10s步长为5的滑动窗口

(5)使用增量聚合函数 AggregateFunction,并结合全窗口函数 WindowFunction 进行窗口聚合,得到每个 url、在每个统计窗口内的浏览量,包装成 UrlViewCount

(6)按照窗口进行 keyBy 分区操作

(7)对同一窗口的统计结果数据,使用 KeyedProcessFunction 进行收集并排序输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7iv6fbOt-1693232836517)(第七章处理函数.assets/image-20230406003916609.png)]

// 自定义增量聚合
public class UrlViewCountAgg implements AggregateFunction<Event, Long, Long> {
    @Override
    public Long createAccumulator() {
        return 0L;
    }

    @Override
    public Long add(Event event, Long accumulator) {
        return accumulator + 1;
    }

    @Override
    public Long getResult(Long accumulator) {
        return accumulator;
    }

    @Override
    public Long merge(Long aLong, Long acc1) {
        return null;
    }
}

便于按窗口统计

public class UrlViewCount {
    public String url;
    public Long count;
    public Long windowStart;
    public Long windowEnd;

    public UrlViewCount() {
    }

    public UrlViewCount(String url, Long count, Long windowStart, Long windowEnd) {
        this.url = url;
        this.count = count;
        this.windowStart = windowStart;
        this.windowEnd = windowEnd;
    }

    @Override
    public String toString() {
        return "UrlViewCount{" +
                "url='" + url + '\'' +
                ", count=" + count +
                ", windowStart=" + windowStart +
                ", windowEnd=" + windowEnd +
                '}';
    }
}

窗口聚合函数

public class UrlViewCountResult extends ProcessWindowFunction<Long, UrlViewCount, String, TimeWindow> {
    @Override
    public void process(String url, ProcessWindowFunction<Long, UrlViewCount, String, TimeWindow>.Context context, Iterable<Long> elements, Collector<UrlViewCount> out) throws Exception {
        long start = context.window().getStart();
        long end = context.window().getEnd();
        System.out.println(url);
        System.out.println(elements);
        out.collect(new UrlViewCount(url, elements.iterator().next(), start, end));
    }
}

排序取TopN

public class TopN extends KeyedProcessFunction<Long, UrlViewCount, String> {
    private Integer n;
    // 定义一个列表状态
    private ListState<UrlViewCount> urlViewCountListState;

    public TopN(Integer n) {
        this.n = n;
    }

    @Override
    public void open(Configuration parameters) throws Exception {
        // 从环境中获取列表状态句柄
        urlViewCountListState = getRuntimeContext().getListState(
                new ListStateDescriptor<UrlViewCount>("url-view-count-list", Types.POJO(UrlViewCount.class)));
    }

    @Override
    public void processElement(UrlViewCount value, KeyedProcessFunction<Long, UrlViewCount, String>.Context ctx, Collector<String> out) throws Exception {
        // 将 count 数据添加到列表状态中,保存起来
        urlViewCountListState.add(value);
        // 注册 window end + 1ms 后的定时器,等待所有数据到齐开始排序
        ctx.timerService().registerEventTimeTimer(ctx.getCurrentKey() + 1);
    }

    @Override
    public void onTimer(long timestamp, KeyedProcessFunction<Long, UrlViewCount, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
        // 将数据从列表状态变量中取出,放入 ArrayList,方便排序
        ArrayList<UrlViewCount> urlViewCountArrayList = new ArrayList<>();
        for (UrlViewCount urlViewCount : urlViewCountListState.get()) {
            urlViewCountArrayList.add(urlViewCount);
        }

        // 清空状态,释放资源
        urlViewCountListState.clear();

        // 排 序
        urlViewCountArrayList.sort(new Comparator<UrlViewCount>(){
            @Override
            public int compare(UrlViewCount o1, UrlViewCount o2) {
                return o2.count.intValue() - o1.count.intValue();
            }
        });

        // 取前两名,构建输出结果
        StringBuilder result = new StringBuilder(); result.append("========================================\n");
        result.append("窗口结束时间:" + new Timestamp(timestamp - 1) + "\n");
        for (int i = 0; i < this.n; i++) {
            UrlViewCount UrlViewCount = urlViewCountArrayList.get(i); String info = "No." + (i + 1) + " "
                    + "url:" + UrlViewCount.url + " "
                    + "浏览量:" + UrlViewCount.count + "\n"; result.append(info);
        } result.append("========================================\n");
        out.collect(result.toString());

    }
}

主方法:

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

        // 从自定义数据源读取数据
        SingleOutputStreamOperator<Event> eventStream	=	env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element,	long recordTimestamp) {
                                return element.getTimestamp();
                            }
                        }));

        SingleOutputStreamOperator<UrlViewCount> urlCountStream = eventStream.keyBy(Event::getUrl)
                .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                .aggregate(new UrlViewCountAgg(), new UrlViewCountResult());

        SingleOutputStreamOperator<String> result = urlCountStream.keyBy(data -> data.windowEnd).process(new TopN(2));

        result.print();
        env.execute();

    }
}

其实这里面是可以优化的。每次其实是把所有url——count都会发过来,保存到一个列表状态中。虽然只是一个窗口的,但是如果数据量大的话还是可以优化的。

7.5 侧输出流

处理函数还有另外一个特有功能,就是将自定义的数据放入“侧输出流”(side output)输出。这个概念我们并不陌生,之前在讲到窗口处理迟到数据时,最后一招就是输出到侧输出流。而这种处理方式的本质,其实就是处理函数的侧输出流功能。

我们之前讲到的绝大多数转换算子,输出的都是单一流,流里的数据类型只能有一种。而侧输出流可以认为是“主流”上分叉出的“支流”,所以可以由一条流产生出多条流,而且这些流中的数据类型还可以不一样。利用这个功能可以很容易地实现“分流”操作。

具体应用时,只要在处理函数的.processElement()或者.onTimer()方法中,调用上下文的.output()方法就可以了

DataStream<Integer> stream = env.addSource(...);
SingleOutputStreamOperator<Long> process = eventStream.process(new ProcessFunction<Integer, Long>() {
    @Override
    public void processElement(Integer value, ProcessFunction<Integer, Long>.Context ctx, Collector<Long> out) throws Exception {
        out.collect(Long.valueOf(value));
        ctx.output(outputTag, "side-output: " + value);
    }
});

这里 output()方法需要传入两个参数,第一个是一个“输出标签”OutputTag,用来标识侧输出流,一般会在外部统一声明;第二个就是要输出的数据。

我们可以在外部先将 OutputTag 声明出来

OutputTag<String> outputTag = new OutputTag<String>("side-output") {};

如果想要获取这个侧输出流,可以基于处理之后的 DataStream 直接调用.getSideOutput() 方法,传入对应的 OutputTag,这个方式与窗口 API 中获取侧输出流是完全一样的。

DataStream<String> stringStream = longStream.getSideOutput(outputTag);

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

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

相关文章

【单片机】UART、I2C、SPI、TTL、RS232、RS422、RS485、CAN、USB、SD卡、1-WIRE、Ethernet等常见通信方式

在单片机开发中&#xff0c;UART、I2C、RS485等普遍在用&#xff0c;这里做一个简单的介绍 UART通用异步收发器 UART口指的是一种物理接口形式(硬件)。 UART是异步&#xff08;指不使用时钟同步&#xff0c;依靠帧长进行判断&#xff09;&#xff0c;全双工&#xff08;收发…

【Axure高保真原型】中继器网格图片拖动摆放

今天和大家分享中继器网格图片拖动摆放的原型模板&#xff0c;我们可以通过鼠标拖动来移动图片&#xff0c;拖动过程其他图标会根据图片拖动自动排列&#xff0c;松开鼠标是图片停放在指定位置&#xff0c;其他图标自动排列。那这个模板是用中继器制作的&#xff0c;所以使用也…

Redis——》Pipeline

推荐链接&#xff1a; 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…

内嵌功能强大、低功耗STM32WB55CEU7、STM32WB55CGU7 射频微控制器 - MCU, 48-UFQFN

一、概述&#xff1a; STM32WB55xx多协议无线和超低功耗器件内嵌功能强大的超低功耗无线电模块&#xff08;符合蓝牙 低功耗SIG规范5.0和IEEE 802.15.4-2011标准&#xff09;。该器件内含专用的Arm Cortex -M0&#xff0c;用于执行所有的底层实时操作。这些器件基于高性能Arm …

想与一个大佬探讨一下,他网站说的先验分布、后验分布,似乎不对?

2_probability 我对这个问题&#xff0c;是这么理解的&#xff1a; 几个网友&#xff0c;一开始都和我持不同的观点。感觉这是最基本的抽样检验问题啊。。。。。。。。。。。。。。。。。。。

Mybatis学习笔记(三)——Mybatis的配置(Mybatis-config.xml)

Mybatis学习笔记&#xff08;三&#xff09;——Mybatis的配置&#xff08;Mybatis-config.xml&#xff09; 传送门&#xff1a;Mybatis中文网——配置 Mybatis配置文档的顶层结构&#xff1a; configuration&#xff08;配置&#xff09; properties&#xff08;属性&#…

Linux用户与组管理(03)(八)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、组管理 1、概述 2、用户信息查看 总结 前言 今天是学习用户与组管理的最后一节课&#xff0c;这节课主要是组管理的内容&#xff0c;希望能一起学习&#xff…

C# 生成唯一ID

1.首先通过nuget安装yitter.idgenerator 下面的三行代码搞定

AP5192 DC-DC降压恒流LED汽车头灯摩托车电动车大灯电源驱动

AP5192是一款PWM工作模式,高效率、外围简单、 内置功率MOS管&#xff0c;适用于4.5-100V输入的高精度 降压LED恒流驱动芯片。最大电流1.5A。 AP5192可实现线性调光和PWM调光&#xff0c;线性调光 脚有效电压范围0.55-2.6V. AP5192 工作频率可以通过RT 外部电阻编程 来设定&…

19.CSS雨云动画特效

效果 源码 <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Cloud & Rain Animation</title><link rel="stylesheet" href="style.css"> </head> <bo…

考虑储能电池参与一次调频技术经济模型的容量配置方法(matlab代码)

目录 1 主要内容 储能参与调频原理 储能参与一次调频的充放电策略 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序复现文献《考虑储能电池参与一次调频技术经济模型的容量配置方法》模型&#xff0c;以调频效果最优为目标&#xff0c;考虑储能参与一次调频的充放电…

VMware虚拟机---Ubuntu无法连接网络该怎么解决?

在学习使用Linux系统时&#xff0c;由于多数同学们的PC上多是Windows系统&#xff0c;故会选择使用VMware创建一个虚拟机来安装Linux系统进行学习。 安装完成之后&#xff0c;在使用时总是会遇到各种各样的问题。本片随笔就主要针对可能出现的网络问题进行一个总结&#xff0c;…

记一次PlanUML时序图学习

记一次PlanUML时序图学习 前言插件效果代码及其属性解析解析actorparticipantqueueskinparam sequenceMessageAlign centerautonumber-->xnote overalt 总结 前言 最近因为工作需要学习了使用PlanUML画时序图&#xff0c;上一次学这个还是在大学的时候&#xff0c;以为这辈…

正则表达式 之 断言详解

正则表达式的先行断言和后行断言一共有 4 种形式&#xff1a; (?pattern) 零宽正向先行断言(zero-width positive lookahead assertion)(?!pattern) 零宽负向先行断言(zero-width negative lookahead assertion)(?<pattern) 零宽正向后行断言(zero-width positive lookb…

基于遗传算法 (Genetic Algorithm, GA) 实现N个城市环游最优路径规划计算

遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;是一种基于生物进化过程的优化算法&#xff0c;通过模拟自然界的遗传和进化机制&#xff0c;寻找问题的最优解。在解决复杂和多变量的优化问题中&#xff0c;遗传算法表现出良好的鲁棒性和全局搜索能力。 遗传…

C++ deque底层原理

deque底层原理 一、目的二、底层实现三、原理图四、类结构五、push_back六、pop_back 一、目的 实现双端数组 二、底层实现 双向开口的连续线性空间 三、原理图 四、类结构 class deque : protected Deque base _Deque_base._Deque_impl M_map 指针数组 _M_map_size …

Spring——Spring基础

文章目录 1. Spring架构2. RestController vs Controller3. Autowired和Resource的区别是啥4. Spring IOC & AOP4.1 谈谈自己对于 Spring IoC 和 AOP 的理解IoCAOP 4.2 Spring AOP 和 AspectJ AOP 有什么区别&#xff1f; 5. Spring bean5.1 Spring 中的 bean 的作用域有哪…

如何能使mp3的音量变大?

如何能使mp3的音量变大&#xff1f;我们经常在日常生活中使用的一种音频格式是MP3。许多朋友在下载音乐后&#xff0c;都会选择MP3格式进行播放。然而&#xff0c;在我们的日常生活中&#xff0c;我们有时会遇到音量太小的问题。这时候&#xff0c;我们听歌可能会感到很不舒服。…

MySQL项目迁移华为GaussDB PG模式指南

文章目录 0. 前言1. 数据库模式选择&#xff08;B/PG&#xff09;2.驱动选择2.1. 使用postgresql驱动2.1. 使用opengaussjdbc驱动 3. 其他考虑因素4. PG模式4.1 MySQL和OpenGauss不兼容的语法处理建议4.2 语法差异 6. 高斯数据库 PG模式JDBC 使用示例验证6. 参考资料 本章节主要…

Jmeter(二十七):BeanShell PostProcessor跨线程全局变量使用

在性能测试中&#xff0c;两个相关联的接口不一定都在同一个线程组&#xff0c;遇见这种情况时&#xff0c;我们要进行跨线程组传参&#xff0c;此处用登录和查询配送单两个请求举例&#xff1b; 1、登录请求中配置json提取器&#xff0c;将接口返回的token保存在变量中&#…