app开发者平台在数字化时代的重要性与发展趋势解析
826
2022-09-08
MySQL的分区特性
对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的 代码实际上是对一组底层表的句柄对象(Handler Object)的封装。对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易发现,每一个分区表都有一个使用#分隔命名的表文件。
MySQL实现分区表的方式——对底层表的封装——意味着索引也是按照分区的子表定义的,而没有全局索引。这和Oracle不同,在Oracle中可以更加灵活地定义索引和表是否进行分区。
MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行査询的时候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样査询就无须扫描所有分区——只需要査找包含需要数据的分区就可以了。
分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。
在下面的场景中,分区可以起到非常大的作用:
表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他均是历史数据。分区表的数据更容易维护。例如,想枇量删除大量数据可以使用清除整个分区的方式。另外,还可以对一个独立分区进行优化、检査、修复等操作。分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。可以使用分区表来避免某些特殊的瓶颈,例如IrnioDB的单个索引的互斥访问、ext3文件系统的inode锁竞争等。如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好。
MySQL的分区实现非常复杂,我们不打算介绍实现的全部细节。这里我们将专注在分区性能方面,所以如果想了解更多的关于分区的基础知识,建议阅读MySQL官方手册中的“分区”一节,其中介绍了很多分区相关的基础知识。另外,还可以阅读CREATE TABLE、SHOW CREATE TABLE、ALTER TABLE和INFORMATION_SCHEMA.PARTITIONS、EXPLAIN关于分区部分的介绍。分区特性使得CREATE TABLE和ALTER TABLE命令变得更加复杂了。
分区表本身也有一些限制,下面是其中比较重要的几点:
一个表最多只能有1024个分区。在MySQL5.1中,分区表达式必须是整数,或者是返回整数的表达式。在MySQL5.5中,某些场景中可以直接使用列来进行分区。如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来。分区表中无法使用外键约束。
1.分区表的原理
如前所述,分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handler object)表示,所以我们也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。
分区表上的操作按照下面的操作逻辑进行:
SELECT查询
当査询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。
INSERT操作
当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应底层表。
DELETE操作
当删除一条记录时,分区层先打儿并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。
UPDATE操作
当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后均数据应该放在哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。
有些操作是支持过滤的。例如,当删除一条记录时,MySQL需要先找到这条记录,如果WHERE条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉。这对UPDATE语句同样有效。如果是INSERT操作,则本身就是只命中一个分区,其他分区都会被过滤掉。MySQL先确定这条记录属于哪个分区,再将记录写入对应的底层分区表,无须对任何其他分区进行操作。
虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB,则会在分区层释放对应表锁。这个加锁和解锁过程与普通InnoDB上的査询类似。
后面我们会通过一些例子来看看,当访问一个分区表的时候,打开和锁住所有底层表的代价及其带来的后果。
2.分区表的类型
MySQL支持多种分区表。我们看到最多的是根据范围进行分区,每个分区存储落在某个范围的记录,分区表达式可以是列,也可以是包含列的表达式。例如,下表就可以将每一年的销售额存放在不同的分区里:
CREATE TABLE sales ( order_date DATETIME NOT NULL, -- Other columns omitted) ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) ( PARTITION p_2010 VALUES LESS THAN (2010), PARTITION p_2011 VALUES LESS THAN (2011), PARTITION p_2012 VALUES LESS THAN (2012), PARTITION p_catchall VALUES LESS THAN MAXVALUE );
PARTITION分区子句中可以使用各种函数。但有一个要求,表达式返回的值要是一个确 定的整数,且不能是一个常数。这里我们使用函数YEAR(),也可以使用任何其他的函数,如TO_DAYS()。根据时间间隔进行分区,是一种很常见的分区方式,后面还会再回过头来看这个例子,看看如何优化这个例子来避免一些问题。
MySQL还支持键值、哈希和列表分区,这其中有些还支持子分区,不过在生产环境中很少见到。在MySQL5.5中,还可以使用RANGE COLUMNS类型的分区,这样即使是基于时间的分区也无须再将其转化成一个整数,后面将详细介绍。
在我们看过的一个子分区的案例中,对一个类似于前面我们设计的按时间分区的InnoDB表,系统通过子分区可降低索引的互斥访问的竞争。最近一年的分区的数据会被非常频繁地访问,这会导致大量的互斥量的竞争。使用哈希子分区可以将数据切成多个小片,大大降低互斥量的竞争问题。
我们还看到的一些其他的分区技术包括:
根据键值进行分区,来减少InnoDB的互斥量竞争。使用数学模函数来进行分区,然后将数据轮询放入不同的分区。例如,可以对日期做模7的运算,或者更简单地使用返回周几的函数,如果只想保留最近几天的数据,这样分区很方便。假设表有一个自增的主键列id,希望根据时间将最近的热点数据集中存放。那么必须将时间戳包含在主键当中才行,而这和主键本身的意义相矛盾。这种情况下也可以使用这样的分区表达式来实现相同的目的:HASH(id DIV 1000000),这将为100万数据建立一个分区。这样一方面实现了当初的分区目的,另一方面比起使用时间范围分区还避免了一个问题,就是当超过一定阈值时,如果使用时间范围分区就必须新增分区。
3.如何使用分区表
假设我们希望从一个非常大的表中査询出一段时间的记录,而这个表中包含了很多年的历史数据,数据是按照时间排序的,例如,希望査询最近几个月的数据,这大约有10亿条记录。假设使用的是2012年的硬件设备,而原表中有10TB的数据,这个数据量远大于内存,并且使用的是传统硬盘,不是闪存(多数SSD也没有这么大的空间)。你打算如何査询这个表?如何才能更髙效?
首先很肯定:因为数据量巨大,肯定不能在每次査询的时候都扫描全表。考虑到索引在空间和维护上的消耗,也不希望使用索引。即使真的使用索引,你会发现数据并不是按照想要的方式聚集的,而且会有大量的碎片产生,最终会导致一个査询产生成千上万的随机I/O,应用程序也随之僵死。情况好一点的时候,也许可以通过一两个索引解决一些问题。不过多数情况下,索引不会有任何作用。这时候只有两条路可选:让所有的查询都只在数据表上做顺序扫描,或者将数据表和索引全部都缓存在内存里。
这里需要再陈述一遍:在数据量超大的时候,B-Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表,査询所有符合条件的记录,如果数据量巨大,这将产生大量随机I/0,随之,数据库的响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高。有些系统,如Infobright,意识到这一点,于是就完全放弃使用B-Tree索引,而选择了一些更粗粒度的但消耗更少的方式检索数据,例如在大量数据上只索引对应的一小块元数据。
这正是分区要做的事情。理解分区时还可以将其当作索引的最初形态,以代价非常小的 方式定位到需要的数据在哪一片“区域”。在这片“区域”中,你可以做顺序扫描,可以建索引,还可以将数据都缓存到内存,等等。因为分区无须额外的数据结构记录每个分区有哪些数据——分区不需要精确定位每条数据的位置,也就无须额外的数据结构——所以其代价非常低。只需要一个简单的表达式就可以表达每个分区存放的是什么数据。
为了保证大数据量的可扩展性,一般有下面两个策略:
全量扫描数据,不要任何索引。
可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。只要能够使用WHERE条件,将需要的数据限制在少数分区中,则效率是很髙的。当然,也需要做一些简单的运算保证査询的响应时间能够满足需求。使用该策略假设不用将数据完全放入到内存中,同时还假设需要的数据全都在磁盘上,因为内存相对很小,数据很快会被挤出内存,所以缓存起不了任何作用。这个策略适用于以正常的方式访问大量数据的时候。警告:后面会详细解释,必须将査询需要扫描的分区个数限制在一个很小的数量。
索引数据,并分离热点。
如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。这样査询就可以只访问一个很小的分区表,能够使用索引,也能够有效地使用缓存。
仅仅知道这些还不够,MySQL的分区表实现还有很多陷阱。下面看看都有哪些,以及如何避免。
4.什么情况下会出问题
上面我们介绍的两个分区策略都基于两个非常重要的假设:査询都能够过滤掉很多额外的分区、分区本身并不会带来很多额外的代价。而事实证明,这两个假设在某些场景下会有问题。下面介绍一些可能会遇到的问题。
NULL值会使分区过滤无效
关于分区表一个容易让人误解的地方就是分区的表达式的值可以是NULL :第一个分区是一个特殊分区。假设按照PARTITION BY RANGE YEAR(order_date)分区,那么所有order_date为NULL或者是一个非法值的时候,记录都会被存放到第一个分区。现在假设有下面的査询:WHERE order_date BETWEEN ‘2012-01-01’ AND ‘2012-01-31’。实际上,MySQL会检査两个分区,而不是之前猜想的一个:它会检査2012年这个分区,同时它还会检査这个表的第一个分区。检査第一个分区是因为YEAR()函数在接收非法值的时候可能会返回NULL值,那么这个范围的值可能会返回NULL而被存放到第一个分区了。这一点对于其他很多函数,例如T0_DAYS()也一样(从用户角度来看,这应该是一个缺陷,不过从MySQL开发者的角度来看这是一个特性。)。
如果第一个分区非常大,特别是当使用“全量扫描数据,不要任何索引”的策略时,代价会非常大。而且扫描两个分区来査找列也不是我们使用分区表的初衷。为了避免这种情况,可以创建一个“无用”的第一个分区,例如,上面的例子中可以使用PARTITION p_nulls VALUES LESS THAN (0)来创建第一个分区。如果插入表中的数据都是有效的,那么第一个分区就是空的,这样即使需要检测第一个分区,代价也会非常小。
在MySQL5.5中就不需要这个优化技巧了,因为可以直接使用列本身而不是基于列的函数进行分区:PARTITION BY RANGE COLUMNS(order_date)。所以这个案例最好的解决方法是能够直接使用MySQL5.5的这个语法。
分区列和索引列不匹配
如果定义的索引列和分区列不匹配,会导致査询无法进行分区过滤。假设在列a上定义了索引,而在列b上进行分区。因为每个分区都有其独立的索引,所以扫描列b上的索引就需要扫描每一个分区内对应的索引。如果每个分区内对应索引的非叶子节点都在内存中,那么扫描的速度还可以接受,但如果能跳过某些分区索引当然会更好。要避免这个问题,应该避免建立和分区列不匹配的索引,除非査询中还同时包含了可以过滤分区的条件。
听起来避免这个问题很简单,不过有时候也会遇到一些意想不到的问题。例如,在一个关联査询中,分区表在关联顺序中是第二个表,并且关联使用的索引和分区条件并不匹配。那么关联时针对第一个表符合条件的每一行,都需要访问并捜索第二个表的所有分区。
选择分区的成本可能很高
如前所述分区有很多类型,不同类型分区的实现方式也不同,所以它们的性能也各不相同。尤其是范围分区,对于回答“这一行属于哪个分区”、“这些符合査询条件的行在哪些分区”这样的问题的成本可能会非常高,因为服务器需要扫描所有的分区定义的列表来找到正确的答案。类似这样的线性搜索的效率不高,所以随着分区数的增长,成本会越来越高。
我们所实际碰到的类似这样的最糟糕的一次问题是按行写入大量数据的时候。每写入一行数据到范围分区的表时,都需要扫描分区定义列表来找到合适的目标分区。可以通过限制分区的数量来缓解此问题,根据实践经验,对大多数系统来说,100个左右的分区是没有问题的。
其他的分区类型,比如键分区和哈希分区,则没有这样的问题。
打开并锁住所有底层表的成本可能很高
当查询访问分区表的时候,MySQL需要打开并锁住所有的底层表,这是分区表的另一个开销。这个操作在分区过滤之前发生,所以无法通过分区过滤降低此开销,并且该开销也和分区类型无关,会影响所有的査询。这一点对一些本身操作非常快的査询,比如根据主键査找单行,会带来明显的额外开销。可以用批量操作的方式来降低单个操作的此类开销,例如使用批量插入或者LOAD DATA INFILE、一次删除多行数据,等等。当然同时还是需要限制分区的个数。
维护分区的成本可能很高
某些分区维护操作的速度会非常快,例如新增或者删除分区(当删除一个大分区可能会很慢,不过这是另一回事)。而有些操作,例如重组分区或者类似ALTER语句的操作:这类操作需要复制数据。重组分区的原理与ALTER类似,先创建一个临时的分区,然后将数据复制到其中,最后再删除原分区。
如上所述,分区表不是什么“银弹”。下面是目前分区实现中的一些其他限制:
所有分区都必须使用相同的存储引擎。分区函数中可以使用的函数和表达式也有一些限制。某些存储引擎不支持分区。对于MyISAM的分区表,不能再使用LOAD INDEX INTO CACHE操作。对于MyISAM表,使用分区表时需要打开更多的文件描述符。虽然看起来是一个表,其实背后有很多独立的分区,每一个分区对于存储引擎来说都是一个独立的表。这样即使分区表只占用一个表缓存条目,文件描述符还是需要多个。因此,即使已经配置了合造的表缓存,以确保不会超过操作系统的单个进程可以打开的文件描述符的个数,但对于分区表而言,还是会出现超过文件描述符限制的问题。
最后,需要指出的是较老版本的MySQL问题会更多些。所有的软件都是有bug的。分区表在MySQL5.1中引入,在后面的5.1.40和5.1.50之后修复了很多分区表的bug。在MySQL5.5中,分区表又做了很多改进,这才使得分区表可以逐步考虑用在生产环境了。在MySQL 5.6版本中,分区表做了更多的增强,例如新引入的ALTER TABLE EXCHANGE PARTITION。
5.查询优化
引入分区给査询优化带来了一些新的思路(同时也带来新的bug)。分区最大的优点就是优化器可以根据分区函数来过滤一些分区。根据粗粒度索引的优势,通过分区过滤通常可以让査询扫描更少的数据(在某些场景下)。
所以,对于访问分区表来说,很重要的一点是要在WHERE条件中带入分区列,有时候即 使看似多余的也要带上,这样就可以让优化器能够过滤掉无须访问的分区。如果没有这些条件,MySQL就需要让对应存储引擎访问这个表的所有分区,如果表非常大的话,就可能会非常慢。
使用EXPLAIN PARTITION可以观察优化器是否执行了分区过滤,下面是一个示例:
mysql> EXPLAIN PARTITIONS SELECT * FROM sales \G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2010,p_2011,p_2012 type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 3 Extra:
正如你所看到的,这个查询将访问所有的分区。下面我们在WHERE条件中再加入一个时 间限制条件:
mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day > '2011-01-01'\G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2011,p_2012
MySQL优化器已经很善于过滤分区。比如它能够将范围条件转化为离散的值列表,并根据列表中的每个值过滤分区。然而,优化器也不是万能的。下面查询的WHERE条件理论上可以过滤分区,但实际上却不行:
mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE YEAR(day) = 2010\G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2010,p_2011,p_2012
MySQL只能在使用分区函数的列本身进行比较时才能过滤分区,而不能根据表达式的值去过滤分区,即使这个表达式就是分区函数也不行。这就和査询中使用独立的列才能使用索引的道理是一样的。所以只需要把上面的查询等价地改写为如下形式即可:
mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day -> WHERE day BETWEEN '2010-01-01' AND '2010-12-31'\G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2010
这里写的WHERE条件中带入的是分区列,而不是基于分区列的表达式,所以优化器能够利用这个条件过滤部分分区。一个很重要的原则是:即便在创建分区时可以使用表达式,但在査询时却只能根据列来过滤分区。
优化器在处理查询的过程中总是尽可能聪明地去过滤分区。例如,若分区表是关联操作 中的第二张表,且关联条件是分区键,MySQL就只会在对应的分区里匹配行。(EXPLAIN无法显示这种情况下的分区过滤,因为这是运行时的分区过滤,而不是査询优化阶段的。)
6.合并表
合并表(Merge table)是一种早期的、简单的分区实现,和分区表相比有一些不同的限制,并且缺乏优化。分区表严格来说是一个逻辑上的概念,用户无法访问底层的各个分区,对用户来说分区是透明的。但是合并表允许用户单独访问各个子表。分区表和优化器的结合更紧密,这也是未来发展的趋势,而合并表则是种将被淘汰的技术,在未来的版本中可能被删除。
和分区表类似的是,在MyISAM中各个子表可以被一个结构完全相同的逻辑表所封装。可以简单地把这个表当作一个“老的、早期的、功能有限的”的分区表,因为它自身的特性,甚至可以提供一些分区表没有的功能。
合并表相当于一个容器,里面包含了多个真实表。可以在CREATE TABLE中使用一种特别的UNION语法来指定包含哪些真实表。下面是一个创建合并表的例子:
mysql> CREATE TABLE t1(a INT NOT NULL PRIMARY KEY)ENGINE=MyISAM;mysql> CREATE TABLE t2(a INT NOT NULL PRIMARY KEY)ENGINE=MyISAM;mysql> INSERT INTO t1(a) VALUES(1),(2);mysql> INSERT INTO t2(a) VALUES(1),(2);mysql> CREATE TABLE mrg(a INT NOT NULL PRIMARY KEY) -> ENGINE=MERGE UNION=(t1, t2) INSERT_METHOD=LAST;mysql> SELECT a FROM mrg;+------+| a |+------+| 1 || 1 || 2 || 2 |+------+
注意到,这里最后建立的合并表和前面的各个真实表字段完全相同,在合并表中有的索引各个真实子表也有,这是创建合并表的前提条件。另外还注意到,各个子表在对应列上都有主键限制,但是最终的合并表中仍然出现了重复值,这是合并表的另一个不足:合并表中的每一个子表行为和表定义都是相同,但是合并表在全局上并不受这些条件限制。
这里的语法INSERT_METHOD=LAST告诉MySQL,将所有的INSERT语句都发送给最后一个表。指定FIRST或者LAST关键字是唯一可以控制行插入到合并表的哪一个子表的方式(当然,还是可以直接在SQL中明确地操作任何一个子表)。而分区表则有更多的方式可以控制数据写入到哪一个子表中。
INSERT语句的执行结果可以在最终的合并表中看到,也可以在对应的子表中看到:
mysql> INSERT INTO mrg(a) VALUES(3);mysql> SELECT a FROM t2;+---+| a |+---+| 1 || 2 || 3 |+---+
合并表还有些有趣的限制和特性,例如,在删除合并表或者删除一个子表的时候会怎样? 删除一个合并表,它的子表不会受任何影响,而如果直接删除其中一个子表则可能会有不同的后果,这要视操作系统而定。例如在GNU/Linux上,如果子表的文件描述还是被打开的状态,那么这个表还存在,但是只能通过合并表才能访问到:
mysql> DROP TABLE t1, t2;mysql> SELECT a FROM mrg;+------+| a |+------+| 1 || 1 || 2 || 2 || 3 |+------+
合并表还有很多其他的限制和行为,下面列举的这几点需要在使用的时候时刻记住。
在使用CREATE语句创建一个合并表的时候,并不会检査各个子表的兼容性。如果子表的定义稍有不同,那么MySQL就可能创建出一个后面无法使用的合并表。另外,如果在成功创建了合并表后再修改某个子表的定义,那么之后再使用合并表可能会看到这样的报错:ERROR 1168(HY000): Unable to open underlying table which is differently defined or of non-MyISAM type or doesn’t exist。根据合并表的特性,不难发现,在合并表上无法使用REPLACE语法,无法使用自增字段。更多的细节请参阅MySQL官方手册。如果一个査询访问合并表,那么它需要访问所有子表。这会让根据键査找单行的査询速度变慢,如果能够只访问一个对应表,速度肯定将更快。所以,限制合并表中的子表数量很重要,特别是当合并表是某个关联査询的一部分的时候,因为这时访问一个表的记录数可能会将比较操作传递到关联的其他表中,这时减少记录的访问就是减少整个关联操作。当你打算使用合并表的时候,还需要记住以下几点:
执行范围査询时,需要在每一个子表上各执行一次,这比直接访问单个表的性能要差很多,而且子表越多,性能越糟。全表扫描和普通表的全表扫描速度相同。在合并表上做唯一键和主键査询时,一旦找到一行数据就会停止。所以一旦査询在合并表的某一个子表中找到一行数据,就会立刻返回,不会再访问任何其他的表。子表的读取顺序和CREATE TABLE语句中的顺序相同。如果需要频繁地按照某个特定顺序访问表,那么可以通过这个特性来让合并排序操作更髙效。
因为合并表的各个子表可以直接被访问,所以它还具有一些MySQL5.5分区所不能提供的特性:
一个MyISAM表可以是多个合并表的子表。可以通过直接复制.frm、.MYI、.MYD文件,来实现在不同的服务器之间复制各个子表。在合并表中可以很容易地添加新的子表:直接修改合并表的定义就可以了。可以创建一个合并表,让它只包含需要的数据,例如只包含某个时间段的数据,而在分区表中是做不到这一点的。如果想对某个子表做备份、恢复、修改、修复或者别的操作时,可以先将其从合并表中删除,操作结束后再将其加回去。可以使用来压缩所有的子表。
相反,分区表的子表都是被MySQL隐藏的,只能通过分区表去访问子表。
7.总结
分区表是一种粗粒度的、简易的索引策略,适用于大数据量的过滤场景。最适合的场景是,在没有合适的索引时,对其中几个分区进行全表扫描,或者是只有一个分区和索引是热点,而且这个分区和索引能够都在内存中;限制单表分区数不要超过150个,并且注意某些导致无法做分区过滤的细节,分区表对于单条记录的查询并没有什么优势,需要注意这类査询的性能。
作者:小家电维修
转世燕还故榻,为你衔来二月的花。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~