分库分表实战及中间件(二)

分库分表实战及中间件(二)

首页模拟经营代号DS更新时间:2024-05-09
分库分表实战及中间件(二)前言

分库分表实战及中间件(一)中我们使用了在代码中使用硬编码的方式手动定义路由规则以及获取不同表进行分表设计。

此篇使用中间件来进行分表操作,这儿使用,具体详情可以看官方连接。

shardingSphere简介

apache ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。它由Sharding-jdbc、Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器云原生等各种多样化的应用场景。

ShardingSphere项目状态如下:


ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。

具体分为三部分:

具体之间的关系如下图所示:
我们的应用层使用Sharding-JDBC对数据进行操作,然后可以由于Sharding-Proxy进行代理,


Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar三者区别如下:

Sharding-JDBC

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架的使用。

主要功能

数据分片

分布式事务

数据库治理

图中黄色部分表示的是Sharding-JDBC的入口API,采用工厂方法的形式提供。 目前有

图中蓝色部分表示的是Sharding-JDBC的配置对象,提供灵活多变的配置方式。

图中红色部分表示的是内部对象,由Sharding-JDBC内部使用,应用开发者无需关注。ShardingJDBC通过ShardingRuleConfiguration和MasterSlaveRuleConfiguration生成真正供ShardingDataSource和MasterSlaveDataSource使用的规则对象。ShardingDataSource和MasterSlaveDataSource实现了DataSource接口,是JDBC的完整实现方案。

规则配置

Sharding-JDBC可以通过Java,YAML,Spring命名空间和Spring Boot Starter四种方式配置,开
发者可根据场景选择适合的配置方式。

创建DataSource

通过ShardingDataSourceFactory工厂和规则配置对象获取ShardingDataSource,然后即可通过
DataSource选择使用原生JDBC开发,或者使用JPA, MyBatis等ORM工具。
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, props);

数据分片理论知识核心概念

表概念

在分片之后,同一类表结构的名称(总成)。例如b_order。

在分片之后,由数据源和数据表组成。例如ds0.b_order1

指的是分片规则一致的关系表(主表、子表),例如b_order和b_order_item,均按照
order_id分片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积
关联,可以提升关联查询效率。

b_order:b_order0、b_order1
b_order_item:b_order_item0、b_order_item1 select * from b_order o join b_order_item i on(o.order_id=i.order_id) where o.order_id in (10,11);

如果不配置绑定表关系,采用笛卡尔积关联,会生成4个SQL

select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order0 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);

如果配置绑定表关系,生成2个SQL

select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);

在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能需要与海量数据的表进行关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全相同。

分片算法(ShardingAlgorithm)

由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供4种分片算法。

分片策略(ShardingStrategy)

分片策略包含分片键和分片算法,真正可用于分片操作的是分片键 分片算法,也就是分片策
略。目前提供5种分片策略。

只支持单分片键。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,对于
简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。如: t_user_$->
{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

分片策略配置

对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。

用于配置数据被分配的目标数据源。

用于配置数据被分配的目标表,由于表存在与数据源内,所以表分片策略是依赖数据源分片策略结果的。

流程剖析SQL解析

SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。
Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:

查询优化

负责合并和优化分片条件,如OR等。

SQL路由

根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。

SQL改写

将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。

SQL执行

通过多线程执行器异步执行SQL。

结果归并

将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使
用装饰者模式的追加归并这几种方式。

SQL使用规范

具体详情可以查官方说明文档点击跳转

Sharding-JDBC使用

首先引入依赖,这儿我们直接使用JPA来进行操作

xml复制代码 <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>$4.1.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>${springboot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${springboot.version}</version> </dependency>

首先我们来演示分表即单库中多个表,即分表策略

我们准备两个表

实体类

java复制代码@Entity @Table(name="position") public class Position implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "name") private String name; @Column(name = "salary") private String salary; @Column(name = "city") private String city; //set get 省略 }

JPA接口

java复制代码public interface PositionRepository extends JpaRepository<Position,Long> {}

策略以及数据源配置

ini复制代码 #打印shardingsphere sql spring.shardingsphere.props.sql.show=true ## 分表 # 分库信息配置 spring.shardingsphere.datasource.names=test0 #配置数据库信息 #配置连接池 spring.shardingsphere.datasource.test0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test0.jdbc-url=jdbc:mysql://localhost:3306/test0 spring.shardingsphere.datasource.test0.username=root spring.shardingsphere.datasource.test0.password=root # 分表配置 spring.shardingsphere.sharding.tables.position.actual-data-nodes=test0.position_${0..1} # 配置分片数据库 字段 spring.shardingsphere.sharding.tables.position.table-strategy.Inline.sharding-column=id # 配置分片规则 这儿根据id取模2 进行单库分表 spring.shardingsphere.sharding.tables.position.table-strategy.inline.algorithm-expression=position_$->{id % 2} # 配置指定使用雪花算法生成id #对应主键字段名 spring.shardingsphere.sharding.tables.position.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position.key-generator.type=SNOWFLAKE 分片配置

这儿我们使用行表达式进行分片配置

具体配置

只需要在配置中使用 e x p r e s s i o n 或 { expression }或 expression或->{ expression }标识行表达式即可。例如:

行表达式中如果出现多个 或 {}或 或->{}表达式,整个表达式结果会将每个子表达式结果进行笛卡尔(积)组合。例如,以下行表达式:

最终会解析为:

数据节点配置:

分片算法配置

行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。

shell复制代码ds${id % 10} 或者 ds$->{id % 10}

表示的结果为:ds0、ds1、ds2… ds9

分布式主键

ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

内置主键生成器:

自定义主键生成器:

在Apache ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的。注意:在resources目录下新建META-INF文件夹,再新建services文件夹,然后新建文件的名字为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,打开文件,复制自定义主键类全路径到文件中保存。

ini复制代码#对应主键字段名 spring.shardingsphere.sharding.tables.t_book.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.t_book.key- generator.type=TestKEY

然后测试一下分表

java复制代码 @org.junit.Test public void insert(){ for (int i = 0; i < 100; i ) { Position position = new Position(); position.setCity("test -> " i); position.setName("name" i); position.setSalary(""); positionRepository.save(position); } }

日志可以看到

然后查看数据库

可以看到 尾号为0的表都是偶数值,尾号为1的表都是奇数,这样我们的单库分表就成功了。

但是如果对于数据量倾斜严重的数据我们应该如何取存储呢?比如上一篇博文中不同的客户对应的数据不一致,我们将数据量小的几个客户放在一张表中(或者存放原表不分离),一些多的各方一张表中。此时,分表策略就不适合我们这种业务场景,对此我们可以自己自定义一个分片测了。

自定义分表算法

ini复制代码# 自定义分表算法 spring.shardingsphere.sharding.tables.position.table-strategy.standard.sharding-column=name spring.shardingsphere.sharding.tables.position.table-strategy.standard.precise-algorithm-class-name=com.udeam.config.MyPreciseShardingAlgorithm

自定义分表策略
我们根据name来分表,0结尾的存在position_0中 1寸1 其他存3中

java复制代码public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { //availableTargetNames 表示所有的表 即 节点中配置的 spring.shardingsphere.sharding.tables.position.actual-data-nodes=test0.position_${0..4} System.out.println("availableTargetNames->" availableTargetNames.toString()); if (shardingValue.getValue().endsWith("0")) { return "position_0"; } else if (shardingValue.getValue().endsWith("1")) { return "position_1"; } else { return "position_3"; } } }

其实这个变量集合就是我们定义的所有的表,我们可以根据自己的规则进行选择。

这儿的shardingValue 是我们的分片属性 ,可以根据这个属性来进行选择不同的表

测试一下可以看到

自定义主键

我们还是使用雪花算法来生成主键,只不过在配置类中使用自己的类。

Sharding中提供了自定义主键的接口

php复制代码public interface ShardingKeyGenerator extends TypeBasedSPI { Comparable<?> generateKey(); }

可以看到 内部默认实现了雪花算法和UUID ,还有一个TestID是我们自己定义的主键实现类

通过实现接口我们来自定义自己的主键策略

java复制代码public class TestId implements ShardingKeyGenerator { @Override public Comparable<?> generateKey() { System.out.println(" ---- 自定义主键 ---- "); return new SnowflakeShardingKeyGenerator().generateKey(); } @Override public String getType() { return "TESTKEY"; //需要注定的主键类型名 } @Override public Properties getProperties() { return null; } @Override public void setProperties(Properties properties) { } }

配置

ini复制代码 ##对应主键字段名 spring.shardingsphere.sharding.tables.position.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position.key-generator.type=TESTKEY

还需要在类路径下指定全限定命名,文件夹和文件名是固定写死的,不能随意更改,内部的实现类名是我们自己定义的算法类。


测试后可以看到,打印出了我们的日志,以及编译后的sql,生成的主键

分库

分表解决了单库中数据量过多以及查询效率问题,并没有解决并发以及数据IO等性能,针对数据库并发以及IO等问题,我们可以进行分库操作,如上面的问题,我们可以在两个或者多个库中存储数据。

如下图,我们在test0,test1库中分别有position表

我们可以按照主键的奇数偶数来选择存放test0,test1库中的表position

因为是两个数据库,所以我们需要使用分库策略,因为表是相同的我们不需要分表。

具体配置如下

ini复制代码 #打印shardingsphere sql spring.shardingsphere.props.sql.show=true ## 分库 # 分库信息配置 spring.shardingsphere.datasource.names=test0,test1 #配置数据库信息 #配置连接池 spring.shardingsphere.datasource.test0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test0.jdbc-url=jdbc:mysql://localhost:3306/test0 spring.shardingsphere.datasource.test0.username=root spring.shardingsphere.datasource.test0.password=root #配置连接池 spring.shardingsphere.datasource.test1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test1 spring.shardingsphere.datasource.test1.username=root spring.shardingsphere.datasource.test1.password=root # 配置分片数据库 字段 spring.shardingsphere.sharding.tables.position.database-strategy.inline.sharding-column=id # 配置分片规则 这儿根据id取模2 进行分库 spring.shardingsphere.sharding.tables.position.database-strategy.inline.algorithm-expression=test$->{id % 2} # 配置指定使用雪花算法生成id #对应主键字段名 spring.shardingsphere.sharding.tables.position.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position.key-generator.type=SNOWFLAKE

测试

还是使用上面的测试用例

java复制代码 @org.junit.Test public void insert(){ for (int i = 0; i < 100; i ) { Position position = new Position(); position.setCity("test -> " i); position.setName("name" i%4); position.setSalary(""); positionRepository.save(position); } }

从日志我们可以看到 偶数的存在了 test0库,奇数存在了test1库

分库 垂直分表

在业务中比如我们一张大表字段很多,一些不常用的字段很少使用,而这张表有频繁更新一些常用字段,我们就使用垂直拆分将其拆成领一张表,通过主键关联。在这儿我们演示分库 垂直分表。

需要注意的是:分库中,我们尽量让分库后的表在一个数据库中,这样可以夸库事务,连接的问题,从设计上避免这些问题。

具体配置如下:

还是test0,tes1两个库,分别有表position,position_detail通过pid关联

ini复制代码 #打印shardingsphere sql spring.shardingsphere.props.sql.show=true ## 分库 : 拆表 分库 # 分库信息配置 spring.shardingsphere.datasource.names=test0,test1 #配置数据库信息 #配置连接池 spring.shardingsphere.datasource.test0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test0.jdbc-url=jdbc:mysql://localhost:3306/test0 spring.shardingsphere.datasource.test0.username=root spring.shardingsphere.datasource.test0.password=root #配置连接池 spring.shardingsphere.datasource.test1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test1 spring.shardingsphere.datasource.test1.username=root spring.shardingsphere.datasource.test1.password=root # 配置分片数据库 字段 spring.shardingsphere.sharding.tables.position.database-strategy.inline.sharding-column=id # 配置分片规则 这儿根据id取模2 进行分库分表 spring.shardingsphere.sharding.tables.position.database-strategy.inline.algorithm-expression=test$->{id % 2} # 配置分片数据库 字段 spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.sharding-column=pid # 配置分片规则 这儿根据id取模2 进行分库 这儿使用垂直分库的id进行分库 spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.algorithm-expression=test$->{pid % 2} # 配置指定使用雪花算法生成id #对应主键字段名 spring.shardingsphere.sharding.tables.position.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position.key-generator.type=SNOWFLAKE #对应主键字段名 spring.shardingsphere.sharding.tables.position_detail.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position_detail.key-generator.type=SNOWFLAKE

dao代码

java复制代码public interface PositionDetailRepository extends JpaRepository<PositionDetail,Long> { }

实体代码

java复制代码@Entity @Table(name = "position_detail") public class PositionDetail implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "pid") private long pid; @Column(name = "description") private String description; //set get省略 }

用过测试可以看到

java复制代码 @org.junit.Test public void insert2(){ for (int i = 0; i < 100; i ) { Position position = new Position(); position.setCity("test -> " i); position.setName("name" i%4); position.setSalary(""); positionRepository.save(position); PositionDetail positionDetail = new PositionDetail(); positionDetail.setDescription(" 描述信息 "); positionDetail.setPid(position.getId()); //垂直分表 主键关联 positionDetailRepository.save(positionDetail); } }

可以看到 position的id和 position_detail表的pid是一致的,然后position_detail表的id也是雪花算法生成

查看表可以看到再同库中,垂直分库是成功的。

分库分表

分库解决了IO以及单库的性能问题,分表解决了数据量大的问题,分库分表及解决数量大有解决并发以及IO等性能问题。

先看数据库


我们分两个库test0,test1 然后position表分为四个 分别以_0,_1,_2结尾四个表、test0中全部存偶数,然后再在position3个表中再分表;以position主键 垂直分表到position_detail表中。

test0中偶数,position 0,1,2中偶数再分,position_detail中偶数
test1中奇数,position 0,1,2中奇数再分,position_detail中奇数

这儿不能对4取模因为4和2是一样的。

具体的配置如下

ini复制代码 #打印shardingsphere sql spring.shardingsphere.props.sql.show=true ## 分库 分表 垂直分表 # 分库信息配置 spring.shardingsphere.datasource.names=test0,test1 #配置数据库信息 #配置连接池 spring.shardingsphere.datasource.test0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test0.jdbc-url=jdbc:mysql://localhost:3306/test0 spring.shardingsphere.datasource.test0.username=root spring.shardingsphere.datasource.test0.password=root #配置连接池 spring.shardingsphere.datasource.test1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test1 spring.shardingsphere.datasource.test1.username=root spring.shardingsphere.datasource.test1.password=root # 笛卡尔积 # 分库分表节点 会自动映射 成 成test0 中 test0.position_0,test0.position_1,,test0.position_2, 3张表 # 分库分表节点 会自动映射 成 成test1 中 test1.position_1,test1.position_1,,test1.position_2, 3张表 spring.shardingsphere.sharding.tables.position.actual-data-nodes=test${0..1}.position_${0..3} # 分库 字段 spring.shardingsphere.sharding.tables.position.database-strategy.inline.sharding-column=id # 根据position id % 2 分为两个库 spring.shardingsphere.sharding.tables.position.database-strategy.inline.algorithm-expression=test$->{id % 2} # 分表 字段 spring.shardingsphere.sharding.tables.position.table-strategy.inline.sharding-column=id # 分表 四个表 spring.shardingsphere.sharding.tables.position.table-strategy.inline.algorithm-expression=position_$->{id % 3} # 配置指定使用雪花算法生成id #对应主键字段名 spring.shardingsphere.sharding.tables.position.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position.key-generator.type=SNOWFLAKE # 配置 position_detail 表的分库分表策略 这儿没有分表 spring.shardingsphere.sharding.tables.position_detail.actual-data-nodes=test${0..1}.position_detail ## position_detail 分库策略 根据 position id 垂直分表2个表 spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.sharding-column=pid spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.algorithm-expression=test$->{pid % 2} # position_detail 表 雪花算法id spring.shardingsphere.sharding.tables.position_detail.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.position_detail.key-generator.type=SNOWFLAKE

测试一下可以看到数据

![在这里插入图片描述](img-blog.csdnimg.cn/20210401233…

再看我们的 position_detail 表

test1我们就不看了,数据都是奇数

字典表

字典表又称为广播表,这儿的表不需要分片但是我们所有的数据又要去使用它,所以我们在每个库里都去保存一份,比如我们常用的城市表,配置表等等。

我们的城市表

这儿简单演示一下

Java代码

java复制代码@Entity @Table(name = "city") public class City implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "name") private String name; @Column(name = "province") private String province;

dao层代码

csharp复制代码public interface CityRepository extends JpaRepository<City,Long> { }

配置

ini复制代码 #打印shardingsphere sql spring.shardingsphere.props.sql.show=true ## 广播表 每个库中都存在一份 # 分库信息配置 spring.shardingsphere.datasource.names=test0,test1 #配置数据库信息 #配置连接池 spring.shardingsphere.datasource.test0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test0.jdbc-url=jdbc:mysql://localhost:3306/test0 spring.shardingsphere.datasource.test0.username=root spring.shardingsphere.datasource.test0.password=root #配置连接池 spring.shardingsphere.datasource.test1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.test1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test1 spring.shardingsphere.datasource.test1.username=root spring.shardingsphere.datasource.test1.password=root spring.shardingsphere.sharding.broadcast-tables=city # 表 雪花算法id spring.shardingsphere.sharding.tables.city.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.city.key-generator.type=SNOWFLAKE

测试,我们就不看数据库了 看看日志,同一个id 分别插入test0,test1表中


作者:tizzybepeacejoy
链接:https://juejin.cn/post/7231803859889979452

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved