自学内容网 自学内容网

Hbase实时分布式NoSQL数据库1

  1. Hbase入门

    • 问题1:Hbase与HDFS、Hive、MySQL有什么区别?
      • 定义:Hbase是一个分布式、大数据量实时随机数据读写、面向列的NoSQL数据库
      • HDFS:Hbase底层也使用了HDFS,Hbase是实时、数据库,HDFS是离线、文件系统
      • Hive:Hbase和Hive底层都基于HDFS,都是类数据库结构,Hive是离线工具数据仓库,Hbase是实时数据库
      • MySQL:MySQL是RDBMS【支持SQL,复杂分析,支持完善事务】,Hbase是NoSQL【不支持SQL,不支持复杂分析以及特殊事务】
    • 问题2:Hbase为什么读写很快?
      • 写:直接写内存,所有删除和修改都是逻辑的,通过插入来实现逻辑删除和逻辑修改
      • 读:优先读内存,内存没有再读HDFS:构建有序、列族
    • 问题3:什么是Namespace、Table、Rowkey、ColumnFamily、Qualifier、VERSIONS?
      • Namespace:就是数据库概念,Hbase自带了一个NS叫做default
      • Table:就是表概念,Hbase中表是分布式的,一张表在集群中会对应着多个Region分区,操作表必须通过ns:tbname
      • Region:就是分区概念,每张Hbase的表可以对应多个分区,每个分区可以存储在不同节点上,默认每张表只有1个分区
      • Rowkey:行健,类似于主键概念,唯一标识一行,也是Hbase唯一索引,Hbase每张表自带这一列,必须指定之一列的值
      • ColumnFamily:列族,对列的分组,将拥有相似IO属性的列划分到同一组中,为了提高查询性能
      • Qualifier:列标签,就是列的概念,访问Hbase表中列必须通过cf:col
      • Versions:多版本,Hbase中一列的数据可以存储多个版本的值,列族级别的属性
    • 问题4:Hbase的集群架构以及角色功能是什么?
      • Hbase:分布式主从架构
        • HMaster:负责管理从节点,负责管理元数据
        • HRegionServer:负责存储所有表的Region,提供客户端对Region读写请求,提供分布式内存
      • HDFS:作为Hbase底层分布式磁盘存储,当RegionServer内存达到阈值,将数据从内存溢写到HDFS中
      • Zookeeper:1-实现辅助选举Active Master,2-存储Hbase管理元数据
  2. Hbase的命令

    • 问题:常见的命令以及语法有哪些?
      • DDL:list_namespace、create_namespace、drop_namespace、list、create、drop、disable、enable、exists
      • DML:put、delete、get、scan
  3. Hbase的Java API

    • 问题:Java API规则和类有哪些?

      • 基本:构建连接

        Configuration conf = HbaseConfiguration.create()
        
        conf.set(Hbase服务端就是ZK地址)
        
        Connection conn = ConnectionFactory.createConnection(conf)
        
      • DDL

        • step1:先构建管理员对象:HbaseAdmin admin = conn.getAdmin
        • step2:调用管理员方法
          • admin.createNamesapce
          • admin.deleteNamespace
          • admin.createTable
          • admin.deleteTable
          • admin.tableExists
      • DML

        • step1:先构建Table对象:Table table = conn.getTable(TableName.valueOf(表名))
        • step2:要做什么操作,必须先构建操作的对象
          • Put、Get、Delete、Scan
          • Hbase中所有的数据都以字节数组方式存储
            • 写:Bytes.toBytes(值)
            • 读:Bytes.toString(字节数组)
          • Cell:代表一列,CellUtil.cloneRow、conleColumnFamilly实现取值
          • Result:代表一个Rowkey
          • ResultScanner:代表多个Rowkey
        • step3:调用操作方法
          • table.put
          • table.get
          • table.delete
          • table.getScanner

``
Apache HBase™ is the Hadoop database, a distributed, scalable, big data store.

Hbase是Hadoop的数据库,分布式的、可扩展的大数据量的存储【HDFS离线场景,适用于一次写入多次读取】

Use Apache HBase™ when you need random, realtime read/write access to your Big Data.

使用Hbase当你需要随机的实时的读写访问的你的大数据量

This project’s goal is the hosting of very large tables – billions of rows X millions of columns – atop clusters of commodity hardware.

这个项目目标管理非常大的表 ---- 数十亿计的行以及百万级别的列

Apache HBase is an open-source, distributed, versioned, non-relational database modeled after Google’s Bigtable: A Distributed Storage System for Structured Data by Chang et al.

Hbase是开源的、分布式的、多版本的、NoSQL非关系型数据库诞生自Google的BigTable结构化的分布式存储系统

Just as Bigtable leverages the distributed data storage provided by the Google File System, Apache HBase provides Bigtable-like capabilities on top of Hadoop and HDFS.

类似于谷歌的BigTable依赖于Google的GFS,Hbase也需要类型的存储例如Hadoop的HDFS

  • 定义:分布式的大数据量实时随机读写的NOSQL数据库

  • 诞生

    • 存储需求的发展:早期【能实现分布式大数据量存储】、现在【能实现实时大数据量的读写】

    • Hbase的诞生:Google 21世纪前三驾马车【GFS、MapReduce、BigTable】

  • 功能:提供分布式的、实时的、随机的、大数据量的、结构化数据的、面向列的持久性数据存储

    • 列式存储:所有行这一列的数据都存储在一起
    • 面向列存储:最小操作单元为列,每一行拥有的列可以不一样
  • 场景大数据量、实时读写的场景

    • 时序数据存储:HBase适用于存储和处理大量的时序数据,如传感器数据、日志信息、监控数据等。它可以按照时间戳进行排序和查询,并快速访问最新的数据。
    • 实时计算和分析:HBase提供快速的随机读写能力和低延迟的访问性能,使它成为实时计算和分析的理想存储引擎。例如,将实时交易数据存储在HBase中,以支持实时风险管理和即时决策。
    • 用户个性化推荐:通过将用户行为数据存储在HBase中,并结合分布式计算引擎进行实时处理,可以构建个性化推荐系统。基于用户的历史行为和偏好,提供针对性的推荐结果。
    • 日志分析:企业需要处理大量的日志数据,包括服务器日志、网络日志、安全日志等。HBase可以作为一个高可扩展的存储层,快速存储和查询这些日志数据,便于后续的分析和监控。
    • 在线广告投放:HBase可以存储广告主要素材信息和用户的点击数据,用于实时匹配和投放广告。通过查询HBase中的数据,可以快速为用户提供个性化的广告内容。
    • 电信和物联网应用:电信行业需要存储大量的用户信息、通话记录和设备状态,而物联网应用需要处理海量的传感器数据。HBase可以满足这两个领域的高吞吐量和低延迟的要求,支持快速访问和查询大规模数据。

知识点04:【掌握】Hbase的存储设计

  • 目标掌握Hbase的存储设计

  • 实施

    • 问题1:为什么说Hbase是面向列存储的NoSQL数据库?

      • 面向行:每次操作的最小单元是一行,例如MySQL
        • 操作:insert、delete、update、select
        • 影响:至少是一行数据
      • 面向列:每次操作的最小单元是一行中的一列,例如Hbase
        • 操作:put、delete、get、scan
        • 影响:最小是某一行中的某一列的数据
          • put:为表中的某一行插入某一列或者更新某一列的值
          • delete:删除表中的某一行的某一列
          • get/scan:查询表中的某一行的某一列的值
        • 特点:每一行可以拥有不同的列
    • 问题2:为什么Hbase和Hive底层都基于HDFS,但是Hbase却是实时的?

      • 原因:Hbase的存储是基于:内存 + HDFS数据优先读写内存【空间小,易丢失,性能高】

        • 写:数据先写入内存,如果内存达到一定条件才写入HDFS
        • 读:数据先读取内存,如果内存中没有才从HDFS中读取
      • 思想:冷热数据分离,实时场景中新数据才是热数据,产生很久的数据属于冷数据

        • 应用场景:实时场景,实时的数据量小,数据一产生立即写入并且立即别读取
        • 设计思想:冷热数据分离
          • 冷:不被经常使用的数据,产生了一定时间的数据叫做冷数据,HDFS
          • 热:经常要被使用的数据,刚产生的数据就是热数据,内存中
    • 问题3:各自之间的区别?

      • Hbase与HDFS
        • 共性:本质上Hbase使用的还是HDFS
        • HDFS:分布式文件系统【文件】、离线、分布式磁盘、大数据量永久性存储
        • Hbase:分布式NoSQL数据库【表】、实时、分布式内存+分布式磁盘【HDFS】、大数据量永久性存储或者临时存储
      • Hbase与Hive
        • 共性:底层都基于HDFS,都做为Hadoop系列的存储工具
        • Hbase:基于hadoop的软件 ,数据库工具, hbase是NoSQL存储容器, 延迟性较低, 接入在线实时业务
        • Hive:基于hadoop的软件, 数仓工具 , Hive延迟较高, 接入离线业务, 用于 OLAP操作
      • Hbase与MySQL
        • 共性:都是数据库,都用于实现数据存储
        • Hbase:NoSQL,不支持SQL语句,仅支持单行事务操作,分布式存储 ,面向列操作,不支持join等分析操作
        • MySQL:RDBMS,支持SQL ,支持多行事务操作, 中心化存储,面向行操作,支持join等分析操作
  • 小结:掌握Hbase的存储设计

知识点05:【掌握】Hbase的存储对象

  • 目标掌握Hbase的存储对象

    • MySQL存储数据是通过Database数据库和Table数据表,那Hbase中怎么实现呢?
  • 实施

    • 对比

      概念MySQLElasticSearchHbase
      数据库databaseIndexNamespace
      数据表tableTypetable
      区别单节点分布式分布式
    • Namespace:命名空间

      • 设计:类似于MySQL中databases数据库的概念
      • 功能:用于对Hbase的数据存储进行一级划分,不同业务的数据存储在不同的Namespace中
      • 规则Hbase中默认自带了一个ns叫做default
    • Table:表

      • 设计:类似于MySQL中table数据表的概念,但是Hbase中的Table是分布式的存储概念,表的数据会被分布式存在多台节点
      • 功能:用于对每个ns中的数据进行二级划分,相同业务的不同数据存储在不同的Table中
      • 规则:Hbase中的任何一张表必须属于某个ns中,访问表的时候必须使用ns:table,除非这张表在default数据库中
      • 思考:Hbase表的分布式是怎么体现的?
    • Region:分区

      • 规则:Hbase的一张表可以划分成多个Region分区,默认每张表只有1个分区,Hbase会根据分区规则对数据进行不同分区读写

      • 设计:类似于HDFS中的Block、ElasticSearch中的Shard、RDD中的Partition,分布式的设计

      • 功能:用于将Hbase中的一张表实现分布式存储,一张表可以对应多个分区

  • 小结:掌握Hbase的存储对象

知识点06:【掌握】Hbase的存储概念

  • 目标掌握Hbase的存储概念

  • 实施

    • 原则:Hbase中有很多不太能理解的设计,设计目的一定是为了加快Hbase读/写[增删改]性能

      • 写:直接写内存,本来就很快
    • 读:虽然刚写入的数据在内存,但是如果因为数据量或者数据延迟导致了要读取的数据在HDFS,怎么办?

      • 索引查询
      • 有序文件
      • 二进制文件
      • 缓存
    • 对比

      概念MySQLElasticSearchHbase
      结构RowKVKV
      组成字段DocId + FieldsRowkey + Qualifier
      主键PrimaryKey【可以没有】DocId【必须有】Rowkey行健
      列族--ColumnFamily
      字段ColumnFieldsQualifier
      版本--VERSIONS
    • Rowkey:行健

      • 设计:类似于MySQL中的主键、ElasticSearch中的DocumentId
      • 功能唯一标识Hbase中的一 “ 行 ” 数据作为Hbase中查询的唯一索引【Hbase中不支持创建索引】
      • 规则:Hbase每张表自带行健这一列
      • 注意行健这一列的值必须由用户指定,不能没有,指定的时候需要考虑索引设计问题
    • ColumnFamily:列族/列簇

      • 设计:为了提高Hbase查询时的性能,在存储时将数据列进行分组,查询时根据分组,实现快速检索

      • 功能:将一张表的不同列划分到不同的组中,物理上相同组的列的数据存储在一起

        • 需求:在一张拥有100列的表中,查询某一列的数据

        • 没有列族:最大比较次数:100次

        • 有了列族:最大比较次数:52次

      • 规则每张表在创建时至少要有一个列族,每一列都必须属于表中的某一个列族,一般会将具有相似IO属性的列划分到一组

      • 注意:列族的个数必须合理,不能过多也不能过少,一般工作中最多不超过3个

    • Qualifier:列标签/列

      • 设计:类似于MySQL中的列,更像ElasticSearch中的Fields
      • 功能:用于存储每行不同字段的数据,Hbase一张表可以拥有上百万列,每一行的拥有的列也可以不同
      • 规则:Hbase中每一列都必须属于表中的某一个列族,访问列时必须通过cf:col来访问,列是Hbase表的最小操作单元
    • Versions:多版本

      • 设计:为了能够实现数据回溯、并发控制、历史数据查询和分析,Hbase允许一列存储多个版本的值

      • 功能:用于实现对每一列存储这一列多个版本的值

      • 规则多版本是列族级别的配置,可以针对每个列族修改,默认每个列族下的所有列只保留1个版本的值

      • 思考:某一行的某一列的值可以存储多个版本,那查询的时候显示哪个版本?怎么区分不同的版本值?

        • 如果一列有多个版本的值:默认只显示最新版本的值
        • Hbase在写入数据的每个版本时,会自动增加数据的Timestamp,用于区分不同的版本
        • 最新版本 = 时间最大的版本
      • 例如

        Rowkeybasicbasicbasicotherotherts
        nameagesexphoneaddrts
        zhangsan_20230101zhangsan18male[标记更新]110Beijing2023-01-01
        femaleSanya2023-06-06
        lisi_20230101lisi20female119[标记删除]Shanghai2023-01-01
        1202023-02-01
        1212023-03-01
    • 思考:Hbase底层的数据到底怎么存储的,为什么Hbase的一张表能够存储上百万列,而且Hbase还能存储多个版本?

      • 设计:逻辑行与物理行

        • 逻辑行:Hbase的一个Rowkey只是逻辑上的一行,物理上对应着多行数据
        • 物理行:Hbase的一个Rowkey的一列是物理上的一行,每一个物理行是一个KV结构
      • 结构:Hbase是KV结构的顺序存储,Key:Rowkey + ColumnFamily + Qualifier + Timestamp,Value:存储的值

        Key:row + cf + col + ts[降序]                                Value:值
        lisi_20230101 + basic + age + 20230101                    20
        lisi_20230101 + basic + name + 20230101                    lisi
        lisi_20230101 + basic + sex + 20230101                    female
        lisi_20230101 + other + addr + 20230101                    Shanghai
        lisi_20230101 + other + phone + 20230301                121
        lisi_20230101 + other + phone + 20230201                120
        lisi_20230101 + other + phone + 20230101                119
        
        zhangsan_20230101 + basic + age + 20230101                    18
        zhangsan_20230101 + basic + name + 20230101                    zhangsan
        zhangsan_20230101 + basic + sex + 20230601                    female
        zhangsan_20230101 + basic + sex + 20230101                    male
        zhangsan_20230101 + other + addr + 20230601                    Sanya
        zhangsan_20230101 + other + addr + 20230101                    Beijing
        zhangsan_20230101 + other + phone + 20230101                110
        
  • 小结:掌握Hbase的存储概念

知识点07:【掌握】Hbase的集群架构

  • 目标掌握Hbase的集群架构

  • 实施

    • 架构普通分布式主从架构

    • 角色

      • HMaster:主节点:管理节点,类似于NameNode的设计
        • 负责所有从节点的管理
        • 负责元数据的管理
      • HRegionServer:从节点:存储节点,类似于DataNode设计
        • 负责管理每张表的分区数据:Region,类似于DataNode存储每个文件的块

        • 对外提供Region的读写请求

        • 用于构建分布式内存

    • 组件

      • Hbase:通过RegionServer构建分布式内存,对外提供基于内存的读写
      • HDFS:构建分布式磁盘,当Hbase中的内存达到一定阈值,将内存的数据溢写到HDFS磁盘上
      • Zookeeper:【辅助选举】:多个Master的Active选举,【存储元数据】:Hbase的管理元数据
  • 小结:掌握Hbase的集群架构

知识点08:【实现】Hbase的集群搭建

  • 目标实现Hbase集群的搭建

  • 实施

    • 集群环境:Spark集群:100 ~ 102

    • 解压安装

      • 上传HBASE安装包到第一台机器的/export/software目录下

        cd /export/software/
        rz
        
      • 解压安装

        # 解压
        tar -zxf hbase-2.1.0.tar.gz -C /export/server/
        
        # 切换
        cd /export/server/hbase-2.1.0/
        
        # 修改权限
        chown -R root:root /export/server/hbase-2.1.0/
        
        # 删除文档
        rm -rf /export/server/hbase-2.1.0/docs/
        
    • 修改配置

      • 切换到配置文件目录下

        cd /export/server/hbase-2.1.0/conf/
        
      • 修改hbase-env.sh

        #28行
        export JAVA_HOME=/export/server/jdk
        #125行
        export HBASE_MANAGES_ZK=false
        
      • 修改hbase-site.xml

        cd /export/server/hbase-2.1.0/
        mkdir datas
        vim conf/hbase-site.xml
        
        <property>
            <name>hbase.tmp.dir</name>
            <value>/export/server/hbase-2.1.0/datas</value>
        </property>
        <property>
            <name>hbase.rootdir</name>
            <value>hdfs://node1.itcast.cn:8020/hbase</value>
        </property>
        <property>
            <name>hbase.cluster.distributed</name>
            <value>true</value>
        </property>
        <property>
            <name>hbase.zookeeper.quorum</name>
            <value>node1.itcast.cn,node2.itcast.cn,node3.itcast.cn</value>
        </property>
        <property>
            <name>hbase.unsafe.stream.capability.enforce</name>
            <value>false</value>
        </property>
        
      • 修改regionservers

        vim conf/regionservers
        
        node1
        node2
        node3
        
      • 配置环境变量

        vim /etc/profile
        
        #HBASE_HOME
        export HBASE_HOME=/export/server/hbase-2.1.0
        export PATH=:$PATH:$HBASE_HOME/bin
        
        source /etc/profile
        
      • 复制jar包

        cp lib/client-facing-thirdparty/htrace-core-3.1.0-incubating.jar lib/
        
    • 分发

      cd /export/server/
      scp -r hbase-2.1.0 node2:$PWD
      scp -r hbase-2.1.0 node3:$PWD
      
    • 服务端启动与关闭

      • step1:启动HDFS【第一台机器】,注意一定要等HDFS退出安全模式再启动Hbase

        start-dfs.sh
        
      • step2:启动ZK

        # 先上传资料中的脚本到/export/server/hbase-2.1.0/bin目录下,然后添加执行权限
        start-zk-all.sh
        
      • step3:启动Hbase

        start-hbase.sh
        
      • 关闭:先关闭Hbase再关闭zk

        stop-hbase.sh
        stop-zk-all.sh
        stop-dfs.sh
        
    • 测试

      • 访问Hbase Web UI【node1:16010】,CDH版本的Hbase会沿用老的端口:10060
    • 搭建Hbase HA

      • 关闭Hbase所有节点

        stop-hbase.sh
        
      • 创建并编辑配置文件

        vim conf/backup-masters
        
        node2
        
      • 启动Hbase集群

    • 测试HA

      • 启动两个Master,强制关闭Active Master,观察StandBy的Master是否切换为Active状态

        hbase-daemon.sh stop master
        
      • 【测试完成以后,删除配置,只保留单个Master模式】

  • 小结:实现Hbase集群的搭建

【模块二:Hbase的命令】

知识点09:【理解】Hbase的开发场景

  • 目标理解Hbase的开发场景

  • 实施

    • 场景1:测试开发

      • 需求:一般用于测试开发或者实现DDL操作

      • 实现:Hbase shell命令行

      • 用法:hbase shell

      • 命令

        • 查看帮助:help
        • 查看命令的用法:help ‘command’
        • 退出:exit
    • 场景2:生产开发

      • 需求:一般用于生产开发,通过MapReduce或者Spark等程序读写Hbase,类似于JDBC

      • 实现:分布式计算程序通过Java API读写Hbase,实现数据处理

      • 用法:在MapReduce或者Spark中集成API

      • 场景3:集群管理

        • 应用场景:运维做运维集群管理,我们开发用的不多

        • 需求:封装Hbase集群管理命令脚本,自动化执行

        • 实现:通过Hbase的客户端运行命令文件,通过调度工具进行调度实现定时运行

        • 用法:hbase shell 文件路径

          • step1:将Hbase的命令封装在一个文件中:vim /export/data/hbase.txt

            list
            exit
            
          • step2:运行Hbase命令文件

            hbase shell /export/data/hbase.txt
            
          • step3:封装到脚本

            #!/bin/bash
            hbase shell /export/data/hbase.txt
            
        • 注意:所有的Hbase命令文件,最后一行命令必须为exit,不然就不会退出

  • 小结:理解Hbase的开发场景

知识点10:【掌握】Hbase DDL Namespace

  • 目标掌握Hbase DDL Namespace的使用

  • 实施

    • 列举所有Namespace

      • 命令:list_namespace

      • 语法

        list_namespace
        
      • 示例

        list_namespace
        
    • 列举某个NameSpace中的表

      • 命令:list_namespace_tables

      • 语法

        list_namespace_tables 'Namespace的名称'
        
      • 示例

        list_namespace_tables 'hbase'
        
    • 创建

      • 命令:create_namespace

      • 语法

        create_namespace 'Namespace的名称'
        
      • 示例

        create_namespace 'heima'
        create_namespace 'itcast'
        
    • 删除

      • 命令:drop_namespace

      • 注意:只能删除空数据库,如果数据库中存在表,不允许删除

      • 语法

        drop_namespace 'Namespace的名称'
        
      • 示例

        drop_namespace 'itcast'
        drop_namespace 'heima'
        
  • 小结:掌握Hbase DDL Namespace的使用

知识点11:【掌握】Hbase DDL Table

  • 目标掌握Hbase DDL Table的使用

  • 实施

    • 列举

      • 命令:list

      • 语法:list

      • 示例

        list
        
    • 创建

      • 命令:create

      • 注意:建表时必须指定表名及至少一个列族

        • SQL:create table if not exists dbname.tbname(id int, name string);
        • Hbase:nsname:tbname + cf
      • 语法

        #表示在ns1的namespace中创建一张表t1,这张表有一个列族叫f1,这个列族中的所有列可以存储5个版本的值
        create 'ns1:t1', {NAME => 'f1', VERSIONS => 5}, {NAME => 'f2'}
        
        #在default的namespace中创建一张表t1,这张表有三个列族,f1,f2,f3,每个列族的属性都是默认的
        create 't1', 'f1', 'f2', 'f3'
        
      • 示例

        #如果需要更改列族的属性,使用这种写法
        create 't1',{NAME=>'cf1'},{NAME=>'cf2',VERSIONS => 3}
        
        #如果不需要更改列族属性
        create_namespace 'itcast'
        create 'itcast:t2','cf1','cf2','cf3' # create 't1',{NAME=>'cf1'},{NAME=>'cf2'},{NAME=>'cf3'}
        
    • 查看

      • 命令:desc

      • 语法

        desc '表名'
        
      • 示例

        desc 't1'
        
    • 删除

      • 命令:drop

      • 语法

        drop '表名'
        
      • 示例

        drop 't1'
        
      • 注意:如果要对表进行删除,必须先禁用表,再删除表

    • 禁用/启用

      • 命令:disable / enable

      • 功能:Hbase为了避免修改或者删除表,影响这张表正在对外提供读写服务

      • 规则:修改或者删除表时,必须先禁用表,表示这张表暂时不能对外提供服务

        • 如果是删除:禁用以后删除
        • 如果是修改:先禁用,然后修改,修改完成以后启用
      • 语法

        disable '表名'
        enable '表名'
        
      • 示例

        disable 't1'
        enable 't1'
        
    • 判断存在

      • 命令:exists

      • 语法

        exists '表名'
        
      • 示例

        exists 't1'
        
  • 小结:掌握Hbase DDL Table的使用

知识点12:【理解】Hbase DML Put

  • 目标理解Hbase DML Put操作

  • 实施

    • 功能:插入 / 更新数据【某一行的某一列】

    • 语法

      #表名+rowkey+列族+列+值
      put 'ns:tbname','rowkey','cf:col','value'
      
    • 示例

      disable 'itcast:t2'
      drop 'itcast:t2'
      create 'itcast:t2','cf1',{NAME=>'cf3',VERSIONS => 2}
      
      put 'itcast:t2','20210201_001','cf1:name','laoda'
      put 'itcast:t2','20210201_001','cf1:age',18
      put 'itcast:t2','20210201_001','cf3:phone','110'
      put 'itcast:t2','20210201_001','cf3:addr','shanghai'
      put 'itcast:t2','20210101_000','cf1:name','laoer'
      put 'itcast:t2','20210101_000','cf3:addr','bejing'
      
    • 注意:put:如果不存在,就插入,如果存在就更新

      put 'itcast:t2','20210101_000','cf1:name','laosan'
      put 'itcast:t2','20210101_000','cf3:addr','guangzhou'
      scan 'itcast:t2',{VERSIONS=>10}
      
    • 观察结果

      • 现象1:Hbase表会自动按照底层的Key【rowkey+cf+col+ts】构建字典有序:逐位比较

        外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

        • 设计目标:加快数据的查询,基于有序的数据查询更快
      • 现象2:没有物理更新和删除:通过插入来代替的,实现逻辑更新和删除,做了标记不再显示

        • 设计目标:提高数据写入的性能,通过插入数据来代替物理删除和物理修改
  • 小结:理解Hbase DML Put操作

知识点13:【理解】Hbase DML Get

  • 目标理解Hbase DML Get

  • 实施

    • 功能:读取某个Rowkey的数据

      • 缺点:get命令最多只能返回一个rowkey的数据,根据Rowkey进行检索数据
      • 优点:Get是Hbase中查询数据最快的方式【Rowkey是Hbase唯一索引】,并不是最常用的方式
    • 语法

      get    表名    rowkey        [列族,列]
      get 'ns:tbname','rowkey'
      get 'ns:tbname','rowkey',[cf]
      get 'ns:tbname','rowkey',[cf:col]
      
    • 示例

      get 'itcast:t2','20210201_001'
      get 'itcast:t2','20210201_001','cf1'
      get 'itcast:t2','20210201_001','cf1:name'
      
  • 小结:理解Hbase DML Get

知识点14:【理解】Hbase DML Delete

  • 目标理解Hbase DML delete

  • 实施

    • 功能:删除Hbase中的数据

    • 语法

      #删除某列的数据
      delete     tbname,rowkey,cf:col
      
      #删除某个rowkey数据
      deleteall  tbname,rowkey
      
      #清空所有数据:生产环境不建议使用,有问题,建议删表重建
      truncate   tbname
      
    • 示例

      delete 'itcast:t2','20210101_000','cf3:addr'
      deleteall 'itcast:t2','20210101_000'
      truncate 'itcast:t2'
      
  • 小结:理解Hbase DML delete

知识点15:【理解】Hbase DML Scan

  • 目标理解Hbase DML Scan

  • 实施

    • 功能:根据条件匹配读取多个Rowkey的数据

    • 语法

      #读取整张表的所有数据
      scan 'tbname'//一般不用
      #根据条件查询:工作中主要使用的场景
      scan 'tbname',{Filter} //用到最多
      
    • 示例

      • 插入模拟数据

        put 'itcast:t2','20210201_001','cf1:name','laoda'
        put 'itcast:t2','20210201_001','cf1:age',18
        put 'itcast:t2','20210201_001','cf3:phone','110'
        put 'itcast:t2','20210201_001','cf3:addr','shanghai'
        put 'itcast:t2','20210201_001','cf1:id','001'
        put 'itcast:t2','20210101_000','cf1:name','laoer'
        put 'itcast:t2','20210101_000','cf3:addr','bejing'
        put 'itcast:t2','20210901_007','cf1:name','laosan'
        put 'itcast:t2','20210901_007','cf3:addr','bejing'
        put 'itcast:t2','20200101_004','cf1:name','laosi'
        put 'itcast:t2','20200101_004','cf3:addr','bejing'
        put 'itcast:t2','20201201_005','cf1:name','laowu'
        put 'itcast:t2','20201201_005','cf3:addr','bejing'
        
      • 查看Scan用法

        scan 't1', {ROWPREFIXFILTER => 'row2', FILTER => "
            (QualifierFilter (>=, 'binary:xyz')) AND (TimestampsFilter ( 123, 456))"}
        
      • 测试Scan的使用

        scan 'itcast:t2'
        #rowkey前缀过滤器
        scan 'itcast:t2', {ROWPREFIXFILTER => '2021'}
        scan 'itcast:t2', {ROWPREFIXFILTER => '202101'}
        #rowkey范围过滤器
        #STARTROW:从某个rowkey开始,包含,闭区间
        #STOPROW:到某个rowkey结束,不包含,开区间
        scan 'itcast:t2',{STARTROW=>'20210101_000'}
        scan 'itcast:t2',{STARTROW=>'20210201_001'}
        scan 'itcast:t2',{STARTROW=>'20210101_000',STOPROW=>'20210201_001'}
        scan 'itcast:t2',{STARTROW=>'20210201_001',STOPROW=>'20210301_007'}
        
        # 不走索引的
        scan 'itcast:t2',{FILTER => "QualifierFilter (>=, 'binary:b')"}
        
    • 在Hbase数据检索,尽量走索引查询:按照Rowkey前缀条件查询

      • 尽量避免走全表扫描,优先使用索引Rowkey查询

      • Hbase所有Rowkey的查询都是前缀匹配,只有按照前缀匹配才走索引

  • 小结:理解Hbase DML Scan

知识点16:【理解】Hbase DML incr、count

  • 目标理解Hbase DML incr、count的使用

  • 实施

    • incr:自动计数命令

      • 功能:一般用于自动计数的,不用记住上一次的值,直接做自增

      • 场景:一般用于做数据的计数

      • 思考:与Put区别

        • put:需要记住上一次的值是什么

        • incr:不需要知道上一次的值是什么,自动计数

      • 语法

        incr  '表名''rowkey','列族:列'
        get_counter '表名''rowkey','列族:列'
        
      • 示例

        create 'NEWS_VISIT_CNT', 'C1'
        incr 'NEWS_VISIT_CNT','20210101_001','C1:CNT',12
        get_counter 'NEWS_VISIT_CNT','20210101_001','C1:CNT'
        incr 'NEWS_VISIT_CNT','20210101_001','C1:CNT'
        
    • count:统计命令

      • 功能:统计某张表的行数【rowkey的个数】

      • 语法

        count  '表名'
        
      • 示例

        count 'itcast:t2'
        
    • 面试题:Hbase中如何统计一张表的行数最快

      • 方案一:分布式计算程序,读取Hbase数据,统计rowkey的个数

        #在第一台机器启动
        start-yarn.sh
        #在第一台机器运行
        hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'itcast:t2'
        
      • 方案二:count命令,相对比较常用,速度中等

        count 'itcast:t2'
        
      • 方案三:协处理器,最快的方式

        • 类似于Hive中的UDF,自己开发一个协处理器,监听表,表中多一条数据,就加1
        • 直接读取这个值就可以得到行数了
  • 小结:理解Hbase DML incr、count的使用

【模块三:Hbase的Java API】

知识点17:【掌握】Java API:Connection

  • 目标掌握JavaAPI Connection

  • 实施

    package bigdata.itcast.cn.hbase.client;
    
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.hbase.HBaseConfiguration;
    import org.apache.hadoop.hbase.client.Connection;
    import org.apache.hadoop.hbase.client.ConnectionFactory;
    import org.junit.After;
    import org.junit.Before;
    
    import java.io.IOException;
    
    /**
     * Todo:通过Java客户端API实现对HbaseDDL操作
     *          Namespace:创建、删除
     *          Table:创建、删除
     */
    public class HbaseDDLClientTest {
        // 定义一个连接对象
        Connection conn = null;
    
        /**
         * 用于获取一个Hbase的连接
         * @throws IOException
         */
        @Before
        public void getHbaseConnection() throws IOException {
            // 构建一个配置对象,管理当前程序的配置
            Configuration conf = HBaseConfiguration.create();
    
            // 在配置中指定Hbase地址:任何一个Hbase客户端要想访问Hbase服务端都要连接Zookeeper
            conf.set("hbase.zookeeper.quorum", "node1.itcast.cn,node2.itcast.cn,node3.itcast.cn");
    
            // 获取一个hbase连接对象
            conn = ConnectionFactory.createConnection(conf);
        }
    
        /**
         * 程序结束释放Hbase所有连接
         * @throws IOException
         */
        @After
        public void closeConnection() throws IOException {
            conn.close();
        }
    }
    
    
  • 小结:掌握JavaAPI Connection

知识点18:【掌握】Java API:DDL

  • 目标掌握Java API DDL

  • 实施

    • 构建管理员Java API中所有的DDL操作都是由管理员对象构建的

      // 在Hbase的Java API中,如果要实现DDL操作,必须先构建Hbase的管理员对象HbaseAdmin
          public HBaseAdmin getHbaseAdmin() throws IOException {
              //从连接中获取管理员对象
              HBaseAdmin admin = (HBaseAdmin) conn.getAdmin();
              return admin;
          }
      
    • 实现DDL管理:创建、删除NS和创建、删除Table

      package bigdata.itcast.cn.hbase.client;
      
      
      import org.apache.hadoop.conf.Configuration;
      import org.apache.hadoop.hbase.HBaseConfiguration;
      import org.apache.hadoop.hbase.NamespaceDescriptor;
      import org.apache.hadoop.hbase.TableName;
      import org.apache.hadoop.hbase.client.*;
      import org.apache.hadoop.hbase.util.Bytes;
      import org.junit.After;
      import org.junit.Before;
      import org.junit.Test;
      
      import java.io.IOException;
      
      /**
       * Todo:通过Java客户端API实现对HbaseDDL操作
       *          Namespace:创建、删除
       *          Table:创建、删除
       */
      public class HbaseDDLClientTest {
          // 定义一个连接对象
          Connection conn = null;
      
          /**
           * 用于获取一个Hbase的连接
           * @throws IOException
           */
          @Before
          public void getHbaseConnection() throws IOException {
              // 构建一个配置对象,管理当前程序的配置
              Configuration conf = HBaseConfiguration.create();
      
              // 在配置中指定Hbase地址:任何一个Hbase客户端要想访问Hbase服务端都要连接Zookeeper
              conf.set("hbase.zookeeper.quorum", "node1.itcast.cn,node2.itcast.cn,node3.itcast.cn");
      
              // 获取一个hbase连接对象
              conn = ConnectionFactory.createConnection(conf);
          }
      
          public HBaseAdmin getHbaseAdmin() throws IOException {
              // 从连接中获取一个管理员
              HBaseAdmin admin = (HBaseAdmin) conn.getAdmin();
              // 返回管理员对象
              return admin;
          }
      
          /**
           * 创建NS
           * @throws IOException
           */
          @Test
          public void createNs() throws IOException {
              // 获取管理员
              HBaseAdmin admin = getHbaseAdmin();
      
              // 所有你要的DDL操作都在admin的方法中
              // 构建一个NS对象
              NamespaceDescriptor descriptor = NamespaceDescriptor
                      .create("heima")
                      .build();
      
              // 创建NS
              admin.createNamespace(descriptor);
      
              // 释放资源
              admin.close();
          }
      
          /**
           * 删除NS
           * @throws IOException
           */
          @Test
          public void dropNs() throws IOException {
              // 获取管理员
              HBaseAdmin admin = getHbaseAdmin();
      
              // 所有你要的DDL操作都在admin的方法中
              admin.deleteNamespace("heima");
      
              // 释放资源
              admin.close();
          }
      
          @Test
          public void createHbaseTable() throws IOException {
              // 获取管理员
              HBaseAdmin admin = getHbaseAdmin();
      
              // 建表:如果表存在,就先删除,itcast:t1, basic-3, other
              // 构建表名对象
              TableName tbname = TableName.valueOf("itcast:t1");
      
              // 先判断表是否存在,如果存在就先删除
              if(admin.tableExists(tbname)){
                  // 禁用
                  admin.disableTable(tbname);
                  // 删除
                  admin.deleteTable(tbname);
              }
      
              // 构建列族描述对象
              ColumnFamilyDescriptor basic = ColumnFamilyDescriptorBuilder
                      .newBuilder(Bytes.toBytes("basic")) // 列族的名称
                      .setMaxVersions(3) // 设置版本数
                      .build();
      
              ColumnFamilyDescriptor other = ColumnFamilyDescriptorBuilder
                      .newBuilder(Bytes.toBytes("other")) // 列族的名称
                      .build();
      
              // 建表的对象
              TableDescriptor table = TableDescriptorBuilder
                      .newBuilder(tbname) // 指定表名
                      .setColumnFamily(basic) // basic
                      .setColumnFamily(other) // other
                      .build();
      
              // 创建表
              admin.createTable(table);
      
              // 释放资源
              admin.close();
          }
      
      
          /**
           * 程序结束释放Hbase所有连接
           * @throws IOException
           */
          @After
          public void closeConnection() throws IOException {
              conn.close();
          }
      }
      
  • 小结:掌握Java API DDL

知识点19:【掌握】Java API:Put

  • 目标掌握Java API Put

  • 实施

    • 构建HTable:DML操作都必须构建Hbase表的对象来进行操作

      //构建Hbase表的实例
      public Table getHbaseTable() throws IOException {
              // 构建表名
              TableName tbname = TableName.valueOf("itcast:t1");
              // 获取一个Hbase表的对象
              Table table = conn.getTable(tbname);
              return table;
          }
      
    • 实现Put

      @Test
          public void testPut() throws IOException {
              // 获取表的对象
              Table table = getHbaseTable();
      
              // 构建Put类的对象:一个Put只能操作一个Rowkey
              Put put = new Put(Bytes.toBytes("20220806_888"));
      
              // 配置put对象
              put.addColumn(Bytes.toBytes("basic"), Bytes.toBytes("name"), Bytes.toBytes("zhangsan"));
              put.addColumn(Bytes.toBytes("basic"), Bytes.toBytes("age"), Bytes.toBytes("18"));
              put.addColumn(Bytes.toBytes("basic"), Bytes.toBytes("sex"), Bytes.toBytes("male"));
              put.addColumn(Bytes.toBytes("other"), Bytes.toBytes("phone"), Bytes.toBytes("110"));
      
              // 对表执行put操作
              table.put(put);
      
              // 释放资源
              table.close();
          }
      
  • 小结:掌握Java API Put

知识点20:【掌握】Java API:Get

  • 目标掌握Java API Get

  • 实施

    • 插入数据

      put 'itcast:t1','20210201_000','basic:name','laoda'
      put 'itcast:t1','20210201_000','basic:age',18
      put 'itcast:t1','20210101_001','basic:name','laoer'
      put 'itcast:t1','20210101_001','basic:age',20
      put 'itcast:t1','20210101_001','basic:sex','male'
      put 'itcast:t1','20210228_002','basic:name','laosan'
      put 'itcast:t1','20210228_002','basic:age',22
      put 'itcast:t1','20210228_002','other:phone','110'
      put 'itcast:t1','20210301_003','basic:name','laosi'
      put 'itcast:t1','20210301_003','basic:age',20
      put 'itcast:t1','20210301_003','other:phone','120'
      put 'itcast:t1','20210301_003','other:addr','shanghai'
      
    • 实现Get

          @Test
          public void testGet() throws IOException {
              // 获取表的对象
              Table table = getHbaseTable();
      
              // 构建Get类的对象
              Get get = new Get(Bytes.toBytes("20220806_888"));
      
              // 配置Get
      //        get.addFamily();  // 指定读取哪个列族
      //        get.addColumn(); // 指定读取哪一列
      
              /*
               * Result:一个Result代表Hbase中一条Rowkey的所有数据
               * Cell:一个Cell代表Hbase中一个Rowkey中的一列的数据
               * CellUtil:专门为Cell对象设计的,用于从Cell取出元素
               */
              Result result = table.get(get);
              // 取出每一列的数据
              for(Cell cell : result.rawCells()){
                  System.out.println("==============================================");
                  // 获取这个Cell中的每个值进行输出: 20220806_888  column=basic:age, timestamp=1691918512803, value=18
                  System.out.println(
                          Bytes.toString(CellUtil.cloneRow(cell)) + "\t" +
                          Bytes.toString(CellUtil.cloneFamily(cell)) + "\t" +
                          Bytes.toString(CellUtil.cloneQualifier(cell)) + "\t" +
                          Bytes.toString(CellUtil.cloneValue(cell)) + "\t" +
                          cell.getTimestamp()
                  );
              }
      
              // 释放资源
              table.close();
          }
      
  • 小结:掌握Java API Get

知识点21:【掌握】Java API:Delete

  • 目标掌握Java API Delete

  • 实施

        @Test
        public void testDelete() throws IOException {
            // 获取表的对象
            Table table = getHbaseTable();
    
            // 构建Delete对象
            Delete delete = new Delete(Bytes.toBytes("20220806_888"));
    
            // 配置delete
            delete.addColumn(Bytes.toBytes("basic"), Bytes.toBytes("sex")); // 删除最新版本
            delete.addColumns(Bytes.toBytes("basic"), Bytes.toBytes("sex")); // 删除所有版本
    
            // 让表执行delete
            table.delete(delete);
    
            // 释放资源
            table.close();
        }
    
  • 小结:掌握Java API Delete

知识点22:【掌握】Java API:Scan

  • 目标掌握Java API Scan

  • 实施

    • Scan

          @Test
          public void testScan() throws IOException {
              // 获取表的对象
              Table table = getHbaseTable();
      
              // 构建Scan类的对象
              Scan scan = new Scan();
      
              /*
               * ResultScanner:表示多个Rowkey的数据,是Result对象的集合
               * Result:一个Result代表Hbase中一条Rowkey的所有数据
               * Cell:一个Cell代表Hbase中一个Rowkey中的一列的数据
               * CellUtil:专门为Cell对象设计的,用于从Cell取出元素
               */
              ResultScanner scanner = table.getScanner(scan);
              // 循环取出每个Rowkey
              for(Result result: scanner){
                  System.out.println("==============================================");
                  System.out.println(Bytes.toString(result.getRow()));
                  // 循环取出每个Rowkey的每一列的数据
                  // 取出每一列的数据
                  for(Cell cell : result.rawCells()){
                      // 获取这个Cell中的每个值进行输出: 20220806_888  column=basic:age, timestamp=1691918512803, value=18
                      System.out.println(
                              Bytes.toString(CellUtil.cloneRow(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneFamily(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneQualifier(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneValue(cell)) + "\t" +
                                      cell.getTimestamp()
                      );
                  }
              }
      
              // 释放资源
              table.close();
          }
      
    • Scan + Filter

      package bigdata.itcast.cn.hbase.client;
      
      import org.apache.hadoop.conf.Configuration;
      import org.apache.hadoop.hbase.*;
      import org.apache.hadoop.hbase.client.*;
      import org.apache.hadoop.hbase.filter.*;
      import org.apache.hadoop.hbase.util.Bytes;
      import org.junit.After;
      import org.junit.Before;
      import org.junit.Test;
      
      import java.io.IOException;
      
      /**
       * @ClassName HbaseScanFilterTest
       * @Description TODO 用于测试通过Hbase Java API 实现 Scan + Filter
       * @Create By     Frank
       */
      public class HbaseScanFilterTest {
      
          //构建Hbase连接的对象
          Connection conn = null;
      
          // todo:1-构建连接
          @Before
          public void getHbaseConnection() throws IOException {
              //构建一个配置对象
              Configuration conf = HBaseConfiguration.create();
              //配置Hbase服务端地址:任何一个Hbase客户端都要连接Zookeeper
              conf.set("hbase.zookeeper.quorum","node1,node2,node3"); //windows必须要能解析地址以及node1.itcast.cn
              //赋值
              conn = ConnectionFactory.createConnection(conf);
          }
      
          // todo:2-从连接中获取操作对象,实现操作
          public Table getHbaseTable() throws IOException {
              // 构建表名
              TableName tbname = TableName.valueOf("itcast:t1");
              // 获取一个Hbase表的对象
              Table table = conn.getTable(tbname);
              return table;
          }
      
          @Test
          // Scan语法:scan ns:tbname filter
          public void testScanFilter() throws IOException {
              // 获取表的对象
              Table table = getHbaseTable();
              // 构建Scan对象
              Scan scan = new Scan();
      
              //todo:需求1:查询2021年1月和2月的数据
      //        scan.withStartRow(Bytes.toBytes("202101"));
      //        scan.withStopRow(Bytes.toBytes("202103"));
      
              //todo:需求2:查询2021年的所有数据
      //        Filter prefixFilter = new PrefixFilter(Bytes.toBytes("2021"));
      
              //todo:需求3:查询所有age = 20的数据:select * from table where age = 20
              Filter valueFilter = new SingleColumnValueFilter(
                      Bytes.toBytes("basic"),
                      Bytes.toBytes("age"),
                      CompareOperator.EQUAL,
                      Bytes.toBytes("20")
              );
              //todo:需求4:查询所有数据的name和age这两列:select name,age from table
              byte[][] prefixes = {
                      Bytes.toBytes("name"),
                      Bytes.toBytes("age")
              };
              Filter columnFilter = new MultipleColumnPrefixFilter(prefixes);
      
              //todo:需求5:查询所有年age = 20的人的name和age
              FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
              filterList.addFilter(valueFilter);
              filterList.addFilter(columnFilter);
      
              //让Scan加载过滤器
              scan.setFilter(filterList);
      
              // 对表执行Scan操作
              /**
               *  ResultScanner:Hbase  JavaAPI中用于封装多个Rowkey的数据的对象,一个ResultScanner对象代表多个Rowkey的聚合
               *  Result:Hbase Java API中用于封装一个Rowkey的数据的对象,一个Result对象就代表一个Rowkey
               *  Cell:Hbase Java API中用于封装一列的数据的对象,一个Cell对象就代表一列
               */
              ResultScanner scanner = table.getScanner(scan);
              // 输出每个Rowkey的每一列的数据
              for (Result rs : scanner) {
                  // 将这个rowkey的每一列打印出来 = 要将这个Result对象中的Cell数组中的每个Cell对象中的值取出来
                  for(Cell cell : rs.rawCells() ){
                      //打印每一列的内容:rowkey、cf、col、value、ts
                      System.out.println(
                              Bytes.toString(CellUtil.cloneRow(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneFamily(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneQualifier(cell)) + "\t" +
                                      Bytes.toString(CellUtil.cloneValue(cell)) + "\t" +
                                      cell.getTimestamp()
                      );
                  }
                  System.out.println("+========================================================+");
              }
              // 释放资源
              table.close();
          }
      
      
          // todo:3-释放资源连接
          @After
          public void closeConn() throws IOException {
              conn.close();
          }
      }
      
  • 小结:掌握Java API Scan

附录一:Hbase Maven依赖

    <repositories>
        <repository>
            <id>aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-mapreduce</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-jobclient</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-core</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-auth</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
         <!-- JUnit 4 依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>
    </dependencies>

原文地址:https://blog.csdn.net/Amzmks/article/details/143718130

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!