【十八掌●武功篇】第八掌:HBase之基本操作Java API

这一篇博文是【大数据技术●降龙十八掌】系列文章的其中一篇,点击查看目录:这里写图片描述大数据技术●降龙十八掌


系列文章:
:【十八掌●武功篇】第八掌:HBase之基本概念
【十八掌●武功篇】第八掌:HBase之Shell
【十八掌●武功篇】第八掌:HBase之基本操作Java API
【十八掌●武功篇】第八掌:HBase之过滤器总结
【十八掌●武功篇】第八掌:HBase之性能调优
【十八掌●武功篇】第八掌:HBase之安装与集成 [草稿]

第一部分: HBase 基本读写API

一、 写入数据
1、 单行Put

HBase Java API使用Put对象封装一行数据,包括rowkey、列族信息、列标签信息、单元格版本信息、单元格值。然后使用Put对象对Table中的数据进行写入,包括插入和更新操作。
Put对象插入和更新HBase数据适合小数据量的写操作。


private static void test1() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        //实例化一个Put对象,指定Rowkey
        Put put = new Put(Bytes.toBytes("100003_00201612_500000"));
        //向Put对象里添加一列,列族为cf,列标签为factory_id,值为100003,版本默认为当前时间戳
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"),Bytes.toBytes("100003"));
        //向Put对象里添加一列,列族为cf,列标签为log_date,值为00201612
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("log_date"),Bytes.toBytes("00201612"));
        //将put对象添加进table
        table.put(put);
    } catch (IOException e) {
        e.printStackTrace();
    }
    finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2、 Put列表

Table表的put方法可以传入一个List。
当列表中的多个Put有部分出错时,不会影响其他Put的写入操作。

private static void test3() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        List<Put> list = new ArrayList<>();
        //实例化一个Put对象,指定Rowkey
        Put put = new Put(Bytes.toBytes("100003_00201612_500000"));
        //向Put对象里添加一列,列族为cf,列标签为factory_id,值为100003,版本默认为当前时间戳
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100008"));
        list.add(put);

        Put put2 = new Put(Bytes.toBytes("100004_00201612_500000"));
        //向Put对象里添加一列,列族为cf,列标签为factory_id,值为100003,版本默认为当前时间戳
        put2.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100009"));
        list.add(put2);

        table.put(list);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3、 写缓冲区

每一个table的put操作都是一个客户端和HBase服务器的RPC通信操作,高频率的写入操作时会产生大量的网络数据传输次数,效率比较低。
解决这个问题的一种方法是使用BufferedMutator对象,使用缓冲区,缓冲区负责收集put操作,然后调用RPC操作一次性地put到HBase服务器上。当缓冲区里的数据量到达一定的阀值时或者在调用BufferedMutator的close()方法时,客户端能自动提交更新到HBase服务器。

private static void test2() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    BufferedMutator t = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        t = connection.getBufferedMutator(TableName.valueOf("DLR:ft_fact_data_month_quarter"));

        Put put = null;
        //实例化一个Put对象,指定Rowkey,将put对象添加进table
        put = new Put(Bytes.toBytes("100004_00201612_500000"));
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100004"));
        t.mutate(put);

        //实例化一个Put对象,指定Rowkey,将put对象添加进table
        put = new Put(Bytes.toBytes("100005_00201612_500000"));
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100005"));
        t.mutate(put);

        //实例化一个Put对象,指定Rowkey,将put对象添加进table
        put = new Put(Bytes.toBytes("100006_00201612_500000"));
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100006"));

        t.mutate(put);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (t != null) {
                t.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4、 原子性Put

原子性Put可以保证Put操作本身是原子性的,它是通过在更新之前先检查现有的值,如果和预期一致则进行更新操作,如果不一致就放弃Put操作。
这种原子性的Put非常适用于多客户端并发修改的时候使用。

private static void test4() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        Put put = new Put(Bytes.toBytes("100003_00201612_500000"));
        put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"), Bytes.toBytes("100004"));

        //第一个参数为Rowkey,第二个参数为列族,第三个参数为列标签,第四个参数为参考值,第五个为put对象
        boolean isPass = table.checkAndPut(Bytes.toBytes("100003_00201612_500000"), Bytes.toBytes("cf"),
                Bytes.toBytes("factory_id"), Bytes.toBytes("100006"), put);
        if (isPass) {
            System.out.println("更新成功!");
        } else {
            System.out.println("放弃操作!");
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
二、 读取数据
5、 单行Get

可以使用Get对象来读取HBase里的一行数据,可以指定读取的列族、列标签、版本。

private static void test1() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        //实例化一个Get对象
        Get get = new Get(Bytes.toBytes("100003_00201612_500000"));

        //添加一个要Get的列族
        get.addFamily(Bytes.toBytes("cf"));
        //添加一个要Get的列标签
        get.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        //添加一个要Get的列标签
        get.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("log_date"));
        //去HBase服务器执行查询
        Result result = table.get(get);
        System.out.println(result);
        //结果行的RowKey
        String rowKey=Bytes.toString(result.getRow());
        //打印结果
        for (Cell cell:result.listCells()){            
            //列名
            String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
            //单元格的值
            String val = Bytes.toString(CellUtil.cloneValue(cell));
            System.out.println("RowKey:" + rowKey + "列:" + qualifier + ";值长度:" + CellUtil.cloneValue(cell).length + ";值:" + val);
        }      
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
6、 Result类

如上个Get代码实例中所示:使用Get读取数据返回的数据被封装在一个Result对象里,可以通过getRow()方法来获取RowKey,Result对象的listCells属性是查询结果中所有的单元格,可以借助于CellUtil类来获取每个单元格的列标签信息、单元格值信息。

7、 Get列表

类似于Put列表,Get列表是可以一次请求多行数据的。

private static void test2() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        List<Get> list = new ArrayList<>();

        //实例化一个Get对象
        Get get = new Get(Bytes.toBytes("100003_00201612_500000"));
        //添加一个要Get的列标签
        get.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        list.add(get);

        //实例化一个Get对象
        Get get1 = new Get(Bytes.toBytes("100004_00201612_500000"));
        //添加一个要Get的列标签
        get1.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        list.add(get1);

        //实例化一个Get对象
        Get get2 = new Get(Bytes.toBytes("100005_00201612_500000"));
        //添加一个要Get的列标签
        get2.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        list.add(get2);

        //去HBase服务器执行查询
        Result[] results = table.get(list);

        //打印结果
        for (Result result : results) {
            //结果行的RowKey
            String rowKey = Bytes.toString(result.getRow());
            for (Cell cell : result.listCells()) {
                //列名
                String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
                //单元格的值
                String val = Bytes.toString(CellUtil.cloneValue(cell));
                System.out.println("RowKey:" + rowKey + "列:" + qualifier + ";值长度:" + CellUtil.cloneValue(cell).length + ";值:" + val);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
三、 删除数据
1、 单行Delete
private static void test1(){
    Configuration conf=HBaseConfiguration.create();
    Connection connection=null;
    Table table=null;
    try {
        connection= ConnectionFactory.createConnection(conf);
        table=connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        //创建一个Delete对象,指定Rowkey
        Delete delete=new Delete(Bytes.toBytes("100003_00201612_500000"));
        //指定删除某一个列,只删除最新版本
        delete.addColumn(Bytes.toBytes("cf"),Bytes.toBytes("factory_id"));
        //指定删除某一个列族
        delete.addFamily(Bytes.toBytes("cf"));
        //指定删除方法
        table.delete(delete);
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2、 Delete列表
private static void test2() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        List<Delete> list = new ArrayList<>();

        //创建一个Delete对象,指定Rowkey
        Delete delete = new Delete(Bytes.toBytes("100004_00201612_500000"));
        //指定删除某一个列,只删除最新版本
        delete.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        list.add(delete);

        Delete delete2 = new Delete(Bytes.toBytes("100006_00201612_500000"));
        //指定删除某一个列,删除所有版本
        delete2.addColumns(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        list.add(delete2);

//删除所有的列族、列、所有版本
Delete delete3 = new Delete(Bytes.toBytes("100005_00201612_500000"));
list.add(delete3);

        //指定删除方法
        table.delete(list);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (table != null) {
                table.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
四、 扫描表

HBase中的数据是按照RowKey顺序存储的,扫描表是根据RowKey进行扫描,可以指定扫描的开始rowkey和结束rowkey,但是扫描结果里,是包括开始rowkey的,但是并不包括结束rowkey,如果没有指定开始rowkey,就从表的开始位置扫描,如果没有指定结束rowkey,就扫描到表的末尾。
另外可以指定扫描哪个列族、哪个列、开始Rowkey、结束RowKey、过滤器等。

private static void test1() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    Table table = null;

    try {
        connection = ConnectionFactory.createConnection(conf);
        table = connection.getTable(TableName.valueOf("DLR:ft_fact_data_month_quarter"));
        Scan scan = new Scan();
        //指定开始rowkey
        scan.setStartRow(Bytes.toBytes("000003_00020164_000000"));
        //指定结束rowkey
        scan.setStopRow(Bytes.toBytes("000004_20161117_340000"));
        //指定扫描哪个列族
        scan.addFamily(Bytes.toBytes("cf"));
        //指定扫描哪个列
        scan.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("factory_id"));
        //得到一个表扫描器
        ResultScanner resultScanner = table.getScanner(scan);
        //循环打印扫描结果
        for (Result result : resultScanner) {
            //打印rowkey
            System.out.println("RowKey:" + result.getRow());
            String columnsVal="";
            //循环一行中的各个单元格
            for(Cell cell:result.listCells()){
                //获取单元格所在的列族
                String family= Bytes.toString(CellUtil.cloneFamily(cell));
                //获取单元格所在的列
                String qualifier= Bytes.toString(CellUtil.cloneQualifier(cell));
                //获取单元格所在的值
                String val=Bytes.toString(CellUtil.cloneValue(cell));
                columnsVal+="列族:"+family+",列:"+qualifier+",值:"+val+";";
            }
            System.out.println(columnsVal);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

第二部分: HBase管理功能API

五、 Table模式定义

使用HTableDescriptor对象进行对表的模式定义,实例如下:

private void test1()
{
    HTableDescriptor tableDesc=new HTableDescriptor(TableName.valueOf("Test:demo1"));
    HColumnDescriptor columnDesc=new HColumnDescriptor("cf");
    //添加列族
    tableDesc.addFamily(columnDesc);
    //设置region分割点大小
    tableDesc.setMaxFileSize(524288000);
    //设置表只读
    tableDesc.setReadOnly(true);
    //设置刷写大小
    tableDesc.setMemStoreFlushSize(33554432);
    //设置用户自定义参数
    tableDesc.setValue("mykey","myval");
    //获取自定义参数
    tableDesc.getValue("mykey");
    //移出自定义参数
    tableDesc.remove("mykey");
}

(1) 列族
HBase建表时要指定列族,之后可以添加列族、判断列族是否存在、获取列族列表、获取某一个列族的描述、删除列族等操作。使用HColumnDescriptor对象来对列族进行定义,后面会详细说明HColumnDescriptor。

(2) 文件大小限制
当一个region大小达到配置的大小的时候,HBase会拆分region,这个region进行拆分的分割点可以配置,HTableDescriptor对象的setMaxFileSize()方法来进行设置,当表的数据量特别大时候,可以适量地调高这个值。

(3) 表只读
默认的表是可写的,如果将一个表设置为只读,可以调用HTableDescriptor对象的setReadOnly方法来设置。

(4) 写缓冲区大小
HBase的内存中会预留一个写缓冲区,写入的数据会先存放在写缓冲区中,然后再存入文件中,可以用HTableDescriptor的setMemStoreFlushSize方法设置缓冲区的大小。

(5) 自定义参数
用户可以自定义一些键值对到HTable上。

六、 列族模式定义

列族的模式定义使用HColumnDescriptor类来封装

(1) 列族名称
每个列族都用名字,但是除了构造函数,没有其他重命名列族的途径。

(2) 最大版本数
列族中限制了每个值能保留的最大版本数量。

(3) 压缩
HBase支持插件式的压缩算法,可以用的压缩算法如下:

算法描述
NONE不压缩(HBase默认值)
GZ使用Java提供的或者本地库提供的GZIP
LZO需要独立安装LZO的类库
SNAPPY需要独立安装

通过HColumnDescriptor的setCompressionType来设置压缩算法,压缩类型是Compression.Algorithm枚举值。

(4) 块大小
HBase中存储的文件会被划分为很多小块,这些小块在get或者scan的时候会被加载到内存中,可以设置这个块的大小。
这个块不同于HDFS中的分块,HDFS中分块是为了分布式存储拆分的,并利于MapReduce的计算而划分的,比较大;这里的块用于HBase高效地在内存中加载和缓冲数据,只用于HBase内部,比较小。

(5) 是否开启读取缓存块
默认是是打开的,读取数据时把数据存入缓存并不一定能提高性能,如果只是对列族的get操作和顺序化地scan操作,没有必要启用读取缓存。当有对一个rowkey要进行连续反复访问时,适合将读取缓存打开。

(6) 生存期
值不但可以指定版本数,也可以设置保存的最大生命周期,如果超过设置的时间,数据就会被删除。单位是秒,默认是Integer.MAX_VALUE。

(7) 是否在内存
当设置为true的时候,列族的数据会被优先放入读取缓存。

(8) 布隆过滤器
布隆过滤器可以提高某些时候的查询时间,但是它会增加内存和存储的开销,默认布隆过滤器是被关闭的,可以通过setBloomFilterType设置布隆过滤器,值为BloomType类型的枚举。

类型描述
NONE不使用布隆过滤器
ROW在rowkey上使用布隆过滤器进行过滤
ROWCOL在列标识符级别使用布隆过滤器

七、 表操作

表操作需要创建一个HBaseAdmin实例来进行各种操作。
(1) 创建表

private void test1() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
//创建HBaseAdmin实例
        HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        HTableDescriptor tableDesc = new HTableDescriptor(TableName.valueOf("Test:demo1"));
        HColumnDescriptor columnDesc = new HColumnDescriptor("cf");

        //添加列族
        tableDesc.addFamily(columnDesc);
        //设置region分割点大小
        tableDesc.setMaxFileSize(524288000);
        //设置表只读
        tableDesc.setReadOnly(true);
        //设置刷写大小
        tableDesc.setMemStoreFlushSize(33554432);
        //设置用户自定义参数
        tableDesc.setValue("mykey", "myval");
        //获取自定义参数
        tableDesc.getValue("mykey");
        //移出自定义参数
        tableDesc.remove("mykey");

        //创建表
        admin.createTable(tableDesc);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(2) 预分区建表
预分区创建表时将在创建表时划分出若干特定的Region。

private static void test2() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        HTableDescriptor tableDesc = new HTableDescriptor(TableName.valueOf("weibo:demo2"));
        HColumnDescriptor columnDesc = new HColumnDescriptor("cf");
        tableDesc.addFamily(columnDesc);

        //创建预分区表,指定rowkey边界和分区数
        admin.createTable(tableDesc, Bytes.toBytes(1L),Bytes.toBytes(100L),10);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
private static void test3() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        if (!admin.tableExists(TableName.valueOf("weibo:demo3"))) {
            HTableDescriptor tableDesc = new HTableDescriptor(TableName.valueOf("weibo:demo3"));
            HColumnDescriptor columnDesc = new HColumnDescriptor("cf");
            tableDesc.addFamily(columnDesc);
            //指定划分region的边界值
            byte[][] regions = new byte[][]{
                    Bytes.toBytes("A"),
                    Bytes.toBytes("B"),
                    Bytes.toBytes("C"),
                    Bytes.toBytes("D"),
                    Bytes.toBytes("E")
            };
            //创建预分区表,指定rowkey边界和分区数
            admin.createTable(tableDesc, regions);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(3) 获取表结构

private static void test4() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        if (admin.tableExists(TableName.valueOf("weibo:demo3"))) {
            HTableDescriptor desc = admin.getTableDescriptor(TableName.valueOf("weibo:demo3"));
            System.out.println(desc);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(4) 删除表
在删除表之前要先将表禁用,禁用时会现将内存中未提交到磁盘数据刷写到磁盘,然后关闭所有region,并更新表的元数据,将所有的region标记为下线。删除表后,对应的数据也会被删除。

private static void test5() {
    Configuration conf = HBaseConfiguration.create();
    Connection connection = null;
    try {
        connection = ConnectionFactory.createConnection(conf);
        HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        TableName tableName = TableName.valueOf("weibo:demo3");
        if (admin.tableExists(tableName)) {
            HTableDescriptor desc=admin.getTableDescriptor(tableName);
            System.out.println("禁用前表当前状态isTableAvailable:"+admin.isTableAvailable(tableName));
            System.out.println("禁用前表isTableEnabled:"+admin.isTableEnabled(tableName));
            admin.disableTable(tableName);
            System.out.println("禁用后表当前状态isTableAvailable:"+admin.isTableAvailable(tableName));
            System.out.println("禁用后表isTableEnabled:"+admin.isTableEnabled(tableName));
            admin.deleteTable(tableName);
            System.out.println("删除后表当前状态isTableAvailable:"+admin.isTableAvailable(tableName));
            admin.createTable(desc);
            System.out.println("创建表后表当前状态isTableAvailable:"+admin.isTableAvailable(tableName));
            System.out.println("创建表后表isTableEnabled:"+admin.isTableEnabled(tableName));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
输出:
禁用前表当前状态isTableAvailable:true
禁用前表isTableEnabled:true
禁用后表当前状态isTableAvailable:true
禁用后表isTableEnabled:false
删除后表当前状态isTableAvailable:false
创建表后表当前状态isTableAvailable:true
创建表后表isTableEnabled:true

这一篇博文是【大数据技术●降龙十八掌】系列文章的其中一篇,点击查看目录:这里写图片描述大数据技术●降龙十八掌

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值