大数据组件之HBase

前言

HBase作为一个列式存储的数据存储框架,其广泛应用于OLAP。前面介绍了大数据组件之HIve,其访问量很高,因此本文主要介绍HBase的shell命令和java操作,希望对于学习大数据或者从事大数据的你有些帮助。

一、HBase

HBase是一个分布式的面向列的开源数据库,该技术来源于 Fay Chang 所撰写的Google论文“Bigtable:一个结构化数据的分布式存储系统”。就像Bigtable利用了Google文件系统(File System)所提供的分布式数据存储一样,HBase在Hadoop之上提供了类似于Bigtable的能力。HBase是Apache的Hadoop项目的子项目。HBase不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库。另一个不同的是HBase基于列的而不是基于行的模式

HBase只有新增操作,通过操作类型和时间戳建立版本管理,按照时间的先后顺序0,1,2,…

可以通过切换版本追踪历史数据

可以设置最大的保留实例记录数

  • 基于列式存储模型,对于数据实现了高度压缩,节省存储成本
  • 采用 LSM 机制而不是B(+)树,这使得HBase非常适合海量数据实时写入的场景
  • 高可靠,一个数据会包含多个副本(默认是3副本),这得益于HDFS的复制能力,由RegionServer提供自动故障转移的功能
  • 高扩展,支持分片扩展能力(基于Region),可实现自动、数据均衡
  • 强一致性读写,数据的读写都针对主Region上进行,属于CP型的系统
  • 易操作,HBase提供了Java API、RestAPI/Thrift API等接口
  • 查询优化,采用Block Cache 和 布隆过滤器来支持海量数据的快速查找

1、Region

Region就是一段数据的集合。HBase中的表一般拥有一个到多个Region。Region具有以下特性:

1. Region不能跨服务器,一个RegionServer上有一个或者多个 Region。

2. 数据量小的时候,一个Region足以存储所有数据;但是,当数据 量大的时候,HBase会拆分Region。
3. 当HBase在进行负载均衡的时候,也有可能会从一台 RegionServer上把Region移动到另一台RegionServer上。
4. Region是基于HDFS的,它的所有数据存取操作都是调用了HDFS的 客户端接口来实现的。

2、RegionServer

RegionServer就是存放Region的容器,直观上说就是服务器上的一 个服务。当客户端从ZooKeeper获取RegionServer的地址后,它会直接从 RegionServer获取数据。

3、Master

Master只负责各种协调工作,比如建表、删表、 移动Region、合并等操作。

4、Zookeeper

Zookeeper 对于 HBase的作用是至关重要的。

  1. Zookeeper 提供了 HBase Master 的高可用实现,并保证同一时刻有且仅有一个主 Master 可用。
  2. Zookeeper 保存了 Region 和 Region Server 的关联信息(提供寻址入口),并保存了集群的元数据(Schema/Table)。
  3. Zookeeper 实时监控Region server的上线和下线信息,并实时通知Master。

二、HBases的Standalone安装

1、解压配置环境变量

1.下载

HBase的下载地址

#利用wget进行HBase安装包下载
wget https://hbase.apache.org/downloads.html

2.解压

#tar解压
tar -zxvf hbase-2.1.4-bin.tar.gz -C /opt/software

3.配置环境变量

#编辑环境变量
vim /etc/profile.d/my.sh
#-------------------------------------
export HBASE_HOME=/opt/software-2.1.4
export PATH=$HBASE_HOME/bin:$PATH
#-------------------------------------
# 生效环境变量
source /etc/profile

2、修改配置文件信息

1.hbase-env.sh

#打开文件
vim /opt/software-2.1.4/conf/hbase-env.sh
#-----------------------------------------
export JAVA_HOME=/opt/software/jdk1.8.0_201
#-----------------------------------------

2.hbase-site.xml

#打开编辑文件,新增以下内容
vim /opt/software-2.1.4/conf/hbase-site.xml

新增以下内容:

<configuration>
 <property>
    <name>hbase.rootdir</name>
    <value>file:///home/hbase/rootdir</value>
  </property>
  <property>
    <name>hbase.zookeeper.property.dataDir</name>
    <value>/home/zookeeper/dataDir</value>
  </property>
  <property>
    <name>hbase.unsafe.stream.capability.enforce</name>
    <value>false</value>
  </property>
</configuration>

配置信息介绍:

  1. hbase.rootdir: 配置 hbase 数据的存储路径;
  2. hbase.zookeeper.property.dataDir: 配置 zookeeper 数据的存储路径;
  3. hbase.unsafe.stream.capability.enforce: 使用本地文件系统存储,不使用 HDFS 的情况下需要禁用此配置,设置为 false。

3.启动HBase

由于已经将 HBase 的 bin 目录配置到环境变量,直接使用以下命令启动:

#启动HBase
start-hbase.sh

4.验证启动是否成功

验证方式一 :使用 jps 命令查看 HMaster 进程是否启动。

[root@hadoop001 hbase-2.1.4]# jps
16336 Jps
15500 HMaster

验证方式二 :访问 HBaseWeb UI 页面,默认端口为 16010

三、HBase完全分布式搭建

#默认无,必须手动创建
vim conf/backup-masters 
#---------
#backup状态
master02
#--------
start-hbase.sh HMaster

溢写64MB

#DDL
list
list_namespace
create_namespace 'kb16nb'
list_namespace_tables 'kb16nb'
#
create 'kb16nb:student','base','score'
describe 'kb16nb:student'
#----------------------------------------------------------------------------------------
Table kb16nb:student is ENABLED                                                         
kb16nb:student                                                                          
COLUMN FAMILIES DESCRIPTION                                                             
{NAME => 'base', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '1', KEEP_DELET
ED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'NONE', TTL => 'FOREV
ER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE 
=> '0'}                                                                                 

{NAME => 'score', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '1', KEEP_DELE
TED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'NONE', TTL => 'FORE
VER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE
 => '0'} 
#----------------------------------------------------------------------------------------
#DML
is_enabled 'kb16nb:student' #查看表是否可用
is_disabled 'kb16nb:student' #查看表示是否禁用
drop 'kb16nb:student' #删除表,删除前需要禁用
enable 'kb16nb:student' #启用表
disable 'kb16nb:student'  #禁用表
truncate 'kb16nb:student' #删除数据
delete 'kb16nb:student','rowkey','columnfamily:colname' #删除某行某列(最新的版本)
deleteall 'kb16nb:student','rowkey' #删除某行(删除所有版本)
put 'kb16nb:student','1','base:name','zhangsan' #插入数据
scan 'kb16nb:student' #查看全表
#----------------------------------------------------------------------------------------
hbase(main):014:0> scan 'kb16nb:student'
ROW                     COLUMN+CELL                                                     
 1                      column=base:name, timestamp=2022-01-28T10:23:06.333, value=zhang
                        san
#----------------------------------------------------------------------------------------
get 'kb16nb:student','2','base:name'
scan 'kb16nb:student',{COLUMN=>'base'} #查询列簇
scan 'kb16nb:student',{COLUMN=>'base:name'} #查询指定列
scan 'kb16nb:student',{COLUMN=>'base:name',LIMIT=>2}
scan 'kb16nb:student',{COLUMN=>'base:name',LIMIT=>2,STARTKEY=>'2'} #开始下标(包含)
scan 'kb16nb:student',{COLUMN=>'base:name',LIMIT=>2,STARTKEY=>'2',STARTROW=>'2'} #Row下标不包含
#BLOCKCACHE的页面置换算法是最近最少使用LRU
create 'kb16nb:'
#shell执行HBase命令
echo "shell指令"|hbase shell -n
echo "list_namespace_tables 'kb16nb'"|hbase shell -n
#布隆过滤器出现误码后怎么处理
#预分区
create_namespace 'kb16'
create 'kb16:pre_split_n1','cf',SPLITS=>['10','20','30']
hbase org.apache.hadoop.hbase.util.RegionSplitter kb16:pre_split_hsp1 HexStringSplit -c 3 -f base,scores
create 'kb16:pre_split_pdp','base',{NUMREGIONS=>4,SPLITALGO=>'DecimalStringSplit'}

四、HBase常见shell指令

HBase为什么这么快

1、基本指令

1.打开HBase shell

#进入HBase的命令
hbase shell

2. 获取帮助

# 获取帮助
help
# 获取命令的详细信息
help 'status'

3. 查看服务器状态

#查看服务器状态
status

4.查看版本信息

#查看HBase版本信息
version

5.创建命名空间

#创建命名空间
create_namespace '命名空间'
create_namespace 'ns1'

6.描述命名空间

#描述命名空间
describe_namespace '命名空间'
describe_namespace 'ns1'

7.删除namespace

#删除namespace
drop_namespace 'ns1'

2、表操作

1. 查看所有表

#查看HBase的所有表
list

2.创建表

命令格式: create ‘表名称’, ‘列族名称 1’,‘列族名称 2’,‘列名称 N’

# 创建一张名为Student的表,包含基本信息(baseInfo)、学校信息(schoolInfo)两个列族
create 'ns1:t1',{NAME => 'f1',VERSION => 5},{NAME => 'f2'}
create 'Student','baseInfo','schoolInfo'

3.查看表的基本信息

命令格式:desc ‘表名’

#查看表的基本信息
describe 'Student'

4.表的启用/禁用

enable 和 disable 可以启用/禁用这个表,is_enabled 和 is_disabled 来检查表是否被禁用

# 禁用表
disable 'Student'
# 检查表是否被禁用
is_disabled 'Student'
# 启用表
enable 'Student'
# 检查表是否被启用
is_enabled 'Student'

5.检查表是否存在

指令格式: exits ‘表名’

#检查表是否存在
exists 'Student'

6.删除表

删除表前需要先禁用表

# 删除表前需要先禁用表
disable 'Student'
# 删除表
drop 'Student'

3、增删改

1.添加列族

命令格式: alter ‘表名’, ‘列族名’

alter 'Student', 'teacherInfo'

2.删除列族

命令格式:alter ‘表名’, {NAME => ‘列族名’, METHOD => ‘delete’}

alter 'Student', {NAME => 'teacherInfo', METHOD => 'delete'}

3.更改列族存储版本的限制

默认情况下,列族只存储一个版本的数据,如果需要存储多个版本的数据,则需要修改列族的属性。修改后可通过 desc 命令查看。

alter 'Student',{NAME=>'baseInfo',VERSIONS=>3}

4.插入数据

命令格式:put ‘表名’, ‘行键’,‘列族:列’,‘值’

注意:如果新增数据的行键值、列族名、列名与原有数据完全相同,则相当于更新操作

put 'Student', 'rowkey1','baseInfo:name','tom'
put 'Student', 'rowkey1','baseInfo:birthday','1990-01-09'
put 'Student', 'rowkey1','baseInfo:age','29'
put 'Student', 'rowkey1','schoolInfo:name','Havard'
put 'Student', 'rowkey1','schoolInfo:localtion','Boston'

put 'Student', 'rowkey2','baseInfo:name','jack'
put 'Student', 'rowkey2','baseInfo:birthday','1998-08-22'
put 'Student', 'rowkey2','baseInfo:age','21'
put 'Student', 'rowkey2','schoolInfo:name','yale'
put 'Student', 'rowkey2','schoolInfo:localtion','New Haven'

put 'Student', 'rowkey3','baseInfo:name','maike'
put 'Student', 'rowkey3','baseInfo:birthday','1995-01-22'
put 'Student', 'rowkey3','baseInfo:age','24'
put 'Student', 'rowkey3','schoolInfo:name','yale'
put 'Student', 'rowkey3','schoolInfo:localtion','New Haven'

put 'Student', 'wrowkey4','baseInfo:name','maike-jack'

5.获取指定行、指定行中的列族、列的信息

# 获取指定行中所有列的数据信息
get 'Student','rowkey3'
# 获取指定行中指定列族下所有列的数据信息
get 'Student','rowkey3','baseInfo'
# 获取指定行中指定列的数据信息
get 'Student','rowkey3','baseInfo:name'

6.删除指定行、指定行中的列

# 删除指定行
delete 'Student','rowkey3'
# 删除指定行中指定列的数据
delete 'Student','rowkey3','baseInfo:name'

4、查询

hbase 中访问数据有两种基本的方式:

  • 按指定 rowkey 获取数据:get 方法;

  • 按指定条件获取数据:scan 方法。

scan 可以设置 begin 和 end 参数来访问一个范围内所有的数据。get 本质上就是 begin 和 end 相等的一种特殊的 scan。

1.Get查询

# 获取指定行中所有列的数据信息
get 'Student','rowkey3'
# 获取指定行中指定列族下所有列的数据信息
get 'Student','rowkey3','baseInfo'
# 获取指定行中指定列的数据信息
get 'Student','rowkey3','baseInfo:name'

2.查询整表数据

指令格式: scan ‘表名’

#查询整表数据
scan 'Student'

3.查询指定列簇的数据

#查看指定列簇数据
scan 'Student', {COLUMN=>'baseInfo'}

4.条件查询

# 查询指定列的数据
scan 'Student', {COLUMNS=> 'baseInfo:birthday'}

除了列 (COLUMNS) 修饰词外,HBase 还支持 Limit(限制查询结果行数),STARTROWROWKEY 起始行,会先根据这个 key 定位到 region,再向后扫描)、STOPROW(结束行)、TIMERANGE(限定时间戳范围)、VERSIONS(版本数)、和 FILTER(按条件过滤行)等。

如下代表从 rowkey2 这个 rowkey 开始,查找下两个行的最新 3 个版本的 name 列的数据:

scan 'Student', {COLUMNS=> 'baseInfo:name',STARTROW => 'rowkey2',STOPROW => 'wrowkey4',LIMIT=>2, VERSIONS=>3}

5. 条件过滤

Filter 可以设定一系列条件来进行过滤。如我们要查询值等于 24 的所有数据:

scan 'Student', FILTER=>"ValueFilter(=,'binary:24')"

值包含 yale 的所有数据:

scan 'Student', FILTER=>"ValueFilter(=,'substring:yale')"

列名中的前缀为 birth 的:

scan 'Student', FILTER=>"ColumnPrefixFilter('birth')"

FILTER 中支持多个过滤条件通过括号、AND 和 OR 进行组合:

# 列名中的前缀为birth且列值中包含1998的数据
scan 'Student', FILTER=>"ColumnPrefixFilter('birth') AND ValueFilter ValueFilter(=,'substring:1998')"

PrefixFilter 用于对 Rowkey 的前缀进行判断:

scan 'Student', FILTER=>"PrefixFilter('wr')"

五、Java API

1、pom.xml

<!--依赖导入-->
<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>2.0.0</version>
</dependency>

2、HBaseClient

import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HbaseClient {
    // config zookeeper
    static private org.apache.hadoop.conf.Configuration configuration = null;
    static private Connection connection = null;
    private static Logger logger = LoggerFactory.getLogger(HbaseClient.class);
    static private Lock lock = new ReentrantLock();

    static Connection getConnectionInstance() {
        if (null == connection) {
            lock.lock();
            try {
                if (null == connection) {
                    configuration = HBaseConfiguration.create();
                    configuration.set("hbase.zookeeper.quorum", "zk1:2182,zk2:2182,zk3:2182");
                    configuration.set("hbase.client.keyvalue.maxsize", "100000000");
                    connection = ConnectionFactory.createConnection(configuration);
                }
            } catch (IOException e) {
                logger.error("create hbase error ", e);
            } finally {
                lock.unlock();
            }
        }
        return connection;
    }
}

3、创建表

/**
* 创建表
*
* @param tableName
*/
public static void createTable(String tableStr, String[] familyNames) {
	System.out.println("start create table ......");
	try {
		Admin admin = connection.getAdmin();
		TableName tableName = TableName.valueOf(tableStr);
		if (admin.tableExists(tableName)) {// 如果存在要创建的表,那么先删除,再创建
		admin.disableTable(tableName);
		admin.deleteTable(tableName);
		System.out.println(tableName + " is exist,detele....");
		}
        HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);
        // 添加表列信息
        if (familyNames != null && familyNames.length > 0) {
        	for (String familyName : familyNames) {
            	tableDescriptor.addFamily(new HColumnDescriptor(familyName));
        	}
       	}
      	admin.createTable(tableDescriptor);
	} catch (MasterNotRunningException e) {
		e.printStackTrace();
	} catch (ZooKeeperConnectionException e) {
      	e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	System.out.println("end create table ......");
}

4、添加行列数据数据

/**
* 添加行列数据数据
*
* @param tableName
* @throws Exception
*/
public static void insertData(String tableName, String rowId, String familyName,String qualifier, String value) throws Exception {
	System.out.println("start insert data ......");
	Table table = connection.getTable(TableName.valueOf(tableName));
	Put put = new Put(rowId.getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值
	put.addColumn(familyName.getBytes(), qualifier.getBytes(), value.getBytes());// 本行数据的第一列
	try {
		table.put(put);
	} catch (IOException e) {
		e.printStackTrace();
	}
    System.out.println("end insert data ......");
}

5、添加行列数据数据

/**
* 添加行列数据数据
*
* @param tableName
* @throws Exception
*/
public static void batchInsertData(String tableName, String rowId, List<String> familyNames,String qualifier, List<String> values) throws Exception {
	if (null == qualifier) qualifier = "tmp";
	Table table = connection.getTable(TableName.valueOf(tableName));
	Put put = new Put(rowId.getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值
	for (int i = 0; i < familyNames.size(); ++i) {
		put.addColumn(familyNames.get(i).getBytes(),
			qualifier.getBytes(), values.get(i).getBytes());// 本行数据的第一列
	}
	try {
		table.put(put);
	} catch (IOException e) {
		e.printStackTrace();
	}
}

6、删除行

/**
* 删除行
*
* @param tablename
* @param rowkey
*/
public static void deleteRow(String tablename, String rowkey) {
	try {
		Table table = connection.getTable(TableName.valueOf(tablename));
		Delete d1 = new Delete(rowkey.getBytes());
		table.delete(d1);//d1.addColumn(family, qualifier);d1.addFamily(family);
		System.out.println("删除行成功!");
	} catch (IOException e) {
		e.printStackTrace();
	}
}

7、查询所有数据

/**
* 查询所有数据
*
* @param tableName
* @throws Exception
*/
public static void queryAll(String tableName) throws Exception {
	Table table = connection.getTable(TableName.valueOf(tableName));
	try {
		ResultScanner rs = table.getScanner(new Scan());
		for (Result r : rs) {
			System.out.println("获得到rowkey:" + new String(r.getRow()));
			for (Cell keyValue : r.rawCells()) {
			System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
		}
		}
		rs.close();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

8、根据rowId查询

/**
* 根据rowId查询
*
* @param tableName
* @throws Exception
*/
public static void queryByRowId(String tableName, String rowId) throws Exception {
	Table table = connection.getTable(TableName.valueOf(tableName));
	try {
		Get scan = new Get(rowId.getBytes());// 根据rowkey查询
		Result r = table.get(scan);
		System.out.println("获得到rowkey:" + new String(r.getRow()));
		for (Cell keyValue : r.rawCells()) {
		System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
}

9、根据列条件查询

/**
* 根据列条件查询
*
* @param tableName
*/
public static void queryByCondition(String tableName, String familyName,String qualifier,String value) {
	try {
		Table table = connection.getTable(TableName.valueOf(tableName));
		Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
		Bytes.toBytes(qualifier), CompareOp.EQUAL, Bytes.toBytes(value)); // 当列familyName的值为value时进行查询
		Scan s = new Scan();
		s.setFilter(filter);
		ResultScanner rs = table.getScanner(s);
		for (Result r : rs) {
			System.out.println("获得到rowkey:" + new String(r.getRow()));
			for (Cell keyValue : r.rawCells()) {
				System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                            new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
			}
		}
		rs.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
}

10、多条件查询

/**
* 多条件查询
*
* @param tableName
*/
public static void queryByConditions(String tableName, String[] familyNames, String[] qualifiers,String[] values) {
	try {
		Table table = connection.getTable(TableName.valueOf(tableName));
		List<Filter> filters = new ArrayList<Filter>();
		if (familyNames != null && familyNames.length > 0) {
			int i = 0;
			for (String familyName : familyNames) {
				Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
Bytes.toBytes(qualifiers[i]), CompareOp.EQUAL,Bytes.toBytes(values[i]));
				filters.add(filter);
				i++;
			}
		}
		FilterList filterList = new FilterList(filters);
        Scan scan = new Scan();
        scan.setFilter(filterList);
        ResultScanner rs = table.getScanner(scan);
        for (Result r : rs) {
			System.out.println("获得到rowkey:" + new String(r.getRow()));
			for (Cell keyValue : r.rawCells()) {
				System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":" +new String(CellUtil.cloneQualifier(keyValue)) +"====值:" + new String(CellUtil.cloneValue(keyValue)));
			}
		}
                rs.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
}

11、删除表

// 懒加载单例模式
static private Connection connection = HbaseClient.getConnectionInstance();
/**
* 删除表
*
* @param tableName
*/
public static void dropTable(String tableStr) {
	try {
		Admin admin = connection.getAdmin();
		TableName tableName = TableName.valueOf(tableStr);
		admin.disableTable(tableName);
		admin.deleteTable(tableName);
		admin.close();
	} catch (MasterNotRunningException e) {
		e.printStackTrace();
	} catch (ZooKeeperConnectionException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
}
  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

绝域时空

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

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

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

打赏作者

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

抵扣说明:

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

余额充值