大数据笔记--HBase(第一篇)

目录

一、简介

1、概述

2、基本概念

①、RowKey:行键

②、Column Family:列族/列簇

③、namespace:名称空间

④、Colum:列

⑤、Timestamp:时间戳

⑥、Cell:单元格

二、基本命令

三、Hive和Hbase的比较

四、HBase的安装

 五、HBase的API

1、HBase空间API

2、Hbase表API


一、简介

1、概述

①、HBase原本是由Yahoo!公司开发后贡献给了Apache的一套开源的、基于Hadoop的、分布式的、可扩展的、非关系型数据库

②、如果需要对大量数据进行随机且实时读写,那么可以考虑用HBase

③、HBase能够管理非常大的表:billions of rows * millions of columns数十亿行 x 数百万列

④、HBase是仿照Google的Big Table来进行实现的,因此,Hbase和BigTable的原理几乎一致,只有实现语言不同。HBase是使用Java语言实现的,BigTable使用的是C语言实现的。HBase最终将数据落地到HDFS上

⑤、Hbase提供了2个大版本,并且2个版本都在同时更新。其中,Hadoop3.1.3版本支持的是HBase2.2.X及以上的版本

⑥、HBase作为非关系型数据库,不支持标准的SQL语法,提供了一套全新的命令

⑦、HBase能够存储稀疏类型的数据,也因此HBase能够存储结构化(数据本身有结构,经过解析之后,能够用传统数据库中的一个或几个表来存储)和半结构化数据(数据本身有结构,但是解析之后无法用传统数据库中的表来存储)

⑧、HBase本身作为数据库,提供了完整的增删改查的功能。HBase基于HDFS来进行存储,HDFS的特点是允许一次写入多次读取,不允许修改而允许追加写入,但HBase提供了”改“功能,HBase如何实现”改“功能的?-HBase实际上并没有去修改写入的数据,而是在文件末尾去追加数据,HBase会对写入的每条数据自动添加一个时间戳,当用户获取数据的时候,HBase自动返回最新的数据,那么从用户角度来看,就是发生了数据的修改。

⑨、在HBase中,数据的每一个时间戳称之为一个版本

⑩、如果要锁定唯一的一条数据,那么需要通过行键+列族+列+时间戳这四个维度来锁定,这种结构称为一个cell(单元格)

⑪、HBase中的表在创建的时候,如果不指定,那么只对外提供一个版本的数据

⑫、如果建好表之后再修改可以获取的版本,那么已经添加的数据不起作用

⑬、即使表允许对外获取多个版本的数据,在获取的时候如果不指定,依然只获取一个版本的数据

2、基本概念

①、RowKey:行键

Ⅰ、在Hbase中没有主键的概念,取而代之的是行键

Ⅱ、不同于传统的关系型数据库,在HBase中,定义表的时候不需要指定行键列,而是在添加数据的时候来手动添加行键

Ⅲ、HBase默认会对行键进行排序,按照字典序排序

②、Column Family:列族/列簇

Ⅰ、在HBase中,没有表关联的概念,取而代之的是用列族来进行统计

Ⅱ、在HBase中,一个表中至少包含1个列族,可以包含多个列族,理论上不限制列族的数量

Ⅲ、在HBase中,强调列族,但是不强调列-在定义表的时候必须定义列族,但是列可以动态增删,一个列族可以包含0到多个列

③、namespace:名称空间

Ⅰ、在HBase中没有database的概念,取而代之的是namespace

Ⅱ、在HBase启动的时候,自带了两个空间:default和hbase。hbase空间下方的是HBase的基本信息;在建表的时候如果不指定,则表默认放在default空间下

④、Colum:列

HBase表的列是由列族名、限定符以及列名组成的,其中“:”为限定符。创建HBase表不需要指定列,因为列是可变的,非常灵活。

⑤、Timestamp:时间戳

表示时间戳,记录每次操作数据的时间,通常记作数据的版本号。

⑥、Cell:单元格

根据行键、列族和列可以映射到一个对应的单元格,单元格是HBase存储数据的具体地址。

二、基本命令

命令解释
processlist查看当前HBase在执行的任务
status查看HBase的运行状态
version查看HBase的版本
whoami查看HBase的当前用户

create 'person', {NAME => 'basic'},{NAME => 'info'},{NAME => 'other'}

或者

create 'person', 'basic', 'info', 'other'

建立一个person表,包含3个列族:basic,info,other
append 'person', 'p1', 'basic:name', 'Bob'

在person表中添加一个行键为p1的数据,向basic列族的name列中添加数据

get 'person', 'p1'获取指定行键的数据

get 'person' , 'p1', {COLUMN => 'basic' }

或者

get 'person', 'p1', 'basic'

获取指定行键指定列族的数据

get 'person', 'p1', {COLUMN => ['basic', 'info']}

或者

get 'person', 'p1', 'basic', 'info'

获取指定行键多列族的数据

get 'person', 'p1', {COLUMN => 'basic:name'}

或者

get 'person', 'p1', 'basic:name'

获取指定行键指定列的数据

scan 'person'

扫描整表

scan 'person', {COLUMNS => 'basic'}

获取指定列族的数据

scan 'person', {COLUMNS => ['basic', 'info']}

获取多列族的数据
scan 'person', {COLUMNS => ['basic:name', 'other:address']}

获取多个列的数据

put 'person', 'p1', 'basic:age', 20

修改数据,还可以增加数据

delete 'person', 'p1', 'other:adderss'

或者

deleteall 'person', 'pb', 'basic:name'

删除指定行键指定列族的指定列

deleteall 'person', 'p1'

删除指定行键的所有数据

create 'students', {NAME => 'basic', VERSIONS => 3}, {NAME => 'info', VERSIONS => 4}

指定每一个列族允许对外获取的版本数量

desc 'students'

或者

describe 'students'

描述表

get 'students', 's1', {COLUMN => 'basic:age', VERSIONS => 3}

获取指定行键指定列的指定数量版本的数据

scan 'students', {COLUMNS => 'basic:age', VERSIONS => 3}

获取指定列的指定数量版本的数据

count 'person'

统计person表中行键的个数

get_splits 'person'

获取person表对应的HRegion的个数

truncate 'person'

摧毁重建person表

list_namespace

查看所有的空间

create_namespace 'demo'

创建demo空间

create 'demo:users', 'basic'

在demo空间下创建users表

list_namespace_tables 'demo'

获取demo空间下的所有表

describe_namespace 'demo'

描述demo空间

drop_namespace 'demo'

删除demo空间,要求这个空间为空

disable 'demo:users'

禁用表

drop 'demo:users'

删除表

enable 'person'

启用表

exists 'users'

判断表是否存在

is_disabled 'person'

判断person表是否被禁用

is_enabled 'person'

判断person表是否被启用

list

查看所有空间下的所有的表

locate_region 'person', 'p1'

定位p1行键所在的HRegion的位置

show_filters

展现所有的过滤器

disable_all 'demo:.*'

禁用demo空间下的所有的表

drop_all 'demo.*'

删除demo空间下的所有的表

enable_all 'demo:.*'

启用demo空间下的所有的表

三、Hive和Hbase的比较

1、Hive本质上是一个用于进行数据仓库管理的工具,在实际过程中经常用于对数据进行分析和清洗,提供了相对标准的SQL结构,底层会将SQL转化为MapReduce来执行,因此Hive的效率相对较低,更适合于离线开发的场景。Hive一般针对历史数据进行分析,一般只提供增加和查询的能力,一般不会提供修改和删除的功能

2、HBase本质上是一个非关系型数据库,在实际过程中,用于存储数据。因为HBase的读写效率较高,吞吐量较大,因此一般使用HBase来存储实时的数据,最终数据会落地到HDFS上。HBase作为数据库,提供了完整的增删改查的能力,但是相对而言,HBase的事务能力较弱。HBase不支持SQL,提供了一套完整的命令

3、总结:Hive强调的是分析能力,但是HBase强调的是存储能力,相同的地方在于两者都是利用HDFS来存储数据

四、HBase的安装

1、硬件环境:至少需要3台虚拟机或者云主机,Centos7.5及以上版本,至少需要双核,至少4G内存+20G磁盘

2、软件环境:JDK1.8+Zookeeper3.5.7+Hadoop3.1.3

3、进入/home/software目录下,上传或者下载HBase的安装包

cd /home/software

rz        hbase-2.4.2-bin.tar.gz

4、解压

tar -xvf hbase-2.4.2-bin.tar.gz

5、配置相关文件

①、进入HBase的配置目录

cd hbase-2.4.2/conf

②、编辑文件hbase-env.sh

export JAVA_HOME=/home/software/jdk1.8.0_131
export HBASE_MANAGES_ZK=false

保存退出,重新生效:

source hbase-env.sh

③、编辑文件hbase-site.xml

<!--指定HBase在HDFS上的数据存储目录-->
<property>
    <name>hbase.rootdir</name>
    <value>hdfs://hadoop01:9000/hbase</value>
</property>
<!--开启HBase的分布式-->
<property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
</property>
<!--配置Zookeeper的连接地址-->
<property>
    <name>hbase.zookeeper.quorum</name>
    <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
</property>
<property>
    <name>hbase.unsafe.stream.capability.enforce</name>
    <value>false</value>
</property>
<property>
    <name>hbase.wal.provider</name>
    <value>filesystem</value>
</property>

④、编辑文件vim regionservers

添加当前的三台主机的主机名

6、需要将Hadoop的核心配置文件拷贝到当前的HBase的配置目录下

cp /home/software/hadoop-3.1.3/etc/hadoop/core-site.xml ./

7、 回到software目录下,远程拷贝给另外两台云主机

cd /home/software/

scp -r hbase-2.4.2 root@hadoop02:$PWD

scp -r hbase-2.4.2 root@hadoop03:$PWD

8、配置三台主机的环境变量

vim /etc/profile

在文件末尾添加:

export HBASE_HOME=/home/software/hbase-2.4.2
export PATH=$PATH:$HBASE_HOME/bin

保存退出,重新生效

source /etc/profile

9、启动

启动Zookeeper

cd /home/software/apache-zookeeper-3.5.7-bin/bin

sh zkServer.sh start

sh zkServer.sh status

在第一台主机上启动Hadoop的HDFS

start-dfs.sh

在第一台主机上启动HBase

start-hbase.sh

三台主机只要有HRegionServer,就成功了

10、访问

可以通过hadoop01:16010来访问HBase的界面 

 五、HBase的API

1、HBase空间API

在我们的IDEA创建maven工程:

在pom.xml引入依赖:

<dependencies>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-common</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-server</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-protocol</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase</artifactId>
            <version>2.4.2</version>
            <type>pom</type>
            <exclusions>
                <exclusion>
                    <groupId>org.glassfish</groupId>
                    <artifactId>javax.el</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-mapreduce</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-zookeeper</artifactId>
            <version>2.4.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

 在包下新建一个类NamespaceDemo,然后进行空间的相关操作:

package org.example.hbase;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;

public class NamespaceDemo {
    private Connection connection;
    private  Admin admin;
    //连接
    @Before
    public void connect() throws IOException {
        //获取HBase配置
        Configuration conf = HBaseConfiguration.create();
        //配置Zookeeper的连接地址
        conf.set("hbase.zookeeper.quorum","hadoop01:2181,hadoop02:2181,hadoop03:2181");
        //发起连接
        connection = ConnectionFactory.createConnection(conf);
        //获取管理权
        admin = connection.getAdmin();
    }
    // 创建空间
    @Test
    public void createNameSpace() throws IOException {
        //构建空间描述器
        NamespaceDescriptor descriptor = NamespaceDescriptor.create("demo").build();
        //创建空间
        admin.createNamespace(descriptor);
    }
    //删除空间
    @Test
    public void deleteNamespace() throws IOException {
        admin.deleteNamespace("demo");
    }

    //获取所有的空间
    @Test
    public void listNamespaces() throws IOException {
        //获取所有空间
        String[] namespaces=admin.listNamespaces();
        //遍历
        for (String namespace : namespaces) {
            System.out.println(namespace);
        }
    }
    @After
    public void close() throws IOException {
        //关闭管理权
        admin.close();
        //关闭连接
        connection.close();
    }
}

2、Hbase表API

同样在上小节的包中新建一个操作表的类:下面是源代码:

package org.example.hbase;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.CompareOperator;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.RegexStringComparator;
import org.apache.hadoop.hbase.filter.ValueFilter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;

public class TableDemo {

    private Connection connection;
    private Admin admin;
    private Table users;

    // 连接
    @Before
    public void connect() throws IOException {
        // 获取HBase的配置
        Configuration conf = HBaseConfiguration.create();
        // 指定Zookeeper的连接地址
        conf.set("hbase.zookeeper.quorum", "hadoop01:2181,hadoop02:2181,hadoop03:2181");
        // 发起连接
        connection = ConnectionFactory.createConnection(conf);
        // 获取管理权
        admin = connection.getAdmin();
        // 指定要操作的表
        users = connection.getTable(TableName.valueOf("users"));
    }

    // 创建表
    @Test
    public void createTable() throws IOException {
        // 构建列族描述器
        ColumnFamilyDescriptor cf1 = ColumnFamilyDescriptorBuilder
                .newBuilder("basic".getBytes(StandardCharsets.UTF_8)).build();
        ColumnFamilyDescriptor cf2 = ColumnFamilyDescriptorBuilder
                .newBuilder("info".getBytes(StandardCharsets.UTF_8)).build();
        // 构建表描述器
        TableDescriptor table = TableDescriptorBuilder.newBuilder(TableName.valueOf("users"))
                .setColumnFamily(cf1).setColumnFamily(cf2).build();
        // 创建表
        admin.createTable(table);
    }

    // 添加数据
    @Test
    public void appendData() throws IOException {
        // 构建Append对象
        Append append = new Append("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列族
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        byte[] info = "info".getBytes(StandardCharsets.UTF_8);
        // 指定列和数据
        append.addColumn(basic, "name".getBytes(StandardCharsets.UTF_8), "David".getBytes(StandardCharsets.UTF_8));
        append.addColumn(basic, "age".getBytes(StandardCharsets.UTF_8), "25".getBytes(StandardCharsets.UTF_8));
        append.addColumn(basic, "gender".getBytes(StandardCharsets.UTF_8), "male".getBytes(StandardCharsets.UTF_8));
        append.addColumn(info, "address".getBytes(StandardCharsets.UTF_8), "beijing".getBytes(StandardCharsets.UTF_8));
        // 添加数据
        users.append(append);
    }

    // 添加/修改数据
    @Test
    public void putData() throws IOException {
        // 构建Put对象
        Put put = new Put("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列和数据
        put.addColumn("basic".getBytes(StandardCharsets.UTF_8), "password".getBytes(StandardCharsets.UTF_8),
                "123456".getBytes(StandardCharsets.UTF_8));
        // 添加/修改数据
        users.put(put);
    }

    // 添加百万条数据 - 7425 ~ 7s
    @Test
    public void putMillionData() throws IOException {
        // 指定列族
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        // 指定列
        byte[] password = "password".getBytes(StandardCharsets.UTF_8);
        // 构建集合实现批量操作
        List<Put> puts = new ArrayList<>();
        // 记录起始时间
        long begin = System.currentTimeMillis();
        // 添加百万条数据
        for (int i = 0; i < 1000000; i++) {
            // 构建Put对象
            Put put = new Put(("u" + i).getBytes(StandardCharsets.UTF_8));
            // 添加数据
            put.addColumn(basic, password, getPassword());
            // 将put对象放到puts集合中
            puts.add(put);
            // 每1000条向HBase中添加一次
            if (puts.size() >= 1000) {
                users.put(puts);
                // 清空集合
                puts.clear();
            }
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }

    // 产生六位大写字母的随机密码
    private byte[] getPassword() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            char c = (char) (Math.random() * 26 + 65);
            sb.append(c);
        }
        return sb.toString().getBytes(StandardCharsets.UTF_8);
    }

    // 获取一行数据 - get 'users', 'u1'
    @Test
    public void getLine() throws IOException {
        // 封装Get对象
        Get get = new Get("u1".getBytes(StandardCharsets.UTF_8));
        // 查询数据,获取结果
        Result result = users.get(get);
        // 获取一行数据
        NavigableMap<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> families = result.getMap();
        for (Map.Entry<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> family : families.entrySet()) {
            // 键表示的是列族名
            System.out.println("Column Family:" + new String(family.getKey()));
            // 值表示的这个列族中所包含的列
            NavigableMap<byte[], NavigableMap<Long, byte[]>> columns = family.getValue();
            for (Map.Entry<byte[], NavigableMap<Long, byte[]>> column : columns.entrySet()) {
                // 键表示的是列名
                System.out.println("\tColumn:" + new String(column.getKey()));
                // 值表示的是实际数据
                NavigableMap<Long, byte[]> values = column.getValue();
                for (Map.Entry<Long, byte[]> value : values.entrySet()) {
                    // 键表示的是时间戳
                    System.out.println("\t\tTimestamp:" + value.getKey());
                    // 值表示的是实际的值
                    System.out.println("\t\tValue:" + new String(value.getValue()));
                }
            }

        }

    }

    // 获取指定行键指定列族的数据 - get 'users', 'u1', 'basic'
    @Test
    public void getFamily() throws IOException {
        // 封装Get对象
        Get get = new Get("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列族
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        get.addFamily(basic);
        // 查询数据,获取结果
        Result result = users.get(get);
        // 获取数据
        NavigableMap<byte[], byte[]> columns = result.getFamilyMap(basic);
        // 遍历
        for (Map.Entry<byte[], byte[]> column : columns.entrySet()) {
            // 键是列族中包含的列名,值是实际数据
            System.out.println(new String(column.getKey()) + "=" + new String(column.getValue()));
        }
    }

    // 获取指定行键指定列的数据 - get 'users', 'u1', 'basic:age'
    @Test
    public void getData() throws IOException {
        // 封装Get对象
        Get get = new Get("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        byte[] password = "password".getBytes(StandardCharsets.UTF_8);
        get.addColumn(basic, password);
        // 查询数据,获取结果
        Result result = users.get(get);
        // 获取实际数据
        byte[] value = result.getValue(basic, password);
        System.out.println(new String(value));
    }

    // 遍历数据 - scan 'users'
    @Test
    public void scan() throws IOException {
        // 封装Scan对象
        // 遍历整表
        Scan scan = new Scan();
        // 获取结果集
        ResultScanner rs = users.getScanner(scan);
        // 遍历结果集
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        byte[] password = "password".getBytes(StandardCharsets.UTF_8);
        for (Result r : rs) {
            byte[] value = r.getValue(basic, password);
            System.out.println(new String(value));
        }
    }

    // 遍历过程中过滤数据
    @Test
    public void filter() throws IOException {
        // 封装Scan对象
        Scan scan = new Scan();
        // 封装Filter对象
        Filter filter = new ValueFilter(CompareOperator.EQUAL, new RegexStringComparator(".*AAA.*"));
        // 设置过滤器
        scan.setFilter(filter);
        // 获取结果集
        ResultScanner rs = users.getScanner(scan);
        byte[] basic = "basic".getBytes(StandardCharsets.UTF_8);
        byte[] password = "password".getBytes(StandardCharsets.UTF_8);
        // 遍历结果集
        for (Result r : rs) {
            byte[] value = r.getValue(basic, password);
            System.out.println(new String(value));
        }
    }

    // 删除数据 - 删除指定行键的指定列
    @Test
    public void deleteData() throws IOException {
        // 封装Delete对象
        Delete del = new Delete("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列
        del.addColumn("basic".getBytes(StandardCharsets.UTF_8), "age".getBytes(StandardCharsets.UTF_8));
        // 删除数据
        users.delete(del);
    }

    // 删除指定行键的指定列族的数据
    @Test
    public void deleteFamily() throws IOException {
        // 封装Delete对象
        Delete del = new Delete("u1".getBytes(StandardCharsets.UTF_8));
        // 指定列族
        del.addFamily("info".getBytes(StandardCharsets.UTF_8));
        // 删除数据
        users.delete(del);
    }

    // 删除指定行键的数据
    @Test
    public void deleteLine() throws IOException {
        // 封装Delete对象
        Delete del = new Delete("u1".getBytes(StandardCharsets.UTF_8));
        // 删除数据
        users.delete(del);
    }

    // 删除表
    @Test
    public void deleteTable() throws IOException {
        // 禁用表
        admin.disableTable(TableName.valueOf("users"));
        // 删除表
        admin.deleteTable(TableName.valueOf("users"));
    }

    // 关流
    @After
    public void close() throws IOException {
        // 关闭管理权
        admin.close();
        // 关闭连接
        connection.close();
    }

}

部分运行结果:

下面是获取一行数据 - get 'users', 'u1'

 下面是获取指定行键指定列族的数据 - get 'users', 'u1', 'basic'

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是小先生

知识是无价的,白嫖也可以的。

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

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

打赏作者

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

抵扣说明:

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

余额充值