【HBase之轨迹】(2)使用 hbase命令 和 JavaAPI 操作 HBase(包括复杂查询--过滤器,HBase 工具类)


0. 前置

前面一弹介绍了如何使用 docker 搭建 hbase
【HBase之轨迹】(1)使用 Docker 搭建 HBase 集群

现在开始通过 hbase 命令和 JavaAPI 使用 HBase
对 HBase 的介绍、运作流程原理和架构等,将在总集篇一起放出来

本篇介绍了 HBase 一系列命令的使用,包括表的增删改查,数据的增删改查
同时列出了官网中提到的各个过滤器和比较器及其功能,最后在命令行和 Java 中进行使用
JavaAPI 另外整理了一个工具集,以及其简单使用


1. 命令行使用

首先进入操作界面

./hbase shell

① DDL :表的增删改查

操作过程中,可以关注 Web 端的 Tables,能看到创建的表的信息

1)	创建表
	create <表名>,<列族名1>,<列族名2>...
例:	create 'user','address','info'

2)	查询所有用户表
	list

3)	查看表详情,可以看到各列族的属性
	describe <表名>
例:	describe 'user'

4)	更改表中列族的属性
	alter <表名>,{NAME=><列族名>,<属性名称>=><属性值>}
例:	alter 'user',{NAME=>'address',VERSIONS=>3}
	该例将 address 列族的最大版本数改为了 3
	表示列族将存储最近 3 个版本的数据,更旧的数据删去
	默认为 1,表示只保留最新版本,有新数据时,其余旧版本的数据都将倍删去

5)	删除表
	先 disable <表名>,后 drop <表名>
例:	disable 'user'
	drop 'user'

6)	创建命名空间
	create_namespace <命名空间名>
例:	create_namespace 'iceclean'

7)	查看所有命名空间
	list_namespace

8)	在指定的命名空间中建表
	create <命名空间名:表名>
例:	create 'iceclean:skill','live','program'
	上边在建表时,没有指定哪一个命名空间,默认就在 default 下建表

9)	删除命名空间
	要删除的命名空间必须为空(下面没有表),不为空的话得先将表删除
	drop_namespace <命名空间名>

② DML :数据的增删改查

注意:在增删操作中,最后边都可以加上时间戳手动指明时间,不加的花由系统默认生成

1)	插入数据(兼更新数据,只要指定了同一个列,就是更新)
	put <表名>,<行键>,<列族名:列名>,<>
例:	put 'user','1','info:name','iceclean'

2)	删除列族/列
	delete <表名>,<行键>,<列族名>
	delete <表名>,<行键>,<列族名:列名>
	注意:如果某个列有多个版本,删除的是最新的版本(所以老版本会跳出来)

3)	删除一整行数据
	deleteall <表名>,<行键>

4)	清空表中的数据
	truncate <表名>

5)	指定行键查询,也可以精确到列族或者列
	get <表名>,<行键>
	get <表名>,<行键>,<列族名>
	get <表名>,<行键>,<列族名:列名>
	注意:如果出现中文,可以加一个参数:{FORMATTER => "toString"}

6)	查询列,且精确到版本号
	get <表名>,<行键>,{COLUMN=><列族名:列名>,VERSIONS=><数量>}
例:	get 'user','101c',{COLUMN=>'info:name',VERSIONS=>10}
	注意,这里虽然指明了要查看 10 个版本,也确实修改了很多个版本
	但如果在建表时,该列指定保留的版本数只为 1,则这里依旧只显示 1 个版本
	因为其余的旧版本并没有被保留下来,自然就查不到啦

7)	全表扫描查询数据
	scan <表名>

8)	计算表数据量
	count <表名> 
	
9)	范围查询,左闭右开,STARTROW 缺省默认为最小,STOPROW 缺省默认为最大
	scan <表名>,{STARTROW=><行键>,STOPROW=><行键>}
例:	scan 'user',{STARTROW=>'101a',STOPROW=>'103f'}

10)	指定列名查询
	增加参数:{COLUMNS => ['<列族名1>:<列名1>', '<列族名2>:<列名2>', ...]}

11)	限制查询
	增加参数:{LIMIT => 3}

12)	查看操作日志
	其中 VERSIONS 表示每条数据最多查看到第几个版本
	scan <表名>,{RAW=>true,VERSIONS=>10}

③ 原子自增 incr

在很多情况下,我们需要某个列值进行自增,如常见的点赞收藏和阅读量等的自增
使用 put 创建的列是不支持自增的,需要使用 incr,语法如下:

icnr <表名>, <行键>, <列族名:列名>, [累加值,默认为1]

2. 过滤器:复杂查询语句

使用 get 只能通过 rowKey 查询数据
而当需要通过条件过滤查询数据时,get 做不到,就需要用到过滤器了

过滤器通常和 scan 结合使用
其实底层也是调用了 HBase 的 JavaAPI,后边 SpringBoot 整合会直接 new 出来用

① 默认过滤器

以下过滤器都有对应的 Java 实现类

种类过滤器名功能
行键过滤器RowFilter实现行键字符串的比较和过滤
PrefixFilterrowkey 前缀过滤器
KeyOnlyFilter只对单元格的键进行过滤和显示,不显示值
FirstKeyOnlyFilter只扫描显示相同键的第一个单元格,其键值对会显示出来
InclusiveStopFilter替代 ENDROW 返回终止条件行
列过滤器FamilyFilter列簇过滤器,只显示对应列簇的数据
QualifierFilter列标识过滤器,只显示对应列名的数据
ColumnPrefixFilter对列名称的前缀进行过滤
MultipleColumnPrefixFilter可以指定多个前缀对列名称过滤
ColumnRangeFilter过滤列名称的范围
值过滤器ValueFilter找到符合值条件的键值对
SingleColumnValueFilter按指定列和指定值过滤,相当于 where key [比较符] value
SingleColumnValueExcludeFilter过滤掉匹配上的键值对
其他过滤器ColumnPaginationFilter对一行的所有列分页,只返回 [offset,offset+limit] 范围内的列
PageFilter对显示结果按行进行分页显示
TimestampsFilter时间戳过滤,支持等值,可以设置多个时间戳
ColumnCountGetFilter限制每个逻辑行返回键值对的个数,在 get 方法中使用
DependentColumnFilter允许用户指定一个参考列或引用列来过滤其他列的过滤器

② 比较器

无论在 Java 还是在 Shell 中,进行过滤都需要比较器辅佐进行

比较器名功能描述表达式缩写
BinaryComparator匹配完整字节数组(字符串)binary:值
BinaryPrefixComparator匹配字节数组前缀(字符串前缀)binaryprefix:值
BitComparator匹配比特位bit:值
NullComparator匹配空值null
RegexStringComparator匹配正则表达式regexstring:值
SubstringComparator匹配子字符串substring:值

③ 实战例子

首先建表以及演示数据:
这里建了一张描述前端标签元素的表,因为没换个标签可以有各自不同的属性,所以可以充分利用 HBase 可随意增加列的特性,来存储它们各自不同的属性
数据只简单插入了两条,更多的数据再自己插入就欧了

这里范例表只用了一个列族,因为如果又多个列族的话会降低 HBase 性能,非必要的情况下一个列族就够了

create 'stardust', 'info'

# 范例数据
put 'stardust', '1', 'info:name', 'btn1'
put 'stardust', '1', 'info:kind', 'button'
put 'stardust', '1', 'info:belong', 'root'
put 'stardust', '1', 'info:x', '100px'
put 'stardust', '1', 'info:y', '200px'
put 'stardust', '1', 'info:width', '50px'
put 'stardust', '1', 'info:height', '100px'
put 'stardust', '1', 'info:store-int', 60

put 'stardust', '2', 'info:name', 'text1'
put 'stardust', '2', 'info:kind', 'label'
put 'stardust', '2', 'info:belong', 'root'
put 'stardust', '2', 'info:x', '100px'
put 'stardust', '2', 'info:y', '150px'
put 'stardust', '2', 'info:width', '30px'
put 'stardust', '2', 'info:height', '100px'
put 'stardust', '2', 'info:store-string', 'label content'

查询示例:

# 查询 id 为 1 的元素
scan 'stardust', {FILTER => "RowFilter(=, 'binary:1')"}

# 过滤出有存储字符串能力的元素(只得到列数据)
scan 'stardust', {FILTER => "QualifierFilter(=, 'binary:store-string')"}

# 查询类型为 button 的元素
scan 'stardust', {FILTER => "SingleColumnValueFilter('info', 'kind', =, 'binary:button')"}

# 查询出值为 100px 的列的元素(只得到列数据)
scan 'stardust', {FILTER => "ValueFilter(=, 'binary:100px')"}

# 查询出 x 坐标在 100px 且名字包含 te 的元素
scan 'stardust', {FILTER => "SingleColumnValueFilter('detail', 'x', =, 'binary:100px') AND SingleColumnValueFilter('info', 'name', =, 'substring:te')"}

3. Java API 使用

Connection 是重量级且线程安全的,需要存下来重复利用
HTable 是轻量级且线程不安全的,需要每次用完都关闭,下一次重新开

① HBase 工具类

下列为参考网上其他工具类自己改写的,底层调用了 HBase 的 JavaAPI
需要修改的是静态代码块中的 zookeeper 配置

public class HBaseUtils {

    private static Connection connection;

    static {
        Configuration configuration = HBaseConfiguration.create();
        configuration.set("hbase.zookeeper.quorum", "hadoop001:12181,hadoop002:12182,hadoop003:12183");
        try {
            connection = ConnectionFactory.createConnection(configuration);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建 HBase 表
     *
     * @param tableName      表名
     * @param columnFamilies 列族的数组
     */
    public static boolean createTable(String tableName, String ... columnFamilies) {
        try {
            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
            if (admin.tableExists(TableName.valueOf(tableName))) {
                admin.close();
                return false;
            }
            TableDescriptorBuilder tableDescriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
            Arrays.stream(columnFamilies).forEach(columnFamily ->
                    tableDescriptor.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily))
                    .setMaxVersions(1)
                    .build()));
            admin.createTable(tableDescriptor.build());
            admin.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 删除 hBase 表
     *
     * @param tableName 表名
     */
    public static boolean deleteTable(String tableName) {
        try {
            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
            // 删除表前需要先禁用表
            admin.disableTable(TableName.valueOf(tableName));
            admin.deleteTable(TableName.valueOf(tableName));
            admin.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 插入数据
     *
     * @param tableName        表名
     * @param rowKey           唯一标识
     * @param columnFamilyName 列族名
     * @param qualifier        列标识
     * @param value            数据
     */
    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier, String value) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Put put = new Put(Bytes.toBytes(rowKey));
            put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));
            table.put(put);
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 插入数据
     *
     * @param tableName        表名
     * @param rowKey           唯一标识
     * @param columnFamilyName 列族名
     * @param pairList         列标识和值的集合
     */
    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List<Pair<String, String>> pairList) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Put put = new Put(Bytes.toBytes(rowKey));
            pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getFirst()), Bytes.toBytes(pair.getSecond())));
            table.put(put);
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 根据 rowKey 获取指定行的数据
     *
     * @param tableName 表名
     * @param rowKey    唯一标识
     */
    public static Result getRow(String tableName, String rowKey) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Get get = new Get(Bytes.toBytes(rowKey));
            Result result = table.get(get);
            printResult(result);
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /** 打印一个结果 */
    public static void printResult(Result result) {
        List<Cell> cells = result.listCells();
        for (Cell cell : cells) {
            // 获取列簇名称
            String cf = Bytes.toString(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength());
            // 获取列名称
            String cn = Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength());
            // 获取值
            String value = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
            System.out.println(cf + ":" + cn + " => " + value);
        }
    }

    /**
     * 获取指定行指定列 (cell) 的最新版本的数据
     *
     * @param tableName    表名
     * @param rowKey       唯一标识
     * @param columnFamily 列族
     * @param qualifier    列标识
     */
    public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Get get = new Get(Bytes.toBytes(rowKey));
            if (!get.isCheckExistenceOnly()) {
                get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
                Result result = table.get(get);
                byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
                table.close();
                return Bytes.toString(resultValue);
            }
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 检索全表
     *
     * @param tableName 表名
     */
    public static ResultScanner getScanner(String tableName) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Scan scan = new Scan();
            return table.getScanner(scan);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 检索表中指定数据
     *
     * @param tableName  表名
     * @param filterList 过滤器
     */
    public static ResultScanner getScanner(String tableName, FilterList filterList) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Scan scan = new Scan();
            scan.setFilter(filterList);
            return table.getScanner(scan);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 检索表中指定数据
     *
     * @param tableName   表名
     * @param startRowKey 起始 RowKey
     * @param endRowKey   终止 RowKey
     * @param filterList  过滤器
     */
    public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey,
                                           FilterList filterList) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Scan scan = new Scan();
            scan.withStartRow(Bytes.toBytes(startRowKey));
            scan.withStopRow(Bytes.toBytes(endRowKey));
            scan.setFilter(filterList);
            return table.getScanner(scan);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 删除指定行记录
     *
     * @param tableName 表名
     * @param rowKey    唯一标识
     */
    public static boolean deleteRow(String tableName, String rowKey) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Delete delete = new Delete(Bytes.toBytes(rowKey));
            table.delete(delete);
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }
    /**
     * 删除指定行指定列
     *
     * @param tableName  表名
     * @param rowKey     唯一标识
     * @param familyName 列族
     * @param qualifier  列标识
     */
    public static boolean deleteColumn(String tableName, String rowKey, String familyName,
                                       String qualifier) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Delete delete = new Delete(Bytes.toBytes(rowKey));
            delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier));
            table.delete(delete);
            table.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }
}


② 使用方法

(1)—— 删除表

可以先删掉上述用命令行创建的表:

void deleteTest() {
    if (HBaseUtils.deleteTable("stardust")) {
        System.out.println("删除成功");
    }
}

(2)—— 创建表并插入数据

void initStardust() {
    // 创建表
    HBaseUtils.createTable("stardust", "info");
    // 插入数据
    String[] kinds = {"button", "label", "input", "img", "div"};
    String kind;
    Random random = new Random(new Date().getTime());
    for (int i = 1; i <= 9; i++) {
        kind = kinds[random.nextInt(5)];
        HBaseUtils.putRow("stardust", "" + i, "info", "id", "" + i);
        HBaseUtils.putRow("stardust", "" + i, "info", "name", kind + random.nextInt(100));
        HBaseUtils.putRow("stardust", "" + i, "info", "kind", "" + kind);
        HBaseUtils.putRow("stardust", "" + i, "info", "belong", "0");

        HBaseUtils.putRow("stardust", "" + i, "info", "x", random.nextInt(500) + "px");
        HBaseUtils.putRow("stardust", "" + i, "info", "y", random.nextInt(500) + "px");
        HBaseUtils.putRow("stardust", "" + i, "info", "width", random.nextInt(500) + "px");
        HBaseUtils.putRow("stardust", "" + i, "info", "height", random.nextInt(500) + "px");

        int skill = random.nextInt(100);
        if (skill < 40) {
            HBaseUtils.putRow("stardust", "" + i, "info", "store-int", random.nextInt(100) + "");
        } else if (skill < 70) {
            HBaseUtils.putRow("stardust", "" + i, "info", "store-string", kind + " content :" + random.nextInt(100));
        } else if (skill < 85) {
            HBaseUtils.putRow("stardust", "" + i, "info", "modify", "(1, 2|x, y|x=x+y)");
        } else if (skill < 95) {
            HBaseUtils.putRow("stardust", "" + i, "info", "check", "(1|x|x=10)");
        } else {
            HBaseUtils.putRow("stardust", "" + i, "info", "clock", "0/30 * * * * ?");
        }
    }
}

(3)—— 打印全表

void printTable(String tableName) {
    ResultScanner scanner = HBaseUtils.getScanner(tableName);
    if (scanner != null) {
        for (Result result : scanner) {
            HBaseUtils.printResult(result);
            System.out.println("---");
        }
    }
}

(4)—— 过滤出 x 在 100px 以上 ,y 在 300px 以上的元素

注意,这里的比较是字符串比较,而不是真正的值比较
所以这里使用的是正则表达式,而非简单的大于小于

void scanTest() {
    SingleColumnValueFilter start = new SingleColumnValueFilter(
            Bytes.toBytes("info"),
            Bytes.toBytes("x"),
            CompareOperator.EQUAL,
            new RegexStringComparator("[2-9][0-9][0-9]px"));

    SingleColumnValueFilter end = new SingleColumnValueFilter(
            Bytes.toBytes("info"),
            Bytes.toBytes("y"),
            CompareOperator.EQUAL,
            new RegexStringComparator((" [3-9][0-9][0-9]px")));

    FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL, start, end);
    ResultScanner scanner = HBaseUtils.getScanner("stardust", filterList);
    if (scanner != null) {
        for (Result result : scanner) {
            HBaseUtils.printResult(result);
            System.out.println("---");
        }
        scanner.close();
    }
}

对工具类的使用可以灵活变通,特别是过滤器可以耍出很多花样,这里就不再示例了
重要的还是对 HBase 工具类的理解,还可以继续往工具类中添加功能(这里示范的只是一个很简单的工具类)


4. 写在最后

使用 HBase 工具类对 HBase 进行操作依旧很麻烦,特别是查询要求复杂时需要用到很多过滤器
所以下一弹会介绍 apache 的另一个项目 Phoenix,可以像 MySQL 一样使用 SQL 操作 HBase
【HBase之轨迹】(3)Apache Phoenix 5.1.2 详细使用,像 MySQL 一样使用 SQL 语句操作 HBase(Shell 操作和 SpringBoot+MyBatis 整合)


冰面上的鱼早就 over 拉(IceClean)

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
### 回答1: HBase-Java API 是一种用于管理 HBase 的编程接口。使用 HBase-Java API,开发人员可以编写 Java 代码来创建、删除、修改和查询 HBase HBase-Java API 提供了一系列类和方法,可以让开发人员轻松地与 HBase 进行交互。例如,可以使用 HBaseAdmin 类来管理 HBase 使用 HTable 类来访问 HBase 的数据,使用 Put 类来插入数据,使用 Get 类来获取数据,等等。总之,HBase-Java APIHBase 的重要组成部分,它为开发人员提供了强大的工具来管理和操作 HBase 。 ### 回答2: HBase是一个分布式的列式存储数据库,在很多大数据应用得到广泛的使用。它采用Hadoop作为其底层基础框架,同时提供了Java API供开发人员使用HBaseJava API为开发人员提供了一个管理的接口,使得开发人员可以对HBase数据库进行创建、读取、修改和删除等基本操作。 首先,我们需要用Java API创建一个HBase数据库使用HBaseJava API创建的流程如下: 1. 首先需要获取HBase Configuration对象,并设置HBase连接参数以连接HBase数据库。 2. 接下来,需要获取HBase Admin对象,以便在操作HBase数据库之前检查是否存在,如果不存在,需要创建该。 3. 通过HBaseJava API创建时,需要指定名、列族的名称以及版本数等属性。 4. 创建时需要考虑的region的分配问题,可以对的region进行手动或自动分片,以此来控制HBase的负载均衡。 创建了HBase数据库之后,我们可以使用Java API进行读写操作。在进行读写操作时,需要使用HBaseJava API提供的Get的方法来获取的数据、Scan方法来扫描整个、以及Put方法来向插入数据。此外,在进行操作时还需要设置一些常见的数据操作参数,例如版本数、时间戳等。 在使用HBaseJava API时,还需要注意一些性能优化的问题,例如何时启用缓存、何时触发分区策略以及如何优化HBase的大小等。这些优化措施能够在HBase的性能以及数据读写时的延迟方面提供很好的支持和帮助。 总的来说,HBaseJava API提供的管理接口为开发人员提供了非常方便和快捷的方式来操作HBase数据库。通过使用这些API,开发人员可以创建、读取、修改和删除的数据,并且能够充分应用HBase的分布式特性来优化数据管理和性能提升。 ### 回答3: HBase是一个开源、分布式、非关系型数据库,它可以存储和处理大规模结构化、半结构化和非结构化数据。HBase Java APIHBase的官方API,它提供了对HBase的管理和操作功能,让开发人员可以通过Java代码轻松地连接到HBase数据库。 在使用HBase Java API管理时,首先需要创建一个HBaseConfiguration对象,它包含了连接HBase数据库所需的配置信息,如Zookeeper地址、HBase根目录等。然后,可以使用HBaseAdmin类创建、删除、修改,以及列族等操作。例如,创建一个可以通过以下代码实现: ``` HBaseAdmin admin = new HBaseAdmin(HBaseConfiguration.create()); HTableDescriptor tableDescriptor = new HTableDescriptor("table_name"); HColumnDescriptor columnDescriptor = new HColumnDescriptor("column_family"); tableDescriptor.addFamily(columnDescriptor); admin.createTable(tableDescriptor); ``` 创建时,需要先通过HTableDescriptor类定义名称,然后通过HColumnDescriptor类定义列族名称。可以通过addFamily()方法将列族添加到描述,最后通过HBaseAdmin的createTable()方法创建。 除了创建之外,HBase Java API还提供了许多其他的操作,如获取信息、获取所有的列、删除等。同时,HBase Java API还提供了对数据的CRUD操作包括put、get、scan、delete等方法,让开发人员可以方便地进行数据操作。 总之,HBase Java API是一个非常强大的工具,它可以使开发人员轻松地管理HBase数据库,并实现数据的高效存储和处理。但是,在使用HBase Java API时,需要了解HBase的基本知识和API的用法,才能更好地发挥其功能。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

寒冰小澈IceClean

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

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

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

打赏作者

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

抵扣说明:

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

余额充值