13.Time/Watermarker、Watermaker水印机制/水位线机制、Watermaker案例演示

本文来自:Flink1.12-2021黑马程序员贺岁视频 的学习笔记

13.Time/Watermarker
13.1.时间分类
13.2.EventTime的重要性和Watermaker的引入
13.3.Watermaker详解
13.3.1.Watermaker水印机制/水位线机制
13.3.1.1.什么是Watermaker ?
13.3.1.2.如何计算Watermaker?
13.3.1.3.Watermarker有什么用?
13.3.1.4.Watermaker如何触发窗口计算的?
13.4.Watermaker案例演示
13.5.代码演示-验证版-了解
13.6.代码演示-侧道输出解决数据丢失-掌握(Allowed Lateness案例)
13.6.1.需求和API

13.Time/Watermarker

13.1.时间分类

Time分类
在Flink的流式处理中,会涉及到时间的不同概念,如下图所示:
事件时间EventTime: 事件真真正正发生产生的时间。
摄入时间IngestionTime: 事件到达Flink的时间
处理时间ProcessingTime: 事件真正被处理/计算的时间

问题:上面的三个时间,我们更关注哪一个?
在这里插入图片描述

13.2.EventTime的重要性和Watermaker的引入

在这里插入图片描述
示例2:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

13.3.Watermaker详解

1.Watermaker本质是时间戳。
2.Watermaker = 当前进来的数据最大的事件时间 - 最大允许的数据延迟时间或乱序时间。
3.Watermaker可以通过改变窗口触发计算时机来解决一定程度上的数据乱序或延迟达到的问题。
4.Watermaker >= 窗口结束时间时触发窗口计算。
5.当前的最大的事件时间 - 最大允许的数据延迟时间或乱序时间>= 窗口结束时间时触发窗口计算。
6.当前的最大的事件时间 >= 窗口结束时间 +最大允许的数据延迟时间或乱序时间时触发窗口计算。

13.3.1.Watermaker水印机制/水位线机制

13.3.1.1.什么是Watermaker ?

Watermaker就是给数据再额外的加的一个时间列,也就是Watermaker是个时间戳。

13.3.1.2.如何计算Watermaker?

Watermaker = 数据的事件时间 - 最大允许的延迟时间或乱序时间。

注意:后面通过源码会发现,准确来说:
Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间。
这样可以保证Watermaker水位线会一直上升(变大),不会下下降。

13.3.1.3.Watermarker有什么用?

之前的窗口都是按照系统时间来触发计算的,如:[10:00:00 ~ 10:00:10)的窗口,一旦系统时间到了10:00:10就会触发计算,那么可能会导致延迟到达的数据丢失。

那么现在有了Watermaker,窗口就可以按照Watermaker来触发计算。
也就是说Watermaker是用来触发窗口计算的!

13.3.1.4.Watermaker如何触发窗口计算的?

1.窗口中有数据 2.Watermaker >= 窗口的结束时间。
Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间。
也就是说只要不断有数据来,就可以保证Watermaker水位线是会一直上升/变大的,不会下降/减小的。所以最终一定是会触发窗口计算的。

注意:
上面的触发公式进行如下变形:
Watermaker >= 窗口的结束时间。
Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间。
当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间 >= 窗口的结束时间。
当前窗口的最大的事件时间 >= 窗口的结束时间 + 最大允许的延迟时间或乱序时间。
在这里插入图片描述
在这里插入图片描述

13.4.Watermaker案例演示

在这里插入图片描述
https://ci.apache.org/projects/flink/flink-docs-release-1.12/dev/event_timestamps_watermarks.html

package day3.demo4;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.calcite.linq4j.Ord;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;

import java.time.Duration;
import java.util.Random;
import java.util.UUID;

/**
 * Desc 演示基于事件时间的窗口计算+Watermaker解决一定程度上的数据乱序/延迟到达的问题
 *
 * @author tuzuoquan
 * @date 2022/5/5 9:49
 */
public class WatermakerDemo01 {

    public static void main(String[] args) throws Exception {
        //TODO 0.env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);

        //TODO 1.source
        DataStreamSource<Order> orderDS = env.addSource(new SourceFunction<Order>() {
            private boolean flag = true;

            @Override
            public void run(SourceContext<Order> ctx) throws Exception {
                Random random = new Random();
                while (flag) {
                    String orderId = UUID.randomUUID().toString();
                    int userId = random.nextInt(2);
                    int money = random.nextInt(101);
                    //随机模拟延迟
                    long eventTime = System.currentTimeMillis() - random.nextInt(5) * 1000;
                    ctx.collect(new Order(orderId, userId, money, eventTime));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //TODO 2.transformation
        //老版本API
        /*DataStream<Order> watermakerDS = orderDS.assignTimestampsAndWatermarks(
                new BoundedOutOfOrdernessTimestampExtractor<Order>(Time.seconds(3)) {//最大允许的延迟时间或乱序时间
                    @Override
                    public long extractTimestamp(Order element) {
                        return element.eventTime;
                        //指定事件时间是哪一列,Flink底层会自动计算:
                        //Watermaker = 当前最大的事件时间 - 最大允许的延迟时间或乱序时间
                    }
        });*/
        //注意:下面的代码使用的是Flink1.12中新的API
        //每隔5s计算最近5s的数据求每个用户的订单总金额,要求:基于事件时间进行窗口计算+Watermaker
        //env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);//在新版本中默认就是EventTime
        //设置Watermaker = 当前最大的事件时间 - 最大允许的延迟时间或乱序时间
        SingleOutputStreamOperator<Order> orderDSWithWatermark = orderDS.assignTimestampsAndWatermarks(
                WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(3))//指定maxOutOfOrderness最大无序度/最大允许的延迟时间/乱序时间
                        .withTimestampAssigner((order, timestamp) -> order.getEventTime())//指定事件时间列
        );

        SingleOutputStreamOperator<Order> result = orderDSWithWatermark.keyBy(Order::getUserId)
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                .sum("money");

        //TODO 3.sink
        result.print();

        //TODO 4.execute
        env.execute();
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Order {
        private String orderId;
        private Integer userId;
        private Integer money;
        private Long eventTime;
    }

}
6> WatermakerDemo01.Order(orderId=0140f77b-e5a2-4849-8a95-5c4ef0a42191, userId=0, money=121, eventTime=1651766908579)
6> WatermakerDemo01.Order(orderId=94d637a9-d621-4f24-b192-3687c80a49c4, userId=1, money=99, eventTime=1651766908598)
6> WatermakerDemo01.Order(orderId=105873f2-e676-46e5-9c08-7dec812a4b38, userId=1, money=318, eventTime=1651766911618)
6> WatermakerDemo01.Order(orderId=05997f89-5065-4646-bbc9-27b797a93c5b, userId=0, money=79, eventTime=1651766911644)
6> WatermakerDemo01.Order(orderId=66bf589f-9e3e-43ff-8f40-441b75399300, userId=0, money=9, eventTime=1651766917699)
6> WatermakerDemo01.Order(orderId=feb8d902-660c-41ee-b721-a0bb1d063367, userId=1, money=236, eventTime=1651766916693)

13.5.代码演示-验证版-了解

通过源码和下面的代码可以验证
总结:
1.Watermaker本质是时间戳
2.Watermaker = 当前窗口的最大的事件时间 - 最大允许的数据延迟时间或乱序时间。
3.Watermaker可以通过改变窗口触发计算时机来解决一定程度上的数据乱序或延迟达到的问题。
4.Watermaker >= 窗口结束时间时触发窗口计算
5.当前窗口的最大的事件时间 - 最大允许的数据延迟时间或乱序时间 >= 窗口结束时间时触发窗口计算。
6.当前窗口的最大的事件时间 >= 窗口结束时间时触发窗口计算 + 最大允许的数据延迟时间或乱序时间。

package day3.demo5;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

/**
 * Desc
 * 模拟实时订单数据,格式为: (订单ID,用户ID,订单金额,时间戳/事件时间)
 * 要求每隔5s,计算5秒内(基于时间的滚动窗口),每个用户的订单总金额
 * 并添加Watermaker来解决一定程度上的数据延迟和数据乱序问题。
 *
 * @author tuzuoquan
 * @date 2022/5/6 0:22
 */
public class WatermakerDemo02_Check {

    public static void main(String[] args) throws Exception {
        FastDateFormat df = FastDateFormat.getInstance("HH:mm:ss");

        //TODO 1.env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //TODO 2.Source
        //模拟实时订单数据(数据有延迟和乱序)
        DataStreamSource<Order> orderDS = env.addSource(new SourceFunction<Order>() {
            private boolean flag = true;
            @Override
            public void run(SourceContext<Order> ctx) throws Exception {
                Random random = new Random();
                while (flag) {
                    String orderId = UUID.randomUUID().toString();
                    int userId = random.nextInt(3);
                    int money = random.nextInt(100);
                    //模拟数据延迟和乱序!
                    long eventTime = System.currentTimeMillis() - random.nextInt(5) * 1000;
                    System.out.println("发送的数据为: "+userId + " : " + df.format(eventTime));
                    ctx.collect(new Order(orderId, userId, money, eventTime));
                    //TimeUnit.SECONDS.sleep(1);
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //TODO 3.Transformation
        /*DataStream<Order> watermakerDS = orderDS
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                .withTimestampAssigner((event, timestamp) -> event.getEventTime())
                );*/

        //开发中直接使用上面的即可
        //学习测试时可以自己实现
        DataStream<Order> watermakerDS = orderDS
                .assignTimestampsAndWatermarks(
                        new WatermarkStrategy<Order>() {
                            @Override
                            public WatermarkGenerator<Order> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                                return new WatermarkGenerator<Order>() {
                                    private int userId = 0;
                                    private long eventTime = 0L;
                                    private final long outOfOrdernessMillis = 3000;
                                    private long maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;

                                    @Override
                                    public void onEvent(Order event, long eventTimestamp, WatermarkOutput output) {
                                        userId = event.userId;
                                        eventTime = event.eventTime;
                                        maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
                                    }

                                    @Override
                                    public void onPeriodicEmit(WatermarkOutput output) {
                                        //Watermaker = 当前最大事件时间 - 最大允许的延迟时间或乱序时间
                                        Watermark watermark = new Watermark(maxTimestamp - outOfOrdernessMillis - 1);
                                        System.out.println("key:" + userId + ",系统时间:" + df.format(System.currentTimeMillis()) + ",事件时间:" + df.format(eventTime) + ",水印时间:" + df.format(watermark.getTimestamp()));
                                        output.emitWatermark(watermark);
                                    }
                                };
                            }
                        }.withTimestampAssigner((order, timestamp) -> order.getEventTime())
                );


        //代码走到这里,就已经被添加上Watermaker了!接下来就可以进行窗口计算了
        //要求每隔5s,计算5秒内(基于时间的滚动窗口),每个用户的订单总金额
        /* DataStream<Order> result = watermakerDS
                 .keyBy(Order::getUserId)
                //.timeWindow(Time.seconds(5), Time.seconds(5))
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                .sum("money");*/

        //开发中使用上面的代码进行业务计算即可
        //学习测试时可以使用下面的代码对数据进行更详细的输出,如输出窗口触发时各个窗口中的数据的事件时间,Watermaker时间
        SingleOutputStreamOperator<String> result = watermakerDS
                .keyBy(Order::getUserId)
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                //把apply中的函数应用在窗口中的数据上
                //WindowFunction<IN, OUT, KEY, W extends Window>
                .apply(new WindowFunction<Order, String, Integer, TimeWindow>() {
                    @Override
                    public void apply(Integer key, TimeWindow window, Iterable<Order> orders, Collector<String> out) throws Exception {
                        //用来存放当前窗口的数据的格式化后的事件时间
                        List<String> list = new ArrayList<>();
                        for (Order order : orders) {
                            Long eventTime = order.eventTime;
                            String formatEventTime = df.format(eventTime);
                            list.add(formatEventTime);
                        }
                        String start = df.format(window.getStart());
                        String end = df.format(window.getEnd());
                        //现在就已经获取到了当前窗口的开始和结束时间,以及属于该窗口的所有数据的事件时间,把这些拼接并返回
                        String outStr = String.format("key:%s,窗口开始结束:[%s~%s),属于该窗口的事件时间:%s", key.toString(), start, end, list.toString());
                        out.collect(outStr);
                    }
                });

        //4.Sink
        result.print();

        //5.execute
        env.execute();
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Order {
        private String orderId;
        private Integer userId;
        private Integer money;
        private Long eventTime;
    }
}

输出结果:

发送的数据为: 0 : 01:07:53
key:0,系统时间:01:07:57,事件时间:01:07:53,水印时间:01:07:50
key:0,系统时间:01:07:57,事件时间:01:07:53,水印时间:01:07:50
key:0,系统时间:01:07:58,事件时间:01:07:53,水印时间:01:07:50
key:0,系统时间:01:07:58,事件时间:01:07:53,水印时间:01:07:50
发送的数据为: 1 : 01:07:54
key:1,系统时间:01:07:58,事件时间:01:07:54,水印时间:01:07:51
key:1,系统时间:01:07:58,事件时间:01:07:54,水印时间:01:07:51
key:1,系统时间:01:07:58,事件时间:01:07:54,水印时间:01:07:51
key:1,系统时间:01:07:59,事件时间:01:07:54,水印时间:01:07:51
key:1,系统时间:01:07:59,事件时间:01:07:54,水印时间:01:07:51
发送的数据为: 0 : 01:07:55
key:0,系统时间:01:07:59,事件时间:01:07:55,水印时间:01:07:52
key:0,系统时间:01:07:59,事件时间:01:07:55,水印时间:01:07:52
key:0,系统时间:01:07:59,事件时间:01:07:55,水印时间:01:07:52
key:0,系统时间:01:08:00,事件时间:01:07:55,水印时间:01:07:52
key:0,系统时间:01:08:00,事件时间:01:07:55,水印时间:01:07:52
发送的数据为: 1 : 01:07:57
key:1,系统时间:01:08:00,事件时间:01:07:57,水印时间:01:07:54
key:1,系统时间:01:08:00,事件时间:01:07:57,水印时间:01:07:54
key:1,系统时间:01:08:00,事件时间:01:07:57,水印时间:01:07:54
key:1,系统时间:01:08:01,事件时间:01:07:57,水印时间:01:07:54
key:1,系统时间:01:08:01,事件时间:01:07:57,水印时间:01:07:54
发送的数据为: 0 : 01:07:57
key:0,系统时间:01:08:01,事件时间:01:07:57,水印时间:01:07:54
key:0,系统时间:01:08:01,事件时间:01:07:57,水印时间:01:07:54
key:0,系统时间:01:08:02,事件时间:01:07:57,水印时间:01:07:54
key:0,系统时间:01:08:02,事件时间:01:07:57,水印时间:01:07:54
key:0,系统时间:01:08:02,事件时间:01:07:57,水印时间:01:07:54
发送的数据为: 0 : 01:08:02

13.6.代码演示-侧道输出解决数据丢失-掌握(Allowed Lateness案例)

13.6.1.需求和API

有订单数据,格式为:(订单ID,用户ID,时间戳/事件时间,订单金额)
要求每隔5s,计算5秒内,每个用户的订单总金额。
并添加Watermaker来解决一定程度上的数据延迟和数据乱序问题。

并使用OutputTag + allowedLateness解决数据丢失问题。(之前的窗口已经关闭的延迟严重的数据)
在这里插入图片描述

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.OutputTag;

import java.time.Duration;
import java.util.Random;
import java.util.UUID;

/**
 * desc 演示基于事件时间的窗口计算+Watermaker解决一定程度上的数据乱序/延迟到达的问题
 * 并使用outputTag + allowedLateness来解决数据丢失问题(解决迟到/延迟严重的数据的丢失问题)
 *
 * @author tuzuoquan
 * @date 2022/5/6 9:38
 */
public class WatermakerDemo03 {

    public static void main(String[] args) throws Exception {
        //TODO 0.env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);

        //TODO 1.source
        DataStreamSource<Order> orderDS = env.addSource(new SourceFunction<Order>() {
            private boolean flag = true;

            @Override
            public void run(SourceContext<Order> ctx) throws Exception {
                Random random = new Random();
                while (flag) {
                    String orderId = UUID.randomUUID().toString();
                    int userId = random.nextInt(2);
                    int money = random.nextInt(101);
                    //随机模拟延迟-有可能会很严重
                    long eventTime = System.currentTimeMillis() - random.nextInt(20) * 1000;
                    ctx.collect(new Order(orderId, userId, money, eventTime));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //TODO 2.transformation
        //注意:下面的代码使用的是Flink1.12中新的API
        //每隔5s计算最近5s的数据求每个用户的订单总金额,要求:基于事件时间进行窗口计算+Watermaker
        //env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);//在新版本中默认就是EventTime
        //设置Watermarker = 当前最大的事件时间 - 最大允许的延迟时间或乱序时间
        SingleOutputStreamOperator<Order> orderDSWithWatermark = orderDS.assignTimestampsAndWatermarks(
                WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(3))//指定maxOutOfOrderness最大无序度/最大允许的延迟时间/乱序时间
                        .withTimestampAssigner((order, timestamp) -> order.getEventTime())//指定事件时间列
        );

        //业务操作
        //TODO 准备一个outputTag用来存放迟到严重的数据
        OutputTag<Order> seriousLateOutputTag = new OutputTag<Order>("seriousLate", TypeInformation.of(Order.class));

        SingleOutputStreamOperator<Order> result1 = orderDSWithWatermark
                .keyBy(Order::getUserId)
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                .allowedLateness(Time.seconds(3))
                .sideOutputLateData(seriousLateOutputTag)
                .sum("money");
        DataStream<Order> result2 = result1.getSideOutput(seriousLateOutputTag);

        //TODO 3.sink
        result1.print("正常的/迟到不严重数据");
        result2.print("迟到严重的数据并丢弃后单独收集的数据");

        //TODO 4.execute
        env.execute();
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Order {
        private String orderId;
        private Integer userId;
        private Integer money;
        private Long eventTime;
    }

}

输出结果:

迟到严重的数据并丢弃后单独收集的数据:6> WatermakerDemo03.Order(orderId=8c0062a7-affc-4ca9-b7e9-7bda79991867, userId=0, money=47, eventTime=1651802593104)
迟到严重的数据并丢弃后单独收集的数据:6> WatermakerDemo03.Order(orderId=d36184b8-c95c-482f-8e2e-1aaeea1183bb, userId=0, money=77, eventTime=1651802596112)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=c06f39f0-115b-4671-8feb-bfec8784861f, userId=1, money=25, eventTime=1651802600134)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=658c1f4b-ad02-4afa-92ee-721b2c324be9, userId=0, money=93, eventTime=1651802603146)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=3b0491f2-24c1-4a09-88da-a48f46991025, userId=0, money=34, eventTime=1651802608078)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=f68ff325-7353-450e-86e9-9f7bfae98375, userId=1, money=104, eventTime=1651802609112)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=3b0491f2-24c1-4a09-88da-a48f46991025, userId=0, money=51, eventTime=1651802608078)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=94451969-7b10-4e2e-882b-51b17c0041a7, userId=1, money=27, eventTime=1651802613157)
正常的/迟到不严重数据:6> WatermakerDemo03.Order(orderId=660f398d-ce2b-46f2-b16b-fc63857b24f4, userId=0, money=78, eventTime=1651802613200)
迟到严重的数据并丢弃后单独收集的数据:6> WatermakerDemo03.Order(orderId=794b37ee-3fdd-46e1-b571-494dac51d997, userId=1, money=25, eventTime=1651802607219)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涂作权的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值