Hbase批量查询-scan介绍

1.scan原理

HBase的查询实现只提供两种方式:

1、按指定RowKey 获取唯一一条记录,get方法(org.apache.hadoop.hbase.client.Get)

Get 的方法处理分两种 : 设置了ClosestRowBefore 和没有设置的rowlock .主要是用来保证行的事务性,即每个get 是以一个row 来标记的.一个row中可以有很多family 和column.

 

2、按指定的条件获取一批记录,scan方法(org.apache.Hadoop.hbase.client.Scan)实现条件查询功能使用的就是scan 方式.

1)scan 可以通过setCaching 与setBatch 方法提高速度(以空间换时间);

2)scan 可以通过setStartRow 与setEndRow 来限定范围([start,end)start 是闭区间,

end 是开区间)。范围越小,性能越高。

3)、scan 可以通过setFilter 方法添加过滤器,这也是分页、多条件查询的基础。

 

HBase中scan并不像大家想象的一样直接发送一个命令过去,服务器就将满足扫描条件的所有数据一次性返回给客户端。而实际上它的工作原理如下图所示:

 

上图右侧是HBase scan的客户端代码,其中for循环中每次遍历ResultScanner对象获取一行记录,实际上在客户端层面都会调用一次next请求。next请求整个流程可以分为如下几个步骤:

  • next请求首先会检查客户端缓存中是否存在还没有读取的数据行,如果有就直接返回,否则需要将next请求给HBase服务器端(RegionServer)。
  • 如果客户端缓存已经没有扫描结果,就会将next请求发送给HBase服务器端。默认情况下,一次next请求仅可以请求100行数据(或者返回结果集总大小不超过2M)
  • 服务器端接收到next请求之后就开始从BlockCache、HFile以及memcache中一行一行进行扫描,扫描的行数达到100行之后就返回给客户端,客户端将这100条数据缓存到内存并返回一条给上层业务。

 

HBase 每次 scan 的数据量可能会比较大,客户端不会一次性全部把数据从服务端拉回来。而是通过多次 rpc 分批次的拉取。类似于 TCP 协议里面一段一段的传输,可以做到细粒度的流量控制。至于如何调优,控制每次 rpc 拉取的数据量,就可以通过三个参数来控制。

 

.setCaching => .setNumberOfRowsFetchSize (客户端每次 rpc fetch 的行数)

.setBatch => .setColumnsChunkSize (客户端每次获取的列数)

.setMaxResultSize => .setMaxResultByteSize (客户端缓存的最大字节数)

 

  • hbase.client.scanner.caching - (setCaching):HBase-0.98 默认值为为 100,HBase-1.2 默认值为 2147483647,即 Integer.MAX_VALUE。Scan.next() 的一次 RPC 请求 fetch 的记录条数。配置建议:这个参数与下面的setMaxResultSize配合使用,在网络状况良好的情况下,自定义设置不宜太小, 可以直接采用默认值,不配置。
  • setBatch() 配置获取的列数,假如表有两个列簇 cf,info,每个列簇5个列。这样每行可能有10列了,setBatch() 可以控制每次获取的最大列数,进一步从列级别控制流量。配置建议:当列数很多,数据量大时考虑配置此参数,例如100列每次只获取50列。一般情况可以默认值(-1 不受限)。
  • hbase.client.scanner.max.result.size - (setMaxResultSize):HBase-0.98 无该项配置,HBase-1.2 默认值为 210241024,即 2M。Scan.next() 的一次 RPC 请求 fetch 的数据量大小,目前 HBase-1.2 在 Caching 为默认值(Integer Max)的时候,实际使用这个参数控制 RPC 次数和流量。配置建议:如果网络状况较好(万兆网卡),scan 的数据量非常大,可以将这个值配置高一点。如果配置过高:则可能 loadCache 速度比较慢,导致 scan timeout 异常
  • hbase.server.scanner.max.result.size:服务端配置。HBase-0.98 无该项配置,HBase-1.2 新增,默认值为 10010241024,即 100M。该参数表示当 Scan.next() 发起 RPC 后,服务端返回给客户端的最大字节数,防止 Server OOM。

 

要计算一次扫描操作的RPC请求的次数,用户需要先计算出行数和每行列数的乘积。然后用这个值除以批量大小和每行列数中较小的那个值。最后再用除得的结果除以扫描器缓存值。 用数学公式表示如下:

create 'test1', 'lf', 'sf' 
-- lf: column family of LONG values (binary value) 
-- sf: column family of STRING values

导入数据

put 'test1', 'user1|ts1', 'sf:c1', 'sku1' 
put 'test1', 'user1|ts2', 'sf:c1', 'sku188' 
put 'test1', 'user1|ts3', 'sf:s1', 'sku123' 
put 'test1', 'user2|ts4', 'sf:c1', 'sku2' 
put 'test1', 'user2|ts5', 'sf:c2', 'sku288' 
put 'test1', 'user2|ts6', 'sf:s1', 'sku222' 
put 'test1', 'user3|ts7', 'lf:c1', 12345 
put 'test1', 'user3|ts8', 'lf:c1', 67890

 

1.限制条件

FilterList list = new FilterList(FilterList.Operator.MUST_PASS_ONE); //数据只要满足一组过滤器中的一个就可以SingleColumnValueFilter filter1 = new SingleColumnValueFilter(cf,column,CompareOp.EQUAL,Bytes.toBytes("my value"));list.add(filter1);SingleColumnValueFilter filter2 = new SingleColumnValueFilter(cf,column,CompareOp.EQUAL,Bytes.toBytes("my other value"));list.add(filter2);Scan scan = new Scan();scan.setFilter(list);

2,列值过滤器--SingleColumnValueFilter

SingleColumnValueFilter 用于测试列值相等 (CompareOp.EQUAL ), 不等 (CompareOp.NOT_EQUAL),或单侧范围 (e.g., CompareOp.GREATER)。

构造函数:

(1)比较的关键字是一个字符数组

SingleColumnValueFilter(byte[] family, byte[] qualifier, CompareFilter.CompareOp compareOp, byte[] value)

(2)比较的关键字是一个比较器(比较器下一小节做介绍)

SingleColumnValueFilter(byte[] family, byte[] qualifier, CompareFilter.CompareOp compareOp, ByteArrayComparable comparator)

注意:根据列的值来决定这一行数据是否返回,落脚点在行,而不是列。我们可以设置filter.setFilterIfMissing(true);如果为true,当这一列不存在时,不会返回,如果为false,当这一列不存在时,会返回所有的列信息

测试表user内容如下:

Table table = connection.getTable(TableName.valueOf("user"));SingleColumnValueFilter scvf= new SingleColumnValueFilter(Bytes.toBytes("account"), Bytes.toBytes("name"),CompareOp.EQUAL,"zhangsan".getBytes());scvf.setFilterIfMissing(true); //默认为false, 没有此列的数据也会返回 ,为true则只返回name=lisi的数据Scan scan = new Scan();scan.setFilter(scvf);ResultScanner resultScanner = table.getScanner(scan);for (Result result : resultScanner) {List cells= result.listCells();for (Cell cell : cells) {String row = Bytes.toString(result.getRow());String family1 = Bytes.toString(CellUtil.cloneFamily(cell));String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));String value = Bytes.toString(CellUtil.cloneValue(cell));System.out.println("[row:"+row+"],[family:"+family1+"],[qualifier:"+qualifier+"]"+ ",[value:"+value+"],[time:"+cell.getTimestamp()+"]");}}

如果setFilterIfMissing(true), 有匹配只会返回当前列所在的行数据,基于行的数据 country 也返回了,因为他么你的rowkey是相同的

[row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729]

如果setFilterIfMissing(false),有匹配的列的值相同会返回,没有此列的 name的也会返回,, 不匹配的name则不会返回。

下面 红色是匹配列内容的会返回,其他的不是account:name列也会返回,, name=lisi的不会返回,因为不匹配。

[row:lisi_1495527849910],[family:account],[qualifier:idcard],[value:42963319861234561230],[time:1495556647872][row:lisi_1495527850111],[family:account],[qualifier:password],[value:123451231236],[time:1495556648013][row:lisi_1495527850114],[family:address],[qualifier:city],[value:黄埔],[time:1495556648017][row:lisi_1495527850136],[family:address],[qualifier:province],[value:shanghai],[time:1495556648041][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21],[time:1495556648045][row:lisi_1495527850154],[family:info],[qualifier:sex],[value:女],[time:1495556648056][row:lisi_1495527850159],[family:userid],[qualifier:id],[value:002],[time:1495556648060][row:wangwu_1495595824517],[family:userid],[qualifier:id],[value:009],[time:1495624624131][row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898],[time:1495636370056][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729][row:zhangsan_1495527850951],[family:address],[qualifier:province],[value:guangdong],[time:1495556648855][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878][row:zhangsan_1495527851080],[family:info],[qualifier:sex],[value:男],[time:1495556648983][row:zhangsan_1495527851095],[family:userid],[qualifier:id],[value:001],[time:1495556648996]

 

3 键值元数据

由于HBase 采用键值对保存内部数据,键值元数据过滤器评估一行的键(ColumnFamily:Qualifiers)是否存在 

 

3.1. 基于列族过滤数据的FamilyFilter

构造函数:

FamilyFilter(CompareFilter.CompareOp familyCompareOp, ByteArrayComparable familyComparator)

代码如下:

public static ResultScanner getDataFamilyFilter(String tableName,String family) throws IOException{Table table = connection.getTable(TableName.valueOf("user"));FamilyFilter ff = new FamilyFilter(CompareOp.EQUAL ,new BinaryComparator(Bytes.toBytes("account"))); //表中不存在account列族,过滤结果为空// new BinaryPrefixComparator(value) //匹配字节数组前缀// new RegexStringComparator(expr) // 正则表达式匹配// new SubstringComparator(substr)// 子字符串匹配Scan scan = new Scan();// 通过scan.addFamily(family) 也可以实现此操作scan.setFilter(ff);ResultScanner resultScanner = table.getScanner(scan);return resultScanner;}

测试结果:查询的都是account列簇的内容

[row:lisi_1495527849910],[family:account],[qualifier:idcard],[value:42963319861234561230],[time:1495556647872][row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:lisi_1495527850111],[family:account],[qualifier:password],[value:123451231236],[time:1495556648013][row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898],[time:1495636370056][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729]

3.2. 基于限定符Qualifier(列)过滤数据的QualifierFilter

构造函数:

QualifierFilter(CompareFilter.CompareOp op, ByteArrayComparable qualifierComparator)Table table = connection.getTable(TableName.valueOf("user"));QualifierFilter ff = new QualifierFilter(CompareOp.EQUAL , new BinaryComparator(Bytes.toBytes("name")));// new BinaryPrefixComparator(value) //匹配字节数组前缀// new RegexStringComparator(expr) // 正则表达式匹配// new SubstringComparator(substr)// 子字符串匹配Scan scan = new Scan();// 通过scan.addFamily(family) 也可以实现此操作scan.setFilter(ff);ResultScanner resultScanner = table.getScanner(scan);

测试结果:只返回 name 的列内容

[row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729]

3.3. 基于列名(即Qualifier)前缀过滤数据的ColumnPrefixFilter  

( 该功能用QualifierFilter也能实现 )

构造函数:

ColumnPrefixFilter(byte[] prefix) Table table = connection.getTable(TableName.valueOf("user"));ColumnPrefixFilter ff = new ColumnPrefixFilter(Bytes.toBytes("name"));Scan scan = new Scan();// 通过QualifierFilter的 newBinaryPrefixComparator也可以实现scan.setFilter(ff);ResultScanner resultScanner = table.getScanner(scan);

返回结果:

[row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729]

3.4. 基于多个列名(即Qualifier)前缀过滤数据的MultipleColumnPrefixFilter

MultipleColumnPrefixFilter 和 ColumnPrefixFilter 行为差不多,但可以指定多个前缀

byte[][] prefixes = new byte[][] {Bytes.toBytes("name"), Bytes.toBytes("age")};//返回所有行中以name或者age打头的列的数据MultipleColumnPrefixFilter ff = new MultipleColumnPrefixFilter(prefixes);Scan scan = new Scan();scan.setFilter(ff);ResultScanner rs = table.getScanner(scan);

结果:

[row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21],[time:1495556648045][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878]

3.5. 基于列范围过滤数据ColumnRangeFilter

构造函数:

ColumnRangeFilter(byte[] minColumn, boolean minColumnInclusive, byte[] maxColumn, boolean maxColumnInclusive)

参数解释:

minColumn - 列范围的最小值,如果为空,则没有下限;

minColumnInclusive - 列范围是否包含minColumn ;

maxColumn - 列范围最大值,如果为空,则没有上限;

maxColumnInclusive - 列范围是否包含maxColumn 。

代码:

Table table = connection.getTable(TableName.valueOf("user"));byte[] startColumn = Bytes.toBytes("a");byte[] endColumn = Bytes.toBytes("d");//返回所有列中从a到d打头的范围的数据,ColumnRangeFilter ff = new ColumnRangeFilter(startColumn, true, endColumn, true);Scan scan = new Scan();scan.setFilter(ff);ResultScanner rs = table.getScanner(scan);

结果:返回列名开头是a 到  d的所有列数据

[row:lisi_1495527850114],[family:address],[qualifier:city],[value:黄埔],[time:1495556648017][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21],[time:1495556648045][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878]

4. RowKey

当需要根据行键特征查找一个范围的行数据时,使用Scan的startRow和stopRow会更高效,但是,startRow和stopRow只能匹配行键的开始字符,而不能匹配中间包含的字符:

        byte[] startColumn = Bytes.toBytes("azha");

        byte[] endColumn = Bytes.toBytes("dddf");

        Scan scan = new Scan(startColumn,endColumn);

当需要针对行键进行更复杂的过滤时,可以使用RowFilter:

构造函数:

RowFilter(CompareFilter.CompareOp rowCompareOp, ByteArrayComparable rowComparator)

 

代码:

Table table = connection.getTable(TableName.valueOf("user"));RowFilter rf = new RowFilter(CompareOp.EQUAL ,new SubstringComparator("zhangsan"));// new BinaryPrefixComparator(value) //匹配字节数组前缀// new RegexStringComparator(expr) // 正则表达式匹配// new SubstringComparator(substr)// 子字符串匹配Scan scan = new Scan();scan.setFilter(rf);ResultScanner rs = table.getScanner(scan);

结果:

[row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898],[time:1495636370056][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729][row:zhangsan_1495527850951],[family:address],[qualifier:province],[value:guangdong],[time:1495556648855][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878][row:zhangsan_1495527851080],[family:info],[qualifier:sex],[value:男],[time:1495556648983][row:zhangsan_1495527851095],[family:userid],[qualifier:id],[value:001],[time:1495556648996]

5.PageFilter

指定页面行数,返回对应行数的结果集。

需要注意的是,该过滤器并不能保证返回的结果行数小于等于指定的页面行数,因为过滤器是分别作用到各个region server的,它只能保证当前region返回的结果行数不超过指定页面行数。

构造函数:

PageFilter(long pageSize)

代码:

Table table = connection.getTable(TableName.valueOf("user"));PageFilter pf = new PageFilter(2L);Scan scan = new Scan();scan.setFilter(pf);scan.setStartRow(Bytes.toBytes("zhangsan_"));ResultScanner rs = table.getScanner(scan);

结果:返回的结果实际上有四条,因为这数据来自不同RegionServer, 

[row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898],[time:1495636370056][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan],[time:1495556648729]

6.SkipFilter

根据整行中的每个列来做过滤,只要存在一列不满足条件,整行都被过滤掉。

例如,如果一行中的所有列代表的是不同物品的重量,则真实场景下这些数值都必须大于零,我们希望将那些包含任意列值为0的行都过滤掉。

在这个情况下,我们结合ValueFilter和SkipFilter共同实现该目的:

scan.setFilter(new SkipFilter(new ValueFilter(CompareOp.NOT_EQUAL,new BinaryComparator(Bytes.toBytes(0))));

构造函数:

SkipFilter(Filter filter) 

代码:

Table table = connection.getTable(TableName.valueOf("user"));SkipFilter sf = new SkipFilter(new ValueFilter(CompareOp.NOT_EQUAL,new BinaryComparator(Bytes.toBytes("zhangsan"))));Scan scan = new Scan();scan.setFilter(sf);ResultScanner rs = table.getScanner(scan);

结果:

[row:lisi_1495527849910],[family:account],[qualifier:idcard],[value:42963319861234561230],[time:1495556647872][row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:lisi_1495527850111],[family:account],[qualifier:password],[value:123451231236],[time:1495556648013][row:lisi_1495527850114],[family:address],[qualifier:city],[value:黄埔],[time:1495556648017][row:lisi_1495527850136],[family:address],[qualifier:province],[value:shanghai],[time:1495556648041][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21],[time:1495556648045][row:lisi_1495527850154],[family:info],[qualifier:sex],[value:女],[time:1495556648056][row:lisi_1495527850159],[family:userid],[qualifier:id],[value:002],[time:1495556648060][row:wangwu_1495595824517],[family:userid],[qualifier:id],[value:009],[time:1495624624131][row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898],[time:1495636370056][row:zhangsan_1495527850951],[family:address],[qualifier:province],[value:guangdong],[time:1495556648855][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878][row:zhangsan_1495527851080],[family:info],[qualifier:sex],[value:男],[time:1495556648983][row:zhangsan_1495527851095],[family:userid],[qualifier:id],[value:001],[time:1495556648996]

和原来数据相比  列值为name的 zhagnsan的所在行的 rowkey   为   zhangsan_1495527850824 在上面结果中是过滤了

[row:lisi_1495527849910],[family:account],[qualifier:idcard],[value:42963319861234561230][row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi][row:lisi_1495527850111],[family:account],[qualifier:password],[value:123451231236][row:lisi_1495527850114],[family:address],[qualifier:city],[value:黄埔][row:lisi_1495527850136],[family:address],[qualifier:province],[value:shanghai][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21][row:lisi_1495527850154],[family:info],[qualifier:sex],[value:女][row:lisi_1495527850159],[family:userid],[qualifier:id],[value:002][row:wangwu_1495595824517],[family:userid],[qualifier:id],[value:009][row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646][row:zhangsan_1495527850759],[family:account],[qualifier:passport],[value:5689879898][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china][row:zhangsan_1495527850824],[family:account],[qualifier:name],[value:zhangsan][row:zhangsan_1495527850951],[family:address],[qualifier:province],[value:guangdong][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100][row:zhangsan_1495527851080],[family:info],[qualifier:sex],[value:男][row:zhangsan_1495527851095],[family:userid],[qualifier:id],[value:001]

 

7. FirstKeyOnlyFilter

该过滤器仅仅返回每一行中的第一个cell的值,可以用于高效的执行行数统计操作。

构造函数:

public FirstKeyOnlyFilter()

代码:

Table table = connection.getTable(TableName.valueOf("user"));FirstKeyOnlyFilter fkof = new FirstKeyOnlyFilter();Scan scan = new Scan();scan.setFilter(fkof);ResultScanner rs = table.getScanner(scan);

结果:

[row:lisi_1495527849910],[family:account],[qualifier:idcard],[value:42963319861234561230],[time:1495556647872][row:lisi_1495527850081],[family:account],[qualifier:name],[value:lisi],[time:1495556647984][row:lisi_1495527850111],[family:account],[qualifier:password],[value:123451231236],[time:1495556648013][row:lisi_1495527850114],[family:address],[qualifier:city],[value:黄埔],[time:1495556648017][row:lisi_1495527850136],[family:address],[qualifier:province],[value:shanghai],[time:1495556648041][row:lisi_1495527850144],[family:info],[qualifier:age],[value:21],[time:1495556648045][row:lisi_1495527850154],[family:info],[qualifier:sex],[value:女],[time:1495556648056][row:lisi_1495527850159],[family:userid],[qualifier:id],[value:002],[time:1495556648060][row:wangwu_1495595824517],[family:userid],[qualifier:id],[value:009],[time:1495624624131][row:zhangsan_1495527850759],[family:account],[qualifier:idcard],[value:9897645464646],[time:1495556648664][row:zhangsan_1495527850824],[family:account],[qualifier:country],[value:china],[time:1495636452285][row:zhangsan_1495527850951],[family:address],[qualifier:province],[value:guangdong],[time:1495556648855][row:zhangsan_1495527850975],[family:info],[qualifier:age],[value:100],[time:1495556648878][row:zhangsan_1495527851080],[family:info],[qualifier:sex],[value:男],[time:1495556648983][row:zhangsan_1495527851095],[family:userid],[qualifier:id],[value:001],[time:1495556648996]

看着返回数据还没明白,仅仅返回每一行中的第一个cell的值,可以用于高效的执行行数统计操作。

 

 

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部