Hadoop-MapReduce

Hadoop-MapReduce

1、概述

1.1 分布式并行编程

数据处理能力提升的两条路线:

  • 单核cpu到双核到四核到六核。

  • 分布式并行编程。

  • 分布式计算

    • 将大的数据切分成多个小数据,交给更多的节点参与运算
  • 计算向数据靠拢

    • 将计算传递给有数据的节点上进行工作
传统并行计算框架MapReduce
集群架构/容错性共享式(共享内存/共享存储),容 错性差非共享式,容错性好
硬件/价格/扩展性刀片服务器、高速网、SAN, 价格贵,扩展性差普通PC机,便宜,扩展 性好
编程/学习难度what-how,难what,简单
适用场景实时、细粒度计算、计算密集型批处理、非实时、数据密 集型

1.2 MapReduce模型简介

  • MapReduce将复杂的、运行于大规模集群上的并行计算过程高度地抽 象到了两个函数:Map和Reduce
  • 编程容易,不需要掌握分布式并行编程细节,也可以很容易把自己的 程序运行在分布式系统上,完成海量数据的计算
  • MapReduce采用“分而治之”策略,一个存储在分布式文件系统中的 大规模数据集,会被切分成许多独立的分片(split),这些分片可以被 多个Map任务并行处理
  • MapReduce设计的一个理念就是“计算向数据靠拢”,而不是“数据 向计算靠拢”,因为,移动数据需要大量的网络传输开销
  • MapReduce框架采用了Master/Slave架构,包括一个Master和若干个Slave。 Master上运行JobTracker,Slave上运行TaskTracker
  • Hadoop框架是用Java实现的,但是,MapReduce应用程序则不一定 要用Java来写

1.3 Map和Reduce函数

函数输入输出说明
Map<k1,v1>
如: <行号,”a b c”>
List(<k2,v2>) 如:
<“a”,1>
<“b”,1>
<“c”,1>
1.将小数据集进一步解析成一批 对,输入Map函数中进行 处理
2.每一个输入的会输出一批 。是计算的中间结果
Reduce<k2,List(v2)>
如: <“a”,<1,1,1>>
<k3,v3>
<“a”,3>
输入的中间结果<k2,List(v2)>中的List(v2)表示是一批属于同一个k2的 value

2、MapReduce的体系结构

2.1、设计理念

  • map–>映射(key value)
  • reduce–>归纳
  • mapreduce必须构建在hdfs之上一种大数据离线计算框架
    • 在线:实时数据处理
    • 离线:数据处理时效性没有在线那么强,但是相对也需要很快得到结果
  • MapReduce不会马上得到结果,他会有一定的延时
    • 如果数据量小,使用mapreduce反而不合适
    • 杀鸡用牛刀
  • 原始数据–>map(Key,Value)–>Reduce
  • 分布式计算
    • 将大的数据切分成多个小数据,交给更多的节点参与运算
  • 计算向数据靠拢
    • 将计算传递给有数据的节点上进行工作

2.2、MapReduce 1.x

在这里插入图片描述

MapReduce主要有以下4个部分组成:

1)Client

  • 用户编写的MapReduce程序通过Client提交到JobTracker端
  • 用户可通过Client提供的一些接口查看作业运行状态

2)JobTracker

  • JobTracker负责资源监控和作业调度
  • JobTracker 监控所有TaskTracker与Job的健康状况,一旦发现失败,就 将相应的任务转移到其他节点
  • JobTracker 会跟踪任务的执行进度、资源使用量等信息,并将这些信息 告诉任务调度器(TaskScheduler),而调度器会在资源出现空闲时,选择合适的任务去使用这些资源。

3)TaskTracker

  • TaskTracker 会周期性地通过“心跳”将本节点上资源的使用情况和任 务的运行进度汇报给JobTracker,同时接收JobTracker 发送过来的命令 并执行相应的操作(如启动新任务、杀死任务等)
  • TaskTracker 使用“slot‖等量划分本节点上的资源量(CPU、内存等)。 一个Task 获取到一个slot 后才有机会运行,而Hadoop调度器的作用就是 将各个TaskTracker上的空闲slot分配给Task使用。slot 分为Map slot 和 Reduce slot 两种,分别供MapTask 和Reduce Task 使用

4)Task

  • Task 分为Map Task 和Reduce Task 两种,均由TaskTracker 启动

2.3、 MapReduce 2.x --Yarn

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • ResourceManager
    • 处理客户端请求
    • 启动/监控ApplicationMaster
    • 监控NodeManager
    • 资源分配与调度
  • NodeManager
    • 单个节点上的资源管理
    • 处理来自ResourceManger的命令
    • 处理来自ApplicationMaster的命令
  • ApplicationMaster
    • 为应用程序申请资源, 并分配给内部任务
    • 任务调度、监控与容错

3、MapReduce工作流程

3.1 工作流程概述

3.1.1、简略流程
  • 原始数据–>map(Key,Value)–>Reduce

在这里插入图片描述

在这里插入图片描述

4、 MapReduce 详解

在这里插入图片描述

4.1. 原始数据File

  • 1T数据被切分成块存放在HDFS上,每一个块有128M大小

4.2. 数据块Block

  • hdfs上数据存储的一个单元,同一个文件中块的大小都是相同的
  • 因为数据存储到HDFS上不可变,所以有可能块的数量和集群的计算能力不匹配
  • 我们需要一个动态调整本次参与计算节点数量的一个单位
  • 我们可以动态的改变这个单位–》参与的节点

4.3. 切片Split

  • 切片是一个逻辑概念
  • 在不改变现在数据存储的情况下,可以控制参与计算的节点数目
  • 通过切片大小可以达到控制计算节点数量的目的
  • 一个切片对应一个MapTask
  • 一般切片大小为Block的整数倍(2 1/2) 防止多余创建和很多的数据连接
  • 如果Split>Block ,计算节点少了 如果Split<block,计算节点多了
  • 默认情况下,Split切片的大小等于Block的大小 ,默认128M

4.4. MapTask(拆分计算)

在这里插入图片描述

  • map默认从所属切片读取数据,每次读取一行(默认读取器)到内存中
  • 我们可以根据自己书写的分词逻辑(空格分隔).计算每个单词出现的次数
  • 这是就会产生 (Map<String,Integer>)临时数据,存放在内存中
  • 但是内存大小是有限的,如果多个任务同时执行有可能内存溢出(OOM)
  • 如果把数据都直接存放到硬盘,效率太低
  • 我们需要在OOM和效率低之间提供一个有效方案
  • 可以现在内存中写入一部分,然后写出到硬盘

4.5. 环形数据缓冲区

  • 可以循环利用这块内存区域,减少数据溢写时map的停止时间
  • 每一个Map可以独享的一个内存区域
  • 在内存中构建一个环形数据缓冲区(kvBuffer),默认大小为100M
  • 设置缓冲区的阈值为80%,当缓冲区的数据达到80%开始向外溢写到硬盘
  • 溢写的时候还有20M的空间可以被使用效率并不会被减缓
  • 而且将数据循环写到硬盘,不用担心OOM问题

在这里插入图片描述

  • Kvbuffer就是数组
  • 当内存达到80%的时候,内存开始往硬盘写数据,Map也可以接着往内存中写数据。
  • 在有限空间内,高效工作。

4.6. 分区Partation

  • 根据Key直接计算出对应的Reduce
  • 分区的数量和Reduce的数量是相等的
  • hash(key) % partation = num 默认分区的算法是Hash然后取余
    • Object的hashCode()—equals()
    • 如果两个对象equals,那么两个对象的hashcode一定相等
    • 如果两个对象的hashcode相等,但是对象不一定equlas

4.7. 排序Sort

  • 对要溢写的数据进行排序(QuickSort 快速排序)
  • 按照先Partation后Key的顺序排序–>相同分区在一起,相同Key的在一起
  • 我们将来溢写出的小文件也都是有序的

4.8. 组合器combiner

  • ​ hadoop允许用户对map的输出数据进行处理,用户可自定义combiner函数(如同map函数和 reduce函数一般),其逻辑一般和reduce函数一样,combiner的输入是map的输出,combiner 的输出作为reduce的输入,很多情况下可以直接将reduce函数作为conbiner函数来使用 (job.setCombinerClass(FlowCountReducer.class);)。

  • combiner属于优化方案,所以无法确定combiner函数会调用多少次,可以在环形缓存区溢出文件 时调用combiner函数,也可以在溢出的小文件合并成大文件时调用combiner。但要保证不管调用 几次combiner函数都不会影响最终的结果,所以不是所有处理逻辑都可以使用combiner组件,有 些逻辑如果在使用了combiner函数后会改变最后rerduce的输出结果(如求几个数的平均值,就不 能先用combiner求一次各个map输出结果的平均值,再求这些平均值的平均值,这将导致结果错 误)。

  • combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量。

    • 原先传给reduce的数据是 a1 a1 a1 a1 a1
    • 第一次combiner组合之后变为a{1,1,1,1,…}
    • 第二次combiner后传给reduce的数据变为a{4,2,3,5…}

4.9. 溢写Spill

  • 将内存中的数据循环写到硬盘,不用担心OOM问题
  • 每次会产生一个80M的文件
  • 如果本次Map产生的数据较多,可能会溢写多个文件

4.10. 合并Merge

  • 因为溢写会产生很多有序(分区 key)的小文件,而且小文件的数目不确定
  • 后面向reduce传递数据带来很大的问题
  • 所以将小文件合并成一个大文件,将来拉取的数据直接从大文件拉取即可
  • 合并小文件的时候同样进行排序(归并排序),最终产生一个有序的大文件

4.11. 拉取Fetch

在这里插入图片描述

  • 我们需要将Map的临时结果拉取到Reduce节点
  • 原则:
    • 相同的Key必须拉取到同一个Reduce节点
    • 但是一个Reduce节点可以有多个Key
  • 未排序前拉取数据的时候必须对Map产生的最终的合并文件做全序遍历
    • 而且每一个reduce都要做一个全序遍历
    • 如果map产生的大文件是有序的,每一个reduce只需要从文件中读取自己所需的即可

4.12. 合并Merge

  • 因为reduce拉取的时候,会从多个map拉取数据
  • 那么每个map都会产生一个小文件,这些小文件(文件与文件之间无序,文件内部有序)
  • 为了方便计算(没必要读取N个小文件),需要合并文件
  • 归并算法合并成2个(qishishilia)
  • 相同的key都在一起

4.13. 归并Reduce

  • 将文件中的数据读取到内存中
  • 一次性将相同的key全部读取到内存中
  • 直接将相同的key得到结果–>最终结果

4.14. 写出Output

  • 每个reduce将自己计算的最终结果都会存放到HDFS上

5、 Shuffle过程详解

5.1 Shuffle流程

在这里插入图片描述

5.2、shuffle端详解

在这里插入图片描述

  • Shuffle :KvBuffer–Partation–Sort–Spill-Merge-Fetch-Merge

1.环形数据缓冲区

  • 当Map读取数据后,会计算产生临时结果,为了效率肯定在内存中进行
  • 但是计算的临时结果超过内存的大小,我们不得不将内存的数据临时存放到硬盘
  • 如果每次等到内存消耗完毕在写出到硬盘,会有一个阻塞的时间
  • 因为在写出的时候是不能继续计算的

2.分区 排序

  • 写出数据的时候我们需要对数据排序规则
  • 需要知道排序的算法

3.溢写

  • 将内存中已经排序好的数据写出到硬盘

4.合并

  • 合并溢写的文件方便拉取

5.拉取

  • 就是将Map阶段合并的数据块中的结果拉取到对应的Reduce

6.合并

  • 将从多个Map端拉取的数据合并到一起,方便操作

5.3、Map端Shuffle详解

在这里插入图片描述

  • 每个Map任务分配一个缓存

  • MapReduce默认100MB缓存

  • 设置溢写比例0.8

  • 分区默认采用哈希函数

  • 排序是默认的操作

  • 排序后可以合并(Combine)

  • 合并不能改变最终结果

  • 在Map任务全部结束之前进行归并

  • 归并得到一个大的文件,放在本地 磁盘

  • 文件归并时,如果溢写文件数量大于预定值(默认是3)则可以再次启 动Combiner,少于3不需要

  • JobTracker会一直监测Map任务的执 行,并通知Reduce任务来领取数据

  • 合并(Combine)和归并(Merge)的区别: 两个键值对<―a‖,1>和<―a‖,1>,

    • 如果合并,会得到<―a‖,2>,
    • 如果归并,会得到<―a‖,<1,1>>

5.4、Reduce端Shuffle详解

在这里插入图片描述

  • Reduce任务通过RPC向JobTracker询问Map任务是否已经完成,若完成,则拉去数据
  • Reduce领取数据先放入缓存,来自不同Map机器,先归并,再合并,写入磁盘
  • 多个溢写文件归并成一个或多个大文件,文件中的键值对是排序的
  • 当数据很少时,不需要溢写到磁盘,直接在缓存中归并,然后输出给Reduce

6、Hadoop搭建yarn环境

NameNode01NameNode02DateNodeZKFCZooKeeperJournalNodeResourceManagerNodemanager
node01*******
node02******
node03*****
  • yarn环境搭建基于前面HA环境

6.1. 切换工作目录

[root@node01 ~]# cd /opt/yjx/hadoop-3.1.2/etc/hadoop/

6.2. 修改集群环境

[root@node01 hadoop]# vim hadoop-env.sh
##继续添加用户信息
export JAVA_HOME=/usr/java/jdk1.8.0_231-amd64
export HDFS_NAMENODE_USER=root
export HDFS_DATANODE_USER=root
export HDFS_ZKFC_USER=root
export HDFS_JOURNALNODE_USER=root
export YARN_RESOURCEMANAGER_USER=root
export YARN_NODEMANAGER_USER=root

6.3. 修改配置文件

  • [root@node01 hadoop]# vim mapred-site.xml
    

插入到最后< Config ></ config>中

<!-- 指定mr框架为yarn方式 -->
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<!-- 指定mapreduce jobhistory地址 -->
<property>
<name>mapreduce.jobhistory.address</name>
<value>node01:10020</value>
</property>
<!-- 任务历史服务器的web地址 -->
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>node01:19888</value>
</property>
<!-- 配置运行过的日志存放在hdfs上的存放路径 -->
<property>
<name>mapreduce.jobhistory.done-dir</name>
<value>/history/done</value>
</property>
<!-- 配置正在运行中的日志在hdfs上的存放路径 -->
<property>
<name>mapreudce.jobhistory.intermediate.done-dir</name>
<value>/history/done/done_intermediate</value>
</property>
<property>
<name>mapreduce.application.classpath</name>
<value>
/opt/yjx/hadoop-3.1.2/etc/hadoop,
/opt/yjx/hadoop-3.1.2/share/hadoop/common/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/common/lib/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/hdfs/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/hdfs/lib/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/mapreduce/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/mapreduce/lib/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/yarn/*,
/opt/yjx/hadoop-3.1.2/share/hadoop/yarn/lib/*
</value>
</property>
  • [root@node01 hadoop]# vim yarn-site.xml
    
<!-- 开启RM高可用 -->
<property>
<name>yarn.resourcemanager.ha.enabled</name>
<value>true</value>
</property>
<!-- 指定RM的cluster id -->
<property>
<name>yarn.resourcemanager.cluster-id</name>
<value>yarn-yjx</value>
</property>
<!-- 指定RM的名字 -->
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
<!-- 分别指定RM的地址 -->
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>node01</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>node03</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address.rm1</name>
<value>node01:8088</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address.rm2</name>
<value>node03:8088</value>
</property>
<!-- 指定zk集群地址 -->
<property>
<name>yarn.resourcemanager.zk-address</name>
<value>node01:2181,node02:2181,node03:2181</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<!-- 开启日志聚合 -->
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<property>
<name>yarn.log-aggregation.retain-seconds</name>
<value>86400</value>
</property>
<!-- 启用自动恢复 -->
<property>
<name>yarn.resourcemanager.recovery.enabled</name>
<value>true</value>
</property>
<!-- 制定resourcemanager的状态信息存储在zookeeper集群上 -->
<property>
<name>yarn.resourcemanager.store.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateS
tore</value>
</property>
<!-- Whether virtual memory limits will be enforced for containers. -->
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
<property>
<name>yarn.nodemanager.vmem-pmem-ratio</name>
<value>3</value>
</property>

6.4. 拷贝到其他节点

[root@node01 hadoop]# scp yarn-site.xml mapred-site.xml root@node02:`pwd`
[root@node01 hadoop]# scp yarn-site.xml mapred-site.xml root@node03:`pwd`

6.5. 开启集群

[123]# zkServer.sh start
[root@node01 hadoop]# start-dfs.sh
[root@node01 hadoop]# start-yarn.sh 
[root@node01 hadoop]# mr-jobhistory-daemon.sh start historyserver

6.6. 关机拍摄快照

[1]# stop-all.sh
[1]# mr-jobhistory-daemon.sh stop historyserver
[123]# zkServer.sh stop

7、MapReduce案例

7.1. WordCount项目

英语单词统计;

寻找一片英语书,统计书中单词各单词数量。

将书.txt上传到hadoop的/yjx上。

7.1.1. Java实现
  • pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- Hadoop版本控制 -->
<hadoop.version>3.1.2</hadoop.version>
<!-- commons-io版本控制 -->
<commons-io.version>2.4</commons-io.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopcommon -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoophdfs -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopclient -->
<dependency>
    <groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopmapreduce-client-common -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopmapreduce-client-core -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopmapreduce-client-jobclient -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-jobclient</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
</dependencies>

  • job代码
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;
public class WordCountJob {
	public static void main(String[] args) throws IOException,ClassNotFoundException, InterruptedException {
		//获取配置文件
		Configuration configuration = new Configuration(true);
		//本地模式运行
		configuration.set("mapreduce.framework.name", "local");
		    //创建任务
		Job job = Job.getInstance(configuration);
		//设置任务主类
		job.setJarByClass(WordCountJob.class);
		//设置任务
		job.setJobName("yjx-wordcount-" + System.currentTimeMillis());
		//设置Reduce的数量
		job.setNumReduceTasks(2);
		//设置数据的输入路径
		FileInputFormat.setInputPaths(job, new Path("/yjx/harry.txt"));
		//设置数据的输出路径
		FileOutputFormat.setOutputPath(job, new
		Path("/yjx/result/wordcount_" + System.currentTimeMillis()));
		//设置Map的输入的key和value类型
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);
		//设置Map和Reduce的处理类
		job.setMapperClass(WordCountMapper.class);
		job.setReducerClass(WordCountReducer.class);
		//提交任务
		job.waitForCompletion(true);
	}
}
  • Job的Mapper代码
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
	public class WordCountMapper extends Mapper<LongWritable, Text, Text,IntWritable> {
		//创建对象
		private IntWritable one = new IntWritable(1);
		@Override
		protected void map(LongWritable key, Text value, Context context)
		throws IOException, InterruptedException {
		//替换特殊字符
		String valueString = value.toString();
		valueString = valueString.replaceAll("[^a-zA-Z0-9'\\s]", "");
		//切分字符串
		String[] values = valueString.split(" ");
		//向里面添加数据
		for (String val : values) {
			context.write(new Text(val), one);
		}
	}
}
  • Job的Reducer代码
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.Iterator;
	public class WordCountReducer extends Reducer<Text, IntWritable, Text,IntWritable> {
		@Override
		protected void reduce(Text key, Iterable<IntWritable> values,
		Context context) throws IOException, InterruptedException {
		//获取迭代器
		Iterator<IntWritable> iterator = values.iterator();
		//声明一个计数器
		int count = 0;
		while (iterator.hasNext()) {
			count += iterator.next().get();
		}
		//输出数据
		context.write(key, new IntWritable(count));
	}
}
7.1.2. MR执行的方式

Linux端执行方式

  • hadoop jar wordcount.jar com.yjx.wordcount.WordCountJob 
    

window端本地化执行

  • 拷贝Hadoop配置文件到resource中 (core-site.xml;hdfs-site.xml;mapred-site.xml;yarn-site.xml)
  • configuration.set(“mapreduce.framework.name”, “local”);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值