大数据常用的面试问题笔记

Java
java三大特性
jvm调优
hashmap和hashtable的区别
arraylist和linedlist的区别
stringbuffer和stringbulider的区别
选择排序,冒泡排序,二分查找
Java线程生命周期
创建多线程的方式有几种?
进程与线程
抽象类
I/O流

Hadoop
Hdfs存储过程
Mapreducer的shuffle过程
Yarn的组件
Yarn的执行流程

Hbase
hbase简介
hbase架构
hbase读写过程
rowkey设计原则
热点问题出现的原因以及解决
Hbase region split过程
预分区

Hive
hive简介
hive架构
数据倾斜与解决
Hive优化
Row_number()
Hive存储比较

Spark
spark简介
spark架构
spark读取广播变量
spark与mapreducer 的区别
spark shuffle原理
spark优化
宽窄依赖
spark RDD 持久化级别
checkpoint的流程

kafka
kafka原理
kafka读写快原因
kafka数据丢失问题
kafka使用
kafka的节点挂了怎么办?

综合
hive与hbase的区别
hive与spark的区别
隐式转换
项目的每天实际数据大概多少
项目中遇到的问题
软件版本
问面试官的问题

机器学习

你了解哪些机器学习的算法?分别是做什么的?

线性回归:
逻辑回归:
朴素贝叶斯:文档分类

推荐算法

常见的推荐算法

协同过滤算法:
基于用户的协同过滤:
基于商品的协同过滤:

Java
java三大特性
jvm调优
hashmap和hashtable的区别
arraylist和linedlist的区别
stringbuffer和stringbulider的区别
选择排序,冒泡排序,二分查找
Java线程生命周期
创建多线程的方式有几种?
9. 进程与线程

1.java三大特性
封装、继承、多态一、 封装

  1. 封装就是将类的信息隐藏在类内部,不允许外部程序直接访问,而是通过该类的方法实现对隐藏信息的操作和访问。
  2. 封装是怎么实现的呢?
    a. 需要修改属性的访问控制符(修改为private);
    b. 创建getter/setter方法(用于属性的读写);
    c. 在getter/setter方法中加入属性控制语句(用于判断属性值的合法性)
    二、 继承
    继承是类与类的一种关系,比较像集合中的从属于关系。比如说,狗属于动物。就可以看成狗类继承了动物类,那么狗类就是动物类的子类(派生类),动物类就是狗类的父类(基类)。在Java中是单继承的,也就是说一个子类只有一个父类。
    三、 多态
    多态指的是对象的多种形态。多态有两种:引用多态和方法多态。继承是多态的实现基础。
    1.引用多态
    父类的引用可以指向本类的对象;父类的引用可以指向子类的对象。
  3. 方法多态
    创建父类对象时,调用的方法为父类方法;
    创建子类对象时,调用的方法是子类重写的方法或继承自父类的方法;
    注意:不允许通过父类的引用调用子类独有的方法。

2.jvm调优
Gc调优
参考链接:https://blog.csdn.net/zhoudaxia/article/details/26956831

JVM调优工具
  主要有Jconsole,jProfile,VisualVM。
  Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
  JProfiler:商业软件,需要付费。功能强大。
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。

如何调优
  观察内存释放情况、集合类检查、对象树
  上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能
(1)堆信息查看
可查看堆空间大小分配(年轻代、年老代、持久代分配)。
   提供即时的垃圾回收功能。
   垃圾监控(长时间监控回收情况)。
查看堆内类、对象信息查看:数量、类型等。
对象引用情况查看。
   有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
   – 年老代年轻代大小划分是否合理
    – 内存泄漏
   – 垃圾回收算法设置是否合理
(2)热点分析

3.hashmap和hashtable的区别
Hashmap Hashtable
父类 AbstractMap Dictionary
安全 不安全 安全
Null 允许为null 不允许
可以序列化
映射为无序
速度 快 慢

4.arraylist和linedlist的区别
1.数据结构:ArrayList 基于动态数组的,LinkedList基于链表
2.查询修改 arraylist快
3.增删:LinedList比较占优势 array要移动元素

Collection(单列集合)
List(有序,可重复)
ArrayList
底层数据结构是数组,查询快,增删慢
线程不安全,效率高
Vector
底层数据结构是数组,查询快,增删慢
线程安全,效率低
LinkedList
底层数据结构是链表,查询慢,增删快
线程不安全,效率高
Set(无序,唯一)
HashSet
底层数据结构是哈希表。
哈希表依赖两个方法:hashCode()和equals()
执行顺序:
首先判断hashCode()值是否相同
是:继续执行equals(),看其返回值
是true:说明元素重复,不添加
是false:就直接添加到集合
否:就直接添加到集合
最终:
自动生成hashCode()和equals()即可

			LinkedHashSet
				底层数据结构由链表和哈希表组成。
				由链表保证元素有序。
				由哈希表保证元素唯一。
		TreeSet
			底层数据结构是红黑树。(是一种自平衡的二叉树)
			如何保证元素唯一性呢?
				根据比较的返回值是否是0来决定
			如何保证元素的排序呢?
				两种方式
					自然排序(元素具备比较性)
						让元素所属的类实现Comparable接口
					比较器排序(集合具备比较性)
						让集合接收一个Comparator的实现类对象

Map(双列集合)
A:Map集合的数据结构仅仅针对键有效,与值无关。
B:存储的是键值对形式的元素,键唯一,值可重复。

	HashMap
		底层数据结构是哈希表。线程不安全,效率高
			哈希表依赖两个方法:hashCode()和equals()
			执行顺序:
				首先判断hashCode()值是否相同
					是:继续执行equals(),看其返回值
						是true:说明元素重复,不添加
						是false:就直接添加到集合
					否:就直接添加到集合
			最终:
				自动生成hashCode()和equals()即可
		LinkedHashMap
			底层数据结构由链表和哈希表组成。
				由链表保证元素有序。
				由哈希表保证元素唯一。
	Hashtable
		底层数据结构是哈希表。线程安全,效率低
			哈希表依赖两个方法:hashCode()和equals()
			执行顺序:
				首先判断hashCode()值是否相同
					是:继续执行equals(),看其返回值
						是true:说明元素重复,不添加
						是false:就直接添加到集合
					否:就直接添加到集合
			最终:自动生成hashCode()和equals()即可
	TreeMap
		底层数据结构是红黑树。(是一种自平衡的二叉树)
			如何保证元素唯一性呢?
				根据比较的返回值是否是0来决定
			如何保证元素的排序呢?
				两种方式
					自然排序(元素具备比较性)
						让元素所属的类实现Comparable接口
					比较器排序(集合具备比较性)
						让集合接收一个Comparator的实现类对象

5.stringbuffer和stringbulider的区别
1.运行速度快慢为:StringBuilder > StringBuffer > String
2.StringBuilder是线程不安全的,不同步,而StringBuffer是线程安全的,同步
3.String:适用于少量的字符串操作,实际是重新指向一个地方,长度不可变
 StringBuilder:适用于单线程下在字符缓冲区进行大量操作
 StringBuffer:适用多线程下在字符缓冲区进行大量操作

6.选择排序,冒泡排序,二分查找
A:冒泡排序
相邻元素两两比较,大的往后放,第一次完毕,最大值出现在了最大索引处。同理,其他的元素就可以排好。

		public static void bubbleSort(int[] arr) {
			for(int x=0; x<arr.length-1; x++) {
				for(int y=0; y<arr.length-1-x; y++) {
					if(arr[y] > arr[y+1]) {
						int temp = arr[y];
						arr[y] = arr[y+1];
						arr[y+1] = temp;
					}
				}
			}
		}
		
	B:选择排序
		把0索引的元素,和索引1以后的元素都进行比较,第一次完毕,最小值出现在了0索引。同理,其他的元素就可以排好。
		
		public static void selectSort(int[] arr) {
			for(int x=0; x<arr.length-1; x++) {
				for(int y=x+1; y<arr.length; y++) {
					if(arr[y] < arr[x]) {
						int temp = arr[x];
						arr[x] = arr[y];
						arr[y] = temp;
					}
				}
			}
		}

二分查找(折半查找) 假设元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
针对数组有序的情况(千万不要先排序,在查找)

		public static int binarySearch(int[] arr,int value) {
			int min = 0;
			int max = arr.length-1;
			int mid = (min+max)/2;
			
			while(arr[mid] != value) {
				if(arr[mid] > value) {
					max = mid - 1;
				}else if(arr[mid] < value) {
					min = mid + 1;
				}
				
				if(min > max) {
					return -1;
				}
				
				mid = (min+max)/2;
			}
			
			return mid;
		}

Arrays工具类
二分查找:binarySearch()
7.Java多线程生命周期
Java thread主要包含以下几种状态:
新建状态(New):新创建了一个线程对象;
就绪状态(Runnable):也叫可运行状态。线程创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变的可运行,等待获取CPU的使用权;
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码;
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况有三种:
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中;
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把线程设置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

8.创建多线程的方式有几种?
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。

三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
9. 进程与线程
进程是申请资源,线程为进程的执行单元

  1. 抽象类
    抽象类不能实例化,抽象类不一定有抽象方法
  2. I/O流
    字节流,字符流 节点流和处理流的区别

Hadoop
Hdfs存储过程
Mapreducer的shuffle过程
Yarn的组件
Yarn的执行流程

1.Hdfs存储过程
HDFS写流程

写详细步骤:
客户端向NameNode发出写文件请求。
检查是否已存在文件、检查权限。若通过检查,直接先将操作写入EditLog,并返回输出流对象。
(注:WAL,write ahead log,先写Log,再写内存,因为EditLog记录的是最新的HDFS客户端执行所有的写操作。如果后续真实写操作失败了,由于在真实写操作之前,操作就被写入EditLog中了,故EditLog中仍会有记录,我们不用担心后续client读不到相应的数据块,因为在第5步中DataNode收到块后会有一返回确认信息,若没写成功,发送端没收到确认信息,会一直重试,直到成功)
3.client端按128MB的块切分文件。
4.client将NameNode返回的分配的可写的DataNode列表和Data数据一同发送给最近的第一个DataNode节点,此后client端和NameNode分配的多个DataNode构成pipeline管道,client端向输出流对象中写数据。client每向第一个DataNode写入一个packet,这个packet便会直接在pipeline里传给第二个、第三个…DataNode。
(注:并不是写好一个块或一整个文件后才向后分发)
5.每个DataNode写完一个块后,会返回确认信息。
(注:并不是每写完一个packet后就返回确认信息,个人觉得因为packet中的每个chunk都携带校验信息,没必要每写一个就汇报一下,这样效率太慢。正确的做法是写完一个block块后,对校验信息进行汇总分析,就能得出是否有块写错的情况发生)
6.写完数据,关闭输输出流。
7.发送完成信号给NameNode。
(注:发送完成信号的时机取决于集群是强一致性还是最终一致性,强一致性则需要所有DataNode写完后才向NameNode汇报。最终一致性则其中任意一个DataNode写完后就能单独向NameNode汇报,HDFS一般情况下都是强调强一致性)

HDFS读流程

读相对于写,简单一些
读详细步骤:
1.client访问NameNode,查询元数据信息,获得这个文件的数据块位置列表,返回输入流对象。
2.就近挑选一台datanode服务器,请求建立输入流 。
DataNode向输入流中中写数据,以packet为单位来校验。
4.关闭输入流

2.Mapreducer的shuffle过程
hadoop:map端保存分片数据,通过网络收集到reduce端
Shuffle产生的意义是什么?
完整地从map task端拉取数据到reduce 端;在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗;减少磁盘IO对task执行的影响;
过程
1.每个map有一个环形内存缓冲区,用于存储map的输出。默认大小100MB(io.sort.mb属性),一旦达到阀值0.8(io.sort.spill.percent),一个后台线程把内容溢写到(spill)磁盘的指定目录(mapred.local.dir)下的一个新建文件中。
2.写磁盘前,要partition,sort。如果有combiner,combine排序后数据。
3.等最后记录写完,合并全部文件为一个分区且排序的文件。

1.Reducer通过Http方式得到输出文件的特定分区的数据。
2.排序阶段合并map输出。然后走Reduce阶段。
3.reduce执行完之后,写入到HDFS中。

3.Yarn的组件
1. ResourceManager主要功能是:
(1)接收用户请求(2)管理调度资源(3)启动管理am(4)管理所有nm,处理nm的状态汇报,向nm下达命令。
2.Container:yarn的应用都是运行在容器上的,容器包含cpu,内存等信息。
3.NodeManager:NM是每个节点上的资源和任务管理器,它会定时地向RM汇报本节点上的资源使用情况和各个容器的运行状态;同时负责对容器的启动
和停止。
4. ApplicationMaster:管理应用程序。向RM获取资源、为应用程序分配任务、 监控所有任务运行状态。

4.Yarn的执行流程
RM 通过请求调度器分配一个容器给applicationmaster AM 运行一些对象来监控作业的进度,为每个Inputsplit创建一个map任务,并创建相应的reduce任务。根据整个作业情况来索要更多资源。请求是通过心跳来传输的,尽量将任务分配给有存储数据的节点。

Hbase
hbase简介
hbase架构
hbase读写过程
rowkey设计原则
热点问题出现的原因以及解决
Hbase region split过程
预分区

hbase简介
a) HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩 实时读写的分布式数据库
b) 利用Hadoop HDFS作为其文件存储系统,利用Hadoop MapReduce来处理HBase中的海量数据,利用Zookeeper作为其分布式协同服务
c) 主要用来存储非结构化和半结构化的松散数据(列存 NoSQL 数据库)

HBase数据模型

  1. ROW KEY
    – 决定一行数据
    – 按照字典顺序排序的。
    – Row key只能存储64k的字节数据
  2. Column Family列族 & qualifier列
    – 列簇必须作为表模式(schema)定义的一部分预先给出。如 create ‘test’, ‘course’;
    – 列名以列族作为前缀,新的列族成员(列)可以随后按需、动态加入;
    – 权限控制、存储以及调优都是在列族层面进行的
    – HBase把同一列族里面的数据存储在同一目录下,由几个文件保存
    注:通常公司的列簇一般设置为1个足够用了!
    3)Timestamp时间戳
    4)Cell单元格
    5)HLog(WAL log)
  1. hbase架构
    a) Client
    i. 包含访问HBase的接口并维护cache来加快对HBase的访问
    b) Zookeeper
    i. 保证任何时候,集群中只有一个master
    ii. 存贮所有Region的寻址入口。
    iii. 实时监控Region server的上线和下线信息。并实时通知Master
    iv. 存储HBase的schema和table元数据
    c) Master
    i. 为Region server分配region
    ii. 负责Region server的负载均衡
    iii. 发现失效的Region server并重新分配其上的region
    iv. 管理用户对table的增删改操作

d) RegionServer
i. Region server维护region,处理对这些region的IO请求
ii. Region server负责切分在运行过程中变得过大的region

hbase读写过程
HBase读数据流程
1,Client先访问zookeeper,从meta表读取region的位置,然后读取meta表中的数据。meta中又存储了用户表的region信息。
2,根据namespace、表名和rowkey在meta表中找到对应的region信息
3,找到这个region对应的regionserver
4,查找对应的region
5,先从MemStore找数据,如果没有,再到StoreFile上读(为了读取的效率)。

HBase写数据流程
1,Client先访问zookeeper,从meta表获取相应region信息,然后找到meta表的数据
2,根据namespace、表名和rowkey根据meta表的数据找到写入数据对应的region信息
3,找到对应的regionserver
4,把数据分别写到HLog和MemStore上一份
4,MemStore达到一个阈值后则把数据刷成一个StoreFile文件。(若MemStore中的数据有丢失,则可以总HLog上恢复)
5,当多个StoreFile文件达到一定的大小后,会触发Compact合并操作,合并为一个StoreFile,(这里同时进行版本的合并和数据删除。)
6,当Storefile大小超过一定阈值后,会把当前的Region分割为两个(Split),并由Hmaster分配到相应的HRegionServer,实现负载均衡

补充:
HStore存储是HBase存储的核心,其中由两部分组成,一部分是MemStore,一部分是StoreFiles。
在分布式系统环境中,无法避免系统出错或者宕机,一旦HRegionServer以外退出,
MemStore中的内存数据就会丢失,引入HLog就是防止这种情况。
(WAL)工作机制:每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件,HLog文件定期会滚动出新,并删除旧的文件(已持久化到 StoreFile中的数据)。当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的 HLog文件,将不同region的log数据拆分,分别放到相应region目录下,然后再将失效的region(带有刚刚拆分的log)重新分配,领 取到这些region的 HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore中,然后flush到StoreFiles,完成数据恢复。

注意:
1,Client访问hbase上数据时并不需要Hmaster参与,数据的读写也只是访问RegioneServer,
HMaster仅仅维护这table和Region的元数据信息,负载很低。
2,HBase是通过DFS client把数据写到HDFS上的
3,每一个HRegionServer有多个HRegion,每一个HRegion有多个Store,每一个Store对应 一个列簇。
4,HFile是HBase中真正实际数据的存储格式,HFile是二进制格式文件,StoreFile就是对 HFile进行了封装(其实就是一个东西),
然后进行数据的存储。
5,HStore由MemStore(只有一个)和StoreFile(多个)组成。
6,HLog记录数据的变更信息,用来做数据恢复

rowkey设计原则
1)、RowKey长度原则:RowKey是一个二进制码流,可以是任意字符串,最大长度为64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长。建议是越短越好,不要超过16个字节。原因一是数据的持久化文件HFile中是按照KeyValue存储的,如果RowKey过长比如100字节,1000万列数据光RowKey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;原因二是memstore将缓存部分数据到内存,如果RowKey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此RowKey的字节长度越短越好原因三是目前操作系统大都是64位,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。
2)、RowKey散列原则:如果RowKey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将RowKey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer实现负载均衡的几率,如果没有散列字段,首字段直接是时间信息,将产生所有数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
3)、RowKey唯一原则:必须在设计上保证其唯一性。
RowKey是按照字典排序存储的,因此,设计RowKey时候,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。

热点问题出现的原因以及解决
HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。 热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。
下面是一些常见的避免热点的方法以及它们的优缺点:
加盐
这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。
哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据
反转
第三种防止热点的方法时反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
反转rowkey的例子以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题
时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到key的末尾,例如 [key][reverse_timestamp] , [key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。
比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计rowkey的时候,可以这样设计[userId反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候,直接指定反转后的userId,startRow是[userId反转][000000000000],stopRow是[userId反转][Long.Max_Value - timestamp]
如果需要查询某段时间的操作记录,startRow是[user反转][Long.Max_Value - 起始时间],stopRow是[userId反转][Long.Max_Value - 结束时间]

  1. Hbase region split过程

Hive

  1. hive简介

  2. hive架构

  3. 数据倾斜与解决

  4. Hive优化

  5. Row_number()

  6. Hive存储比较

  7. hive简介

  8. hive架构
    详见思维导图

  9. 数据倾斜与解决

  10. 数据倾斜的原因
    1.1 为什么发生
    1.| Join | 其中一个表较小,但是key集中 | 分发到某一个或几个Reduce上的数据远高平均值
    2.大表与大表,但是分桶的判断字段0值或空值过多,这些空值都由一个reduce处理,非常慢 |
    3.group by group by | 维度过小,某值的数量过多 | 处理某值的reduce非常耗时 |
    4.Count Distinct |某特殊值过多|处理此特殊值的reduce耗时|
    1.2 原因
    1)、key分布不均匀
    2)、业务数据本身的特性
    3)、建表时考虑不周
    4)、某些SQL语句本身就有数据倾斜
    1.3 表现
    任务进度长时间维持在99%(或100%),查看任务监控页面,发现只有少量(1个或几个)reduce子任务未完成。因为其处理的数据量和其他reduce差异过大。
    单一reduce的记录数与平均记录数差异过大,通常可能达到3倍甚至更多。 最长时长远大于平均时长。

2 数据倾斜的解决方案
2.1 参数调节
hive.map.aggr = true
Map 端部分聚合,相当于Combiner
hive.groupby.skewindata=true
数据倾斜的时候进行负载均衡,当项设定为 true,生成的查询计划会有两个 MR Job。第一个 MR Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个 Reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的 Group By Key 有可能被分发到不同的 Reduce 中,从而达到负载均衡的目的;第二个 MR Job 再根据预处理的数据结果按照 Group By Key 分布到 Reduce 中(这个过程可以保证相同的 Group By Key 被分布到同一个 Reduce 中),最后完成最终的聚合操作。
2.2 SQL语句调节
如何Join:
关于驱动表的选取,用join key分布最均匀的表作为驱动表
做好列裁剪和filter操作,以达到两表做join的时候,数据量相对变小的效果。
大小表Join:(mapjoin)
使用map join让小的维度表(1000条以下的记录条数) 先进内存。在map端完成reduce.
大表Join大表,加随机数:
把空值的key变成一个字符串加上随机数,把倾斜的数据分到不同的reduce上,由于null值关联不上,处理后并不影响最终结果。
count distinct大量相同特殊值
count distinct时,将值为空的情况单独处理,如果是计算count distinct,可以不用处理,直接过滤,在最后结果中加1。如果还有其他计算,需要进行group by,可以先将值为空的记录单独处理,再和其他计算结果进行union。
group by维度过小:
采用sum() group by的方式来替换count(distinct)完成计算。
特殊情况特殊处理:
在业务逻辑优化效果的不大情况下,一些时候是可以将倾斜的数据单独拿出来处理。最后union回去。
2.3 空值产生的数据倾斜
场景:如日志中,常会信息丢失的问题,比如日志中的 user_id,如果取其中的 user_id 和用户表中的user_id 关联,会碰到数据倾斜的问题。
解决方法1: user_id为空的不参与关联(红色字体为修改后)
select * from log a join users b on a.user_id is not null and a.user_id = b.user_idunion allselect * from log a where a.user_id is null;
解决方法2 :赋与空值分新的key值
select * from log a left outer join users b on case when a.user_id is null then concat(‘hive’,rand() ) else a.user_id end = b.user_id;
结论:方法2比方法1效率更好,不但io少了,而且作业数也少了。解决方法1中 log读取两次,jobs是2。解决方法2 job数是1 。这个优化适合无效 id (比如 -99 , ’’, null 等) 产生的倾斜问题。把空值的 key 变成一个字符串加上随机数,就能把倾斜的数据分到不同的reduce上 ,解决数据倾斜问题。
3 不同数据类型关联产生数据倾斜
场景:用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。当按照user_id进行两个表的Join操作时,默认的Hash操作会按int型的id来进行分配,这样会导致所有string类型id的记录都分配到一个Reducer中。
解决方法:把数字类型转换成字符串类型
select * from users a left outer join logs b on a.usr_id = cast(b.user_id as string)
3.1 小表不小不大,怎么用 map join 解决倾斜问题
使用 map join 解决小表(记录数少)关联大表的数据倾斜问题,这个方法使用的频率非常高,但如果小表很大,大到map join会出现bug或异常,这时就需要特别的处理。 以下例子:
select * from log a left outer join users b on a.user_id = b.user_id;
users 表有 600w+ 的记录,把 users 分发到所有的 map 上也是个不小的开销,而且 map join 不支持这么大的小表。如果用普通的 join,又会碰到数据倾斜的问题。
解决方法:
select /+mapjoin(x)/* from log a left outer join ( select /+mapjoin©/d.* from ( select distinct user_id from log ) c join users d on c.user_id = d.user_id ) x on a.user_id = b.user_id;
假如,log里user_id有上百万个,这就又回到原来map join问题。所幸,每日的会员uv不会太多,有交易的会员不会太多,有点击的会员不会太多,有佣金的会员不会太多等等。所以这个方法能解决很多场景下的数据倾斜问题。
4 总结
使map的输出数据更均匀的分布到reduce中去,是我们的最终目标。由于Hash算法的局限性,按key Hash会或多或少的造成数据倾斜。大量经验表明数据倾斜的原因是人为的建表疏忽或业务逻辑可以规避的。在此给出较为通用的步骤:
4.1、采样log表,哪些user_id比较倾斜,得到一个结果表tmp1。由于对计算框架来说,所有的数据过来,他都是不知道数据分布情况的,所以采样是并不可少的。
4.2、数据的分布符合社会学统计规则,贫富不均。倾斜的key不会太多,就像一个社会的富人不多,奇特的人不多一样。所以tmp1记录数会很少。把tmp1和users做map join生成tmp2,把tmp2读到distribute file cache。这是一个map过程。
4.3、map读入users和log,假如记录来自log,则检查user_id是否在tmp2里,如果是,输出到本地文件a,否则生成的key,value对,假如记录来自member,生成的key,value对,进入reduce阶段。
4.4、最终把a文件,把Stage3 reduce阶段输出的文件合并起写到hdfs。
5.如果确认业务需要这样倾斜的逻辑,考虑以下的优化方案:
5.1、对于join,在判断小表不大于1G的情况下,使用map join
5.2、对于group by或distinct,设定 hive.groupby.skewindata=true
尽量使用上述的SQL语句调节进行优化

4.Hive优化
*建表层面优化
(1)分区,分桶
一般是按照业务日期进行分区
每天的数据放在一个分区里
(2)一般使用外部表,避免数据误删
1.外部表的路径可以自定义,内部表的路径需要在 hive/warehouse/目录下
2.删除表后,普通表数据文件和表信息都删除。外部表仅删除表信息

(3)选择适当的文件压缩格式
(4)命名要规范
(5)数据分层,表分离,但是也不要分的太散
SQL层面优化
分区裁剪
where过滤
分区分桶,合并小文件
适当的子查询
mapjoin
select /
+mapjoin(b)*/ a.xx,b.xxx from a left outer join b on a.id=b.id
左连的时候,大表在左边,小表在右边。

order by 语句: 是全局排序
sort by 语句: 是单reduce排序
distribute by语句: 是分区字段排序;
cluster by语句:
可以确保类似的数据的分发到同一个reduce task中,并且保证数据有序防止所有的数据分发到同一个reduce上,导致整体的job时间延长

cluster by语句的等价语句:
distribute by Word sort by Word ASC
*数据层面优化
*集群层面优化

  1. Row_number()

Hive存储比较
占比 加载速度 查询速度
Textfile 比较大 快 最低
SequenceFile 大 一般,需要转换 高
Rcfile 最小 最慢,需要转换 最高

Spark
spark简介
spark架构
spark读取广播变量
spark与mapreducer 的区别
spark shuffle原理
spark优化
spark RDD 持久化级别
checkpoint的流程
9. Spark_streaming消费数据注意

spark简介
spark是开源的计算系统,目的为了解决快速的处理和运算数据。
系统:
Mesos(相当于Hadoop生态系统中的YARN),资源管理、任务调度;
Tachyon:分布式内存文件系统,缓存数据并进行快速的读写
Spark-core:核心计算引擎,能够将任务并行化,在集群中进行大规模并行运算;
Spark-streaming: 流式计算引擎
Sprak SQL:SQL on Hadoop,能够提供交互式查询和报表查询,通过JDBC等接口调用;
GraphX:图计算引擎;
Spark-mllib: 机器学习库。
Spark运行速度快的原因:
1.基于内存
2.DAG有向无环图
3.粗粒度模式
Spark-core
RDD 弹性分布式数据集
五大特性:
1.由partition构成
2.方法实际运行在partition上
3.Rdd之间存在联系
4.如果rdd为key类型数据,就可以传入自定义的partitioner
5.最优计算路径,数据本地化

Spark依赖关系
根据shuffle去划分
宽依赖:本质就是shuffle父RDD和子RDD的partition之间的对应关系是一对多
窄依赖:父RDD和子RDD的partition之间的对应关系是一对一的,或多对一

广播变量(broadcast)
作用:为了避免每一个task中都有一个变量副本,占用executor资源,可能导致oom
累加器(accumulator)
类似于全局变量,在Driver中定义,读取,在Executor操作数据

Sparksql
传入一条交互式sql,在海量的数据库中运行(查询)
操作的对象为DataFrame 底层为RDD
Row可以通过下标获得数据,也可以通过字段名
弹性分布式数据集
Spark-streming 实时流式计算引擎
与Strom的区别
1.SparkingStreaming是微批处理,Strom是实时处理
2.Strom对事物支持比SparkStreaming好
3.SparkStraming支持动态修改资源
4.SparkStreaming吞吐量更高
操作的对象为DStream,基于RDD的封装
函数:
foreachRDD 内部为RDD操作,无返回值
运行在Driver端,而foreach和foreachPartion运行在Worker节点。
Transform 内部为RDD操作,返回值为DStream
RDD的算子内,都在exectuor执行,算子外,代码是在Driver端执行的,每个batchInterval执行一次,可以做到动态改变广播变量

算子:有状态算子,基于之前的运行结果运行
updateStateByKey
(相当于对不同批次的累加和更新)
启动以来一直维护这个累加状态(从程序开始到结束的全部累加)
要开启checkpoint机制和功能。
reduceBykeyAndWindow (实现一阶段内的累加 ,而不是程序启动时)
简单版本 重复计算问题
优化版本 必须设置checkpoint路径
(优化:避免重复数据,执行顺序,先加上新值,再减去旧值)
注:窗口长度和滑动间隔必须是batchInterval的整数倍。如果不是整数倍会检测报错。

Spark-on-kafka
接收模式:
Receiver方式(createStream)被动接收
每隔200ms就把数据封装为一个block
需要一个单独的线程去拉取数据,一直在接收数据,5s到了可以直接处理数据
接受到的数据会保存到excutors中,然后由spark Streaming 来启动Job进行处理这些数据。
注:
在默认的配置下,更新完消费offset之后Driver挂了,当前这个batch的数据就丢失了,如果要保证零数据丢失,需要启用WAL(Write Ahead Logs)。 但会影响速度
它同步将接受到数据保存到分布式文件系统上比如HDFS。 所以数据在出错的情况下可以恢复出来。

直接操作(createDirectStream) 主动拉取数据
定期地从kafka的topic+partition中查询最新的偏移量,再根据偏移量范围在每个batch里面处理数据
每隔一个bath去拉取偏移量,不是一直在接收数据,但需要时间拉取
rrd的partition对应kafka的partition数
偏移量由自己保存

spark架构
Client:客户端进程,负责提交作业到Master。
Master:Standalone模式中主控节点,负责接收Client提交的作业,管理Worker,并命令Worker启动Driver和Executor。
Worker:Standalone模式中slave节点上的守护进程,负责管理本节点的资源,定期向Master汇报心跳,接收Master的命令,启动Driver和Executor。
Driver: 一个Spark作业运行时包括一个Driver进程,也是作业的主进程,负责作业的解析、生成Stage并调度Task到Executor上。包括DAGScheduler,TaskScheduler。
Executor:即真正执行作业的地方,一个集群一般包含多个Executor,每个Executor接收Driver的命令Launch Task,一个Executor可以执行一到多个Task。

DAGScheduler: 实现将Spark作业分解成一到多个Stage,每个Stage根据RDD的Partition个数决定Task的个数,然后生成相应的Task set放到TaskScheduler中。
Stage:一个Spark作业一般包含一到多个Stage。
Task:一个Stage包含一到多个Task,通过多个Task实现并行运行的功能。
TaskScheduler:实现Task分配到Executor上执行。

	Excutor里由blockmanager,BlockManager有几个关键组件

DiskStore负责对磁盘上的数据进行读写
MemoryStore负责对内存中的数据进行读写
ConnectionManager负责建立BlockManager到远程其他节点的BlockManager的网络连接
BlockTransferService负责对远程其他节点的BlockManager的数据读写。

任务调度
1.创建SparkContext对象,启动DAGScheduler和TaskScheduler
2.启用线程吧,ApplicationMaster注册给Master
含资源信息(内存,core,executor数量)
3.Master选择一批worker,启动Executor
4.Worker上Exectuor启动完成,汇报给Master
5.Executor反向注册给TaskScheduler
6.代码中的action算子触发一个job
7.构建有向无环图
8.DAG根据宽窄依赖关系,切分Stage,并按照taskSet的形式发送给TaskScheduler
stage会重试4次
9.TaskScheduler接收taskset,分解出task,根据最最优算法(数据本地化)发送到对应的Executor上执行
task可能会出现执行失败,此时会重试3次,每次间隔5s,当重试完不成功或者报Shuffle file not found,taskScheduler不重试,交由DAGScheduler重试Stage
10.Exectuor执行完task,汇报结果给TaskScheduler
推测执行(默认关闭)
可能存在某个task执行过慢,此时会再启用一个task去执行该任务,最先完成的task,作为结果,并停止掉同任务的task
11.一个job的全部task执行完成
12.全部的job执行完毕,applition执行完成,释放资源

spark读取广播变量

  1. spark与mapreducer 的区别
    a。MapReduce:基于磁盘的大数据批量处理系统
    细粒度资源调度,会消耗较多的启动时间,不适合运行低延迟类型的作业,但资源可以得到充分的利用
    b。Spark:基于RDD(弹性分布式数据集)数据处理、显示的将RDD数据存储到磁盘和内存中
    粗粒度资源调度,启动速度快,会出现严重的资源争用
    DAG引擎,减少多次计算之间中间结果写到HDFS的开销
    易用
      1)提供了丰富的API,支持Java,Scala,Python和R四种语言
      2)代码量比MapReduce少2~5倍

  2. 与Hadoop集成 读写HDFS/Hbase 与YARN集成

  3. spark shuffle原理

6.Spark优化
具体见文件
1.尽可能复用同一个RDD,减少产生RDD的个数
2.对多次使用的RDD进行持久化
3.高性能算子
reduceByKey/aggregateByKey 替代 groupByKey(reduceByKey进行shuffle之前会先做合并)
mapPartitions 替代普通map Transformation算子 (是对rdd中的每个分区的迭代器进行操作)
foreachPartitions 替代 foreach Action算子
rdd.partitionBy() //其实自定义一个分区器
repartition coalesce(numPartitions,true) 增多分区使用这个
calesce(numPartitions,false) 减少分区 没有shuffle只是合并partition
4.优化数据结构 parquet 数据格式。字符串代替对象,int 替代 String
5.spark.default.parallelism
  参数说明:该参数用于设置每个stage的默认task数量
6.并行度 new SparkConf().set(“spark.defalut.parallelism”,”“500)
reduceByKey(num)指定分区数, coalesce() 降低分区
shuffle 之前不受影响 默认一个block快就是一个task。 shuffle后才会起作用
一般 task数是 CPU core 的2-3倍小文件 。 saveastextfile 前 减少分区。
task没有设置,或者设置的很少,比如就设置了,100个task 。 50个executor ,每个executor 有4个core
Application 任何一个stage运行的时候,都有总数200个cpu core ,可以并行运行。但是,你现在只有100个task.

数据倾斜
hive预处理过滤无用的倾斜数据
countByKey 可以直接找到倾斜严重的key
提高shuffle操作的并行度 reduceByKey(100 代表100并行度)
两阶段聚合(局部聚合+全局聚合)
拆分join
将reduce join转为map join (适用 在对RDD使用join类操作)

7.ark RDD 持久化级别
用于RDD重用和节省重新计算,方便构建迭代算法,缓存粒度为整个RDD
StorageLevel 说明
MEMORY_ONLY 使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化,默认的持久化策略
MEMORY_AND_DISK 使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中。不会立刻输出到磁盘
MEMORY_ONLY_SER RDD的每个partition会被序列化成一个字节数组,节省空间,读取时间更占CPU
MEMORY_AND_DISK_SER 序列化存储,超出部分写入磁盘文件中
DISK_ONLY 使用未序列化的Java对象格式,将数据全部写入磁盘文件中
MEMORY_ONLY_2 对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上
如果默认能满足使用默认的
如果不能于MEMORY_ONLY很好的契合,建议使用MEMORY_ONLY_SER
尽可能不要存储数据到磁盘上,除非数据集函数计算量特别大,或者它过滤了大量数据,否则从新计算一个分区的速度和从磁盘中读取差不多

8.Checkpoint的流程

kafka

  1. kafka原理

  2. kafka读写快原因

  3. kafka数据丢失问题

  4. kafka使用

  5. kafka的节点挂了怎么办?

  6. kafka的原理
    高吞吐量的、持久性的、分布式,没有中心的发布-订阅消息系统(消息队列)
    三大特性:
    (1)高吞吐量:可以满足每秒百万级别消息的生产和消费——生产消费。QPS
    (2)持久性:有一套完善的消息存储机制,确保数据的高效安全的持久化——中间存储。
    (3)分布式:基于分布式的扩展和容错机制;Kafka的数据都会复制到几台服务器上。当某一台故障失效时,生产者和消费者转而使用其它的机器——整体健壮性。
    (4)容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败);
    (5)高并发:支持数千个客户端同时读写;
    (6)支持实时在线处理和离线处理:可以使用Storm这种实时流处理系统对消息进行实时进行处理,同时还可以使用Hadoop这种批处理系统进行离线处理;

    架构:
    Topic:主题,Kafka处理的消息的不同分类。
    Broker:消息代理,Kafka集群中的一个kafka服务节点称为一个broker,主要存储消息数据。存在硬盘中。每个topic都是有分区的。
    Partition:Topic物理上的分组,一个topic在broker中被分为1个或者多个partition,分区在创建topic的时候指定。
    Message:消息,是通信的基本单位,每个消息都属于一个partition
    Producer:消息和数据的生产者,向Kafka的一个topic发布消息。
    Consumer:消息和数据的消费者,定于topic并处理其发布的消息。
    Zookeeper:协调kafka的正常运行。
    保存元信息
    建立起生产者和消费者的订阅关系,并实现生产者与消费者的负载均衡

    一个Topic里有多个partition,Partirion内部有序,消息都有偏移量(offset),通过偏移量来判断数据读取到那一步。
    每个partition都有对应的leader来进行管理,每个partition都有副本,当对应的leader宕机时,会重新选举出一个新的leader来管理,一般之前那的leader重新上线,会使用原先的leader,降低新leader的压力,实现负载均衡。需要手动开启auto.leader.rebalance.enable=true
    注:
    kafka删除消息是按照时间删除的,一般为7天消费了会被打上标记(过期),不会直接删除
    producer自己决定往哪个partition写消息,可以是轮询的负载均衡,加权随机也可以,或者是基于hash的partition策略
    消息不经过内存缓冲,直接写入文件

拓展
注意一:
向kafka中写数据的时候,必须指定kafka的所有brokers节点
读取Kafka中的数据的时候是需要指定zk的节点,也为全部
注意二:
kafka中存储的是键值对,即使我们没有明确些出来key,获取的时候也是需要利用tuple的方式获取值的;而对于放到一个kafka中的数据,这个数据到底存放到那个partition中呢?这个就需要使用hashPartition方式或者普通的轮询方式存放;对于没有明确指定key的发往kafka的数据,使用的就是轮询方式;

  1. kafka读写快原因
    • 先进先出(FIFO)顺序保证
    Zero-copy的复制模式
    计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式

  2. kafka数据丢失问题
    kafka不存在数据丢失的问题,即使丢失数据也是前面的flume等采集数据的时候丢失了数据,kafka具有容错性,他会把读取的数据复制到其他的broker上。

  3. kafka使用
    生产者=>broker=>消费者
    flume=>kafka(消息缓冲)=>消费数据

  4. kafka的节点挂了怎么办?
    Partition在其他的节点上存在备份,只需要重新启动spark streming应用即可

综合

  1. hive与hbase的区别

  2. hive与spark的区别

  3. 隐式转换

  4. 项目的每天实际数据大概多少

  5. 项目中遇到的问题

  6. 软件版本

  7. 问面试官的问题

  8. flume是什么

  9. http有那2种方式

  10. 资源调度工具
    11.Spark和storm的区别

  11. cluster资源调度

  12. hive与hbase的区别
    二者都基于hdfs存储数据
    Hive是输入一条交互式sql在海量的数据中查找,从严格意义上来说,是一个ETL工具,表实际就是在hdfs的/hive/wavehouse的目录下。Hive本身不存储和计算数据,它完全依赖于HDFS和MapReduce,Hive中的表纯逻辑。hive需要用到hdfs存储文件,需要用到MapReduce计算框架。
    不严格的来说,hive可以认为是map-reduce的一个包装。hive的意义就是把好写的hive的sql转换为复杂难写的map-reduce程序
    hbase是高性能,高可靠,面向列,实时的分布式数据库,不支持mp计算,主要用于实时查询,是nosql型数据库,读写快,适合大规模大写,HBase是为了支持弥补Hadoop对实时操作的缺陷的项目 。
    不严格的来说,hbase可以认为是hdfs的一个包装。他的本质是数据存储,是个NoSql数据库;hbase部署于hdfs之上,并且克服了hdfs在随机读写方面的缺点。

  13. hive与spark的区别
    hive是一个基于hdfs的数据仓库,spark是一个计算引擎,从一定角度上来说他们无法比较。
    唯一可以比较的就只有2点。
    Hive只能做离线处理,并且使用的hadoop的mapreduce
    Spark可以做离线处理,也能实时处理,相对于hive来说不太稳定,成本较高(基于内存)

3.隐式转换
动态给类增加方法,toDF

4.项目的每天实际数据大概多少
根据车流量估算,大概50-100g 一秒钟大概几十m

5.项目中遇到的问题
数据倾斜,kafka拉取过来的数据来不及处理

6.软件版本
Jdk 1.8 1.7 hbase0.98.12.1
Cdh 5.4 zookeeper3.4.6
Hadoop 2.6 hive1.2.1
Flume 1.6 (Flume-ng) spark1.6
Azk 2.5 Redis 3.0 kafka2.1-0.8.2.2

7.问面试官的问题
公司一般做什么项目啊,我进来具体是做什么啊,集群有多大啊,一天处理的数据量是多少?
我面试的怎么样啊,这个问题应该怎么解决啊,能不能给我说一下

  1. flume是什么
    一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统
    Agent主要由:source,channel,sink三个组件组成.
    Source:从数据发生器接收数据,并将接收的数据以Flume的event格式传递给一个或者多个通道channel,Flume提供多种数据接收的方式,比如Avro,Thrift,twitter1%等
    channel是一种短暂的存储容器,它将从source处接收到的event格式的数据缓存起来,直到它们被sinks消费掉,它在source和sink间起着一共桥梁的作用,channal是一个完整的事务,这一点保证了数据在收发的时候的一致性. 并且它可以和任意数量的source和sink链接. 支持的类型有: JDBC channel , File System channel , Memort channel等.
    sink将数据存储到集中存储器比如Hbase和HDFS,它从channals消费数据(events)并将其传递给目标地. 目标地可能是另一个sink,也可能HDFS,HBase.

http有那2种方式
host:传参在body
git:?后面添加信息,不安全

资源调度工具
Azkaban:轻量级 开源 可以二次开发
Ozzie 重量级
11.Spark和storm的区别
Spark:微批处理
Storm:事务性较强,可以动态修改资源并行度
12. cluster资源调度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值