Hadoop基础学习---6、MapReduce框架原理

1、MapReduce框架原理

在这里插入图片描述

1.1 InputFormat数据输入
1.1.1 切片与MapTask并行度决定机制

1、问题引出
MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个job的处理速度。
2、MapTask并行度决定机制
数据块:Block是HDFS物理上吧数据分成一块一块。数据块是HDFS储存数据单位。

数据切片:数据切片只是在逻辑上对输出进行分片,并不会在磁盘上将其切分成片进行储存。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。

在这里插入图片描述

1.1.2 Job提交流程源码与切片源码详解

1、Job提交流程源码解析

waitForCompletion()
submit();
// 1 建立连接
connect();
// 1)创建提交 Job 的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是 yarn 集群运行环境
initialize(jobTrackAddr, conf); 
// 2 提交 job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的 Stag 路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取 jobid ,并创建 Job 路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝 jar 包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向 Stag 路径写 XML 配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交 Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(),
job.getCredentials());

Job提交流程源码解析
在这里插入图片描述
2、FileInputFormat切片源码解析
在这里插入图片描述

1.1.3 FileInputFormat 切片机制

1、切片机制
(1)简单地按照文件的内容长度进行切片
(2)切片大小。默认等于Block大小
(3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
2、案例分析
在这里插入图片描述
3、源码解析
(1)源码中计算切片大小的公式

Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此,默认情况下,切片大小=blocksize。

(2)切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
(3)获取切片信息API

// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
1.1.4 TextInputFormat

1、FileInputFormat实现类
FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、
NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。

2、TextInputFormat
TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text 类型。

1.1.5 CombineTextInputFormat 切片机制

框架默认的TextInputFormat切片机制时对任务按文件规划切片,不管文件多小,都会时一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。

1、应用场景:
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到
一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
2、虚拟储存切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job,4194304);//4M
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
3、切片机制
生成切片过程包括:虚拟储存过程和切片过程两部分。

在这里插入图片描述
(1)虚拟储存过程:
将输入目录下所以文件大小,依次和设置的setMaxInputSpiltSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置·的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值两倍,此时将文件均分成2个虚拟储存块(防止出现太小切片)。
例如setMaxInputSpiltSize值为4M,输入文件大小为8.02M,先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟储存文件,所以将剩余的4.02文件切分成(2.01M,2.01M)两个文件。
(2)切片过程:
(a)判断虚拟储存的文件大小是否大于setMaxInputSpiltSize值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟储存文件进行合并,共同形成一个切片。
(c)测试举例: 有 4 个小文件大小分别为 1.7M、5.1M、3.4M 以及 6.8M 这四个小
文件,则虚拟存储之后形成 6 个文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M 以及(3.4M、3.4M)
最终会形成三个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M

1.1.6 CombineTextInputFormat案例实操

1、需求
将输入的大量小文件合并成一个切片统一处理
(1)输入数据
在这里插入图片描述
(2)期望
期望一个切片处理四个文件
2、实现过程
(1)正常情况下,这是要生成4个切片的
(2)在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 1。

// 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置 20m   
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);

运行如果为 1 个切片:

number of splits:1

说明:为什么设置20M就是只有一个切片呢?
因为四个文件加起来都没有20M大(我们设置的MaxInputSpiltSize的大小)

1.2 MapReduce 工作流程

(1)
在这里插入图片描述
(2)在这里插入图片描述
上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:
(1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
(2)从内存缓冲区不断溢出到本地磁盘问就,可能会溢出多个文件
(3)多个溢出文件会被合并成大的溢出文件
(4)在溢出过程及合并过程中,都有调用Partitioner进行分区和针对key进行排序
(5)ReduceTask根据自己的分区号,去各个MapTask机器上取相对应的结果分区数据
(6)ReduceTask会抓取同一个分区的来着不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
(7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)

注意:
(1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,从原则上·说,缓冲器越大,磁盘IO的次数越少,执行的速度越快。
(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认是100M

1.3 Shuffle机制
1.3.1 Shuffle机制

Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。

在这里插入图片描述

1.3.2 Partition

1、问题引出
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照收集归属地不同省份输出到不同文件中。
2、默认Partitioner分区
在这里插入图片描述
默认分区时根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key储存到哪个分区

3、自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法

public class CustomPartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
// 控制分区代码逻辑
… …
return partition;
	}
}

(2)在驱动中,设置自定义Partitioner

job.setPartitionerClass(CustomPartitioner.class);

(3)自定义Partition后,要根据自定义Partitioner的逻辑设置相对应数量的ReduceTask

job.setNumReduceTasks(5); //我的逻辑分区数量是5

(4)分区总结
(a)如果ReduceTask的数量大于getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(b)如果1 小于 ReduceTask的数量 小于 getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(c)如 果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个
ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
(d)分区号必须从零开始,逐一累加。

1.3.3 Partition 分区案例实操

1、需求
将统计结果按照手机归属地不同省份输出到不同文件中(分区)
(1)输入数据文件(百度网盘自取数据文件)
链接:https://pan.baidu.com/s/1i2FdQTWFijkrr29n9xAj8Q
提取码:zhm6
(2)手机号 136、137、138、139 开头都分别放到一个独立的 4 个文件中,其他开头的放到
一个文件中。
2、需求分析
在这里插入图片描述
3、在序列化实操的基础上,增加一个分区类

package org.example.fenqu;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @ClassName MyPartition
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/21 11:29
 * @Version 1.0
 */
public class MyPartition extends Partitioner<Text,FlowBean> {
    @Override
    public int getPartition(Text text, FlowBean flowBean, int i) {
        String phone=text.toString();
        String prePhone=phone.substring(0,3);

        //定义分区编号
        int partition;
        if ("136".equals(prePhone)){
            partition=0;
        }else if ("137".equals(prePhone)){
            partition=1;
        }else if ("138".equals(prePhone)){
            partition=2;
        }else if ("139".equals(prePhone)){
            partition=3;
        }
        else {
            partition=4;
        }

        return partition;
    }
}


(4)在驱动函数中增加自定义数据分区设置和ReduceTask设置

package org.example.fenqu;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @ClassName MyPartition
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/21 11:29
 * @Version 1.0
 */
public class MyPartition extends Partitioner<Text,FlowBean> {
    @Override
    public int getPartition(Text text, FlowBean flowBean, int i) {
        String phone=text.toString();
        String prePhone=phone.substring(0,3);

        //定义分区编号
        int partition;
        if ("136".equals(prePhone)){
            partition=0;
        }else if ("137".equals(prePhone)){
            partition=1;
        }else if ("138".equals(prePhone)){
            partition=2;
        }else if ("139".equals(prePhone)){
            partition=3;
        }
        else {
            partition=4;
        }

        return partition;
    }
}


1.3.4 WritableComparable排序

1·、排序概述
排序是MapReduce框架中最重要的操作之一。

MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。

默认排序是按照字典顺序排序,且实现该排序的方法是快速排序

对于MapTask,它将会处理的结果暂时存放到环形缓冲区中,当环形缓冲区使用率达到一定阙值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所以文件进行归并排序。

对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阙值,则溢写到磁盘上,否者储存在内存中。如果磁盘上文件数目达到一定阙值,则进行一次归并排序以生成一个更大的文件;如果内存中文件大小或者数目超过一定的阙值,则进行一次合并后将数据溢写到磁盘上。当所以数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。

2、排序的分类
(1)部分排序
MapReduce根据输入记录的键值对数据集进行排序。保证输出的每个文件内部有序。
(2)全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
(3)辅助排序(GroupingComparator)
在Reduce端对key进行分组。应用于:在接受的key为bean对象的时候,想让一个或几个这段相同(全部字段比较不相同)的key进入到同一个Reduce方法,可以采用分组排序。
(4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

3、自定义排序WritableComparable 原理分析
bean对象作为key传输,需要实现WritableComparable接口重写compareTo()方法,就可以实现排序

@Override
public int compareTo(FlowBean bean) {
int result;
// 按照总流量大小,倒序排列
if (this.sumFlow > bean.getSumFlow()) {
result = -1;
}else if (this.sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
1.3.5 WritableComparable排序案例实操(全排序)

1、需求
根据序列化案例(上一篇文章最后一个案例)产生的结果再次对总流量进行倒序排序。
2、需求分析
在这里插入图片描述
3、代码实现
1、FlowBean对象在需求1基础上增加了比较功能

package org.example.paixu;

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @ClassName FlowBean
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/20 10:13
 * @Version 1.0
 */
public class FlowBean implements  WritableComparable<FlowBean> {
    private Long  upFlow;
    private Long downFlow;
    private long sumFlow;

    public FlowBean() {
    }

    public Long getUpFlow() {
        return upFlow;
    }

    public void setUpFlow(Long upFlow) {
        this.upFlow = upFlow;
    }

    public Long  getDownFlow() {
        return downFlow;
    }

    public void setDownFlow(Long  downFlow) {
        this.downFlow = downFlow;
    }

    public void setSumFlow() {
        this.sumFlow = this.upFlow+this.downFlow;
    }

    //实现序列化
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeLong(upFlow);
        dataOutput.writeLong(downFlow);
        dataOutput.writeLong(sumFlow);
    }
    //实现反序列化
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        this.upFlow=dataInput.readLong();
        this.downFlow=dataInput.readLong();
        this.sumFlow=dataInput.readLong();
    }

    @Override
    public String toString() {
        return upFlow+"\t"+downFlow+"\t"+sumFlow;
    }


    @Override
    public int compareTo(FlowBean o) {
        //倒序排列
        return this.sumFlow>o.sumFlow?-1:1;
    }
}


2、编写Mapper类

package org.example.paixu;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @ClassName FlowMapper
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/20 10:21
 * @Version 1.0
 */
public class FlowMapper extends Mapper<LongWritable,Text,FlowBean,Text> {
    private Text outV=new Text();
    private FlowBean outK=new FlowBean();

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
        String s = value.toString();
        String[] split = s.split("\t");

        String phone=split[0];
        outV.set(phone);
        outK.setUpFlow(Long.parseLong(split[1]));
        outK.setDownFlow(Long.parseLong(split[2]));
        outK.setSumFlow();

        context.write(outK,outV);
    }
}


3、编写Reducer类

package org.example.paixu;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @ClassName FlowReducer
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/20 10:29
 * @Version 1.0
 */
public class FlowReducer extends Reducer <FlowBean, Text,Text,FlowBean>{

    @Override
    protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
        for (Text value : values) {
            context.write(value,key);
        }
    }
}


4、编写Driver类

package org.example.paixu;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @ClassName FlowDriver
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/20 10:38
 * @Version 1.0
 */
public class FlowDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        //1、获取job对象
        Configuration configuration=new Configuration();
        Job job=Job.getInstance(configuration);

        //2、关联本Driver类
        job.setJarByClass(FlowDriver.class);


        //3、关联Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        //4、设置Map端输出KV类型
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);

        //5、设置程序最终输出的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class)

        //6、设置程序的输入和输出路径
        FileInputFormat.addInputPath(job,new Path("E:\\test\\output1"));
        FileOutputFormat.setOutputPath(job,new Path("E:\\test\\output2"));

        //7、提交Job
        System.exit(job.waitForCompletion(true)?0:1);
    }
}


1.3.6 WritableComparable排序案例实操(区内排序)

前提:这个案例和上一个案例区别不大,就是增加了分区而已,所以只需要增加自定义分区类和修改一个Driver类就行,像Mapper类和Reducer类是不用修改的。
(1)增加自定义分区类

package org.example.paixu;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @ClassName MyPartition
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/21 11:29
 * @Version 1.0
 */
public class MyPartition extends Partitioner< FlowBean,Text> {

    @Override
    public int getPartition(FlowBean flowBean, Text text, int i) {
        String phone=text.toString();
        String prePhone=phone.substring(0,3);

        //定义分区编号
        int partition;
        if ("136".equals(prePhone)){
            partition=0;
        }else if ("137".equals(prePhone)){
            partition=1;
        }else if ("138".equals(prePhone)){
            partition=2;
        }else if ("139".equals(prePhone)){
            partition=3;
        }
        else {
            partition=4;
        }
        return partition;
    }
}


(2)修改Driver

package org.example.paixu;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @ClassName FlowDriver
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/20 10:38
 * @Version 1.0
 */
public class FlowDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        //1、获取job对象
        Configuration configuration=new Configuration();
        Job job=Job.getInstance(configuration);

        //2、关联本Driver类
        job.setJarByClass(FlowDriver.class);


        //3、关联Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        //4、设置Map端输出KV类型
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);

        //5、设置程序最终输出的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);


        //8、指定自定义分区器
        job.setPartitionerClass(MyPartition.class);

        //9、同时指定相应数量的ReduceTask
        job.setNumReduceTasks(5);

        //6、设置程序的输入和输出路径
        FileInputFormat.addInputPath(job,new Path("E:\\test\\output1"));
        FileOutputFormat.setOutputPath(job,new Path("E:\\test\\output2"));

        //7、提交Job
        System.exit(job.waitForCompletion(true)?0:1);
    }
}


1.3.7 Combiner 合并
  • Combiner是MR程序中Mapper和Reducer之外的一种组件
  • Combiner组件的父类就是Reducer
  • Combiner和Reducer的区别就是运行位置
    • Combiner是在每一个MapTask所在的节点运行
    • Reducer是接受全局所以Mapper的输出结果
  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
  • Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combine的输出kv应该跟Reducer的输入kv类型要对应起来
    在这里插入图片描述

1、自定义Combiner实现步骤
(1)自定义一个Combiner继承Reducer,重写Reduce()方法

package org.example._07Combiner;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @ClassName CombinerClass
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/22 9:11
 * @Version 1.0
 */
public class CombinerClass extends Reducer<Text, IntWritable,Text, IntWritable> {
    private IntWritable outV=new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
        int sum=0;
        for (IntWritable value : values) {
            sum+=value.get();
        }
        outV.set(sum);
        context.write(key,outV);
    }
}


(2)在Job驱动类中设置

job.setCombinerClass(WordCountCombiner.class);
1.3.8 合并案例实操

1、需求(我的还是单词统计的代码)
统计过程中对每一个 MapTask 的输出进行局部汇总,以减小网络传输量即采用Combiner 功能。
(1)数据输入
自己创造一个就行了吧(到这应该会自己创造自己需要用的数据)
(2)期望数据输出
自己算吧

2、案例实操
(1)增加一个类继承Reducer

package org.example._07Combiner;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @ClassName CombinerClass
 * @Description TODO
 * @Author Zouhuiming
 * @Date 2023/5/22 9:11
 * @Version 1.0
 */
public class CombinerClass extends Reducer<Text, IntWritable,Text, IntWritable> {
    private IntWritable outV=new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
        int sum=0;
        for (IntWritable value : values) {
            sum+=value.get();
        }
        outV.set(sum);
        context.write(key,outV);
    }
}


(2)在驱动类中指定Combiner

 job.setCombinerClass(CombinerClass.class);

说明:其实这里也可以直接填以下代码

job.setCombinerClass(WordCountReduce.class);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

星光下的赶路人star

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

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

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

打赏作者

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

抵扣说明:

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

余额充值