Hadoop3.3.0入门到架构完结篇

GroupingComparator分组(辅助排序)

对Reduce阶段的数据根据某一个或某几个字段分组

步骤:

1.自定义类继承WritableComparator

2.重写compare()方法

3.自定义类无参构造中将比较对象传给父类 WritableComparator

protected OrderGroupingComparator() {
	super(Order.class, true);
}

求每一个订单中最贵的商品

eg:

订单id 商品id 金额
0000001 Pdt_01 222.8
0000001 Pdt_02 33.8
0000002 Pdt_05 722.4
0000002 Pdt_03 522.8
0000002 Pdt_04 122.4
0000003 Pdt_06 232.8
0000003 Pdt_02 33.8

订单id商品id金额
0000001Pdt_01222.8
0000002Pdt_05722.4
0000001Pdt_0233.8
0000003Pdt_06232.8
0000003Pdt_0233.8
0000002Pdt_03522.8
0000002Pdt_04122.4

期望得到结果:

1 222.8
2 722.4
3 232.8

代码案例目录 : hadoop-demo/src/main/java/com/example/mapreduce/grouping_comparator/

项目地址:文末

MapTask工作机制

在这里插入图片描述
( 1 ) Read阶段:MapTask 通过RecordReader ,从输入InputSplit中解析出一个个<K1,V1>.
( 2 ) Map阶段:该节点主要是将解析出的<K1,V1>交给用户编写map()函数处理,并产生一系列新的<K2,V2>.
( 3 ) Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果. 在该函数内部,它会将生成的<K2,V2>分区( 调用Partitioner) ,并写入一个环形内存缓冲区中.
( 4 ) Spil阶段:即“溢写”, 当环形缓冲区满后, MapReduce会将数据写到本地磁盘上, 生成一个临时文件. 需要注意的是, 将数据写入本地磁盘之前, 先要对数据进行一次本地排序, 并在必要时对数据进行合并、压缩等操作.
溢写阶段详情:
步骤1:利用快速排序算法对缓存区内的数据进行排序, 排序方式是, 先按照分区编号Partition进行排序, 然后按照key进行排序. 这样, 经过排序后, 数据以分区为单位聚集在一起, 且同一分区内所有数据按照key有序.
步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out( N表示当前溢写次数) 中. 如果用户设置了Combiner, 则写入文件之前, 对每个分区中的数据进行一次聚集操作.
步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中, 其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小. 如果当前内存索引大小超过1MB, 则将内存索引写到文件output/spillN.out.index中.
( 5 ) Combine阶段:当所有数据处理完成后, MapTask对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件.
当所有数据处理完后, MapTask会将所有临时文件合并成一个大文件, 并保存到文件output/file.out中, 同时生成相应的索引文件output/file.out.index.
在进行文件合并过程中, MapTask以分区为单位进行合并. 对于某个分区, 它将采用多轮递归合并的方式. 每轮合并io.sort.factor(默认10) 个文件, 并将产生的文件重新加入待合并列表中, 对文件排序后, 重复以上过程, 直到最终得到一个大文件.
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销.

ReduceTask工作机制

在这里插入图片描述
(1) Copy阶段: ReduceTask从各个MapTask上远程拷贝一片数据, 并针对某一片数据, 如果其大小超过一定阈值, 则写到磁盘上, 否则直接放到内存中.**
(2) Merge阶段: 在远程拷贝数据的同时, ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多.
(3) Sort阶段: 按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据. 为了将key相同的数据聚在一起, Hadoop采用了基于排序的策略. 由于各个MapTask已经实现对自己的处理结果进行了局部排序, 因此, ReduceTask只需对所有数据进行一次归并排序.
(4) Reduce阶段:reduce()函数将计算结果写到HDFS上.

设置ReduceTask并行度(个数)

//默认值:1   手动设置ReduceTask数量:5
job.setNumReduceTasks(5);

ReduceTask=0;//表示没有reduce阶段,输出文件个数和map个数一致
ReduceTask=1;//默认值为1,输出文件个数为1
//分区数不是1,ReduceTask=1; 不执行分区过程, 分区数>ReduceNum个数,才执行分区

OutputFormat数据输出

OutputFormat接口实现类

在这里插入图片描述

OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都继承了OutPutFormat抽象类

常用OutputFormat实现类:

(1) TextOutPutFormat文本输出(默认输出格式: TextOutPutFormat)

把每一条记录写为文本行, 键值可以是任意类型,会调用它们的toString()方法得到String.

(2) SquenceFileOutPutFormat

将SquenceFileOutPutFormat输出作为MapReduce任务的输入,输出格式紧凑,容易压缩.

(3) 自定义OutPutFormat

自定义输出

自定义FileOutPutFormat

控制最终文件的输出路径和输出格式,可以自定义OutPutFormat

案例: 根据数据的不同输出结果到不同的目录
步骤:

  1. 自定义类继承FileOutPutFormat

  2. 重写写RecordWriter.write(),更换输出路径…

eg(含有baidu的域名输出到一个文件,其余一个文件):

log.txt

http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.qq.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com

期望结果 :

baidu.log other.log

代码案例目录:

hadoop-demo/src/main/java/com/example/mapreduce/custom_output_format/

项目地址:文末

Join多种应用

Reduce Join

Map端主要工作 : 为来自不同表或文件的<K,V>对,打标签以区别不同来源的记录.然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出.

Reduce端的主要工作: 在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源不同文件的记录(在Map阶段已经打标志)分开,最后进行合并处理.

案例 :

order.txt

id pid amount

1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6

pd.txt

pid pname

01 小米
02 华为
03 格力

期望最终数据形式

id pname amount

1001 小米 1
1002 小米 4
1003 华为 2
1004 华为 5
1005 格力 3
1006 格力 6

//通过关联条件作为Map输出的key,将两表满足Join条件的数据携带数据来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联

缺点: 这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段产生数据倾斜.

解决办法 : Map端实现数据合并

MapJoin

使用场景

Map Join 适用于一张小表(较小,放内存中),一张大的表的场景.

优点: 在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据压力,尽可能减少数据倾斜.

步骤 :

(1) 在Mapper的setup阶段,将文件读取到缓存集合中.

(2) 在驱动类中加载缓存

//缓存普通文件到MapTask运行节点
job.addCacheFile(new URI("file:///home/file/aaa.txt"));

核心配置:

//Mapper类
//重写setup方法
// 1 获取缓存的文件
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));


// 驱动类
// 6 加载缓存数据
job.addCacheFile(new URI("file:///e:/input/inputcache/pd.txt"));

// 7 Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);

计数器应用

Hadoop为每个作业维护若干内置计数器

计数器API

(1) 采用枚举的方式统计技术

(2) 采用计数器组,计数器名称的方式统计

(3) 计数结果在程序运行后的控制台上看看

数据清洗(ETL)

数据清理过程一般只需运行Mapper程序,不需要Reduce

简单eg:

web.log 清除字段长度小于等于11的日志

Mapper类

核心类:

//Driver类
// 设置reducetask个数为0
job.setNumReduceTasks(0);

清洗复杂案例:

eg: 对日志中各字段识别区分

MapReduce开发总结

  1. 输入数据接口 : InputFormat

    (1) 默认使用实TextInputFormat

    (2) TextInputFormat:一次读一行文本,<该行的起始偏移量,该行内容>

    (3) KeyValueInputFormat: 每一行均为一条记录,被分隔符分隔为key,value;默认是\t

    (4) NLineInputFormat 按照指定的行数N来划分切片.

    (5) CombineInputFormat 可以把多个小文件合并成一个切片处理提高处理效率.

    (6) 用户可以自定义InputFormat

  2. 逻辑处理接口Mapper

    map() setup() cleanup()

  3. Partition分区

    (1)默认实现HashPartitioner,逻辑是根据可以的哈希值和numReduces来返回一个区号; key.hashCode()&Integer.MaxVALUE%numReduces

    (2) 可以自定义

  4. Comparable排序

    (1) 自定义对象作为key输出时,必须实现WritableComparable接口,重写CompareTo方法

    (2) 部分排序: 对最终输出的每一个文件进行内部排序,通常只有一个Reduce

    (3) 全排序: 对所有数据进行排序,通常只有一个Reduce.

    (4) 二次排序: 排序的条件有两个

  5. Combiner合并

    Combiner合并可以提高程序的执行效率,减少IO传输.但是使用是必须不能影响原有的业务处理结果.

  6. Reduce端分组 : GroupingComparator

    在Reduce端对key进行分组.应用于 : 在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序.

  7. 逻辑处理接口: Reducer

    reduce() setup() cleanup()

  8. 输出数据接口 : OutputFormat

    (1) 默认实现TextOutputFormat,将每一个<K,V>向目标文件输出一行.

    (2) 将SequenceFileOutputFormat 输出作为后续MapReduce任务的输入,这便是一种好的格式,格式紧凑,容易压缩.

    (3) 用户自定义OutputFormat

Hadoop数据压缩

Shuffle和Merge花费时间较长,尤其数据规模很大和工作负载密集的情况.

数据压缩可以节省资源,最小化磁盘IO和网络传输,可以在任意NapReduce阶段启用压缩

通过对Mapper,Reducer运行过程的数据进行压缩,减少磁盘IO,提高MR运行速度.

注意 : 采用压缩技术减少了磁盘IO,但增加了CPU运算负担.适当运用才能提高性能

研所原则 :

(1) 运算密集型的job,少用压缩

(2) IO密集型的job,多用压缩

MR支持的压缩编码

压缩格式hadoop自带?算法文件扩展名是否可切分换成压缩格式后,原来的程序是否需要修改
DEFLATE是,直接使用DEFLATE.deflate和文本处理一样,不需要修改
Gzip是,直接使用DEFLATE.gz和文本处理一样,不需要修改
bzip2是,直接使用bzip2.bz2 和文本处理一样,不需要修改
LZO否,需要安装LZO.lzo 需要建索引,还需要指定输入格式
Snappy否,需要安装Snappy.snappy和文本处理一样,不需要修改
压缩格式对应的编码/解码器
DEFLATEorg.apache.hadoop.io.compress.DefaultCodec
gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec
LZOcom.hadoop.compression.lzo.LzopCodec
Snappyorg.apache.hadoop.io.compress.SnappyCodec
压缩算法原始文件大小压缩文件大小压缩速度解压速度
gzip8.3GB1.8GB17.5MB/s58MB/s
bzip28.3GB1.1GB2.4MB/s9.5MB/s
LZO8.3GB2.9GB49.3MB/s74.6MB/s

http://google.github.io/snappy/

On a single core of a Core i7 processor in 64-bit mode, Snappy compresses at about 250 MB/sec or more and decompresses at about 500 MB/sec or more.

Gzip压缩

有点 : 压缩效率较高,压缩解压速度较快;Hadoop本身支持 ,大部分Linux自带Gzip命令

缺点 : 不支持Split

应用场景 : 当每个文件压缩后在130M以内(1个块大小内),够可以考虑Gzip压缩格式

eg:

一天/一个小时的日志压缩成一个Gzip

Bzip2压缩

优点 : 支持Split;有很高的压缩率,比Gzip压缩率高,Hadoop本身自带,使用方便

缺点 : 压缩解压速度慢

应用场景 : 适合速度要求不高,但需要较高压缩率的时候;

或者输出之后的数据比较大,处理之后的数据需要压缩存档j减少磁盘存储空间并且以后数据用得比较少的情况;

或者对单个文本文件想压缩减少存储空间,同时又需要支持Split,而且兼容之前的应用程序的情况.

Lzo压缩

优点 : 压缩/解压较快,合理压缩率,支持Split是Hadoop中最流行的压缩格式;可以在Linux下安装lzop命令,使用方便

缺点 : 压缩率比Gzip要第一些,Hadoop本身不支持,需要安装;在应用中对Lzo格式的文件需要做一些特殊处理(为了支持Split需要创建索引,还需要指定InputFormat为Lzo格式)

应用场景 : 一个很大的文本文件,压缩之后还大于200M以上的可以考虑,而且单个文件越大,Lzo优点越明显

Snappy压缩

优点 : 高速压缩速度和合理的压缩率

缺点 : 不支持Split; 压缩率比Gzip低;Hadoop本省不支持,需要安装.

应用场景: 当MapReduce作业的Map输出的数据比较大的时候,作为Map到Reduce的中间数据的压缩格式;或者作为一个MapReduce作业的输出和另外一个MapReduce作业的输入

压缩位置的选择

压缩可以在MapReduce作用的任意阶段启用.
在这里插入图片描述

参数默认值阶段建议
io.compression.codecs (在core-site.xml中配置)org.apache.hadoop.io.compress.DefaultCodec, org.apache.hadoop.io.compress.GzipCodec, org.apache.hadoop.io.compress.BZip2Codec输入压缩Hadoop使用文件扩展名判断是否支持某种编解码器
mapreduce.map.output.compress(在mapred-site.xml中配置)falsemapper输出这个参数设为true启用压缩
mapreduce.map.output.compress.codec(在mapred-site.xml中配置)org.apache.hadoop.io.compress.DefaultCodecmapper输出企业多使用LZO或Snappy编解码器在此阶段压缩数据
mapreduce.output.fileoutputformat.compress(在mapred-site.xml中配置)falsereducer输出这个参数设为true启用压缩
mapreduce.output.fileoutputformat.compress.codec(在mapred-site.xml中配置)org.apache.hadoop.io.compress. DefaultCodecreducer输出使用标准工具或者编解码器,如gzip和bzip2
mapreduce.output.fileoutputformat.compress.type(在mapred-site.xml中配置)RECORDreducer输出SequenceFile输出使用的压缩类型:NONE和BLOCK

数据流的压缩和解压缩案例

CompressionCodec有两个方法可以用于轻松的压缩和解压数据

要想对正在被写入一个输出流的数据进行压缩,我们可以使用createOutputStream(OutputStreeam out)方法创建一个CompressionOutputStream,将其以压缩格式写入底层的流

相反,要想对从输入流读取而来的数据进行解压缩,则调用createInputStream(InputStream in)函数,从而获取一个CompressionInputStream,从而从底层的读取未压缩的数据.

DEFLATEorg.apache.hadoop.io.compress.DefaultCodec
gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec

案例 :

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.io.compress.CompressionInputStream;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;

public class TestCompress {

	public static void main(String[] args) throws Exception {
		compress("e:/hello.txt","org.apache.hadoop.io.compress.BZip2Codec");
//		decompress("e:/hello.txt.bz2");
	}

	// 1、压缩
	private static void compress(String filename, String method) throws Exception {
		
		// (1)获取输入流
		FileInputStream fis = new FileInputStream(new File(filename));
		
		Class codecClass = Class.forName(method);
		
		CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, new Configuration());
		
		// (2)获取输出流
		FileOutputStream fos = new FileOutputStream(new File(filename + codec.getDefaultExtension()));
		CompressionOutputStream cos = codec.createOutputStream(fos);
		
		// (3)流的对拷
		IOUtils.copyBytes(fis, cos, 1024*1024*5, false);
		
		// (4)关闭资源
		cos.close();
		fos.close();
fis.close();
	}

	// 2、解压缩
	private static void decompress(String filename) throws FileNotFoundException, IOException {
		
		// (0)校验是否能解压缩
		CompressionCodecFactory factory = new CompressionCodecFactory(new Configuration());

		CompressionCodec codec = factory.getCodec(new Path(filename));
		
		if (codec == null) {
			System.out.println("cannot find codec for file " + filename);
			return;
		}
		
		// (1)获取输入流
		CompressionInputStream cis = codec.createInputStream(new FileInputStream(new File(filename)));
		
		// (2)获取输出流
		FileOutputStream fos = new FileOutputStream(new File(filename + ".decoded"));
		
		// (3)流的对拷
		IOUtils.copyBytes(cis, fos, 1024*1024*5, false);
		
		// (4)关闭资源
		cis.close();
		fos.close();
	}
}

Map输出端采用压缩

即使你的MapReduce的输入输出文件都是未压缩的文件,你仍然可以对Map任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到Reduce节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可,我们来看下代码怎么设置.

BZip2Codec 、DefaultCodec

import java.io.IOException;
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.io.compress.BZip2Codec;	
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.GzipCodec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordCountDriver {

	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

		Configuration configuration = new Configuration();

		// 开启map端输出压缩
	configuration.setBoolean("mapreduce.map.output.compress", true);
		// 设置map端输出压缩方式
	configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);

		Job job = Job.getInstance(configuration);

		job.setJarByClass(WordCountDriver.class);

		job.setMapperClass(WordCountMapper.class);
		job.setReducerClass(WordCountReducer.class);

		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);

		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);

		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		boolean result = job.waitForCompletion(true);

		System.exit(result ? 1 : 0);
	}
}

Mapper保持不变

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{

Text k = new Text();
	IntWritable v = new IntWritable(1);

	@Override
	protected void map(LongWritable key, Text value, Context context)throws IOException, InterruptedException {

		// 1 获取一行
		String line = value.toString();

		// 2 切割
		String[] words = line.split(" ");

		// 3 循环写出
		for(String word:words){
k.set(word);
			context.write(k, v);
		}
	}
}

Reducer保持不变

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

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{

	IntWritable v = new IntWritable();

	@Override
	protected void reduce(Text key, Iterable<IntWritable> values,
			Context context) throws IOException, InterruptedException {
		
		int sum = 0;

		// 1 汇总
		for(IntWritable value:values){
			sum += value.get();
		}
		
        v.set(sum);

        // 2 输出
		context.write(key, v);
	}
}

Reduce输出端采用压缩

基于WordCount案例处理

修改驱动

import java.io.IOException;
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.io.compress.BZip2Codec;
import org.apache.hadoop.io.compress.DefaultCodec;
import org.apache.hadoop.io.compress.GzipCodec;
import org.apache.hadoop.io.compress.Lz4Codec;
import org.apache.hadoop.io.compress.SnappyCodec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordCountDriver {

	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
		
		Configuration configuration = new Configuration();
		
		Job job = Job.getInstance(configuration);
		
		job.setJarByClass(WordCountDriver.class);
		
		job.setMapperClass(WordCountMapper.class);
		job.setReducerClass(WordCountReducer.class);
		
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);
		
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));
		
		// 设置reduce端输出压缩开启
		FileOutputFormat.setCompressOutput(job, true);
		
		// 设置压缩的方式
	    FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class); 
//	    FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class); 
//	    FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class); 
	    
		boolean result = job.waitForCompletion(true);
		
		System.exit(result?1:0);
	}
}

Hadoop总结就到此结束了,欢迎大家一起探讨学习~经过了2020才深悟,才知道要学会记录生活,哈哈哈…最后一句话,与君共勉:
将来的你一定感谢现在奋斗的你,加油~

会逐渐完善项目部分,rep代表repository(#^.^#)

项目地址

GitHub : https://github.com/al00000/rep

码云 : https://gitee.com/al00000/rep

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

保龄球

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

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

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

打赏作者

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

抵扣说明:

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

余额充值