ShardingJdbc分库分表简单实现及内容整理

 

 

背景

ShardingSphere官网

随着互联网的迅猛发展和业务量的增多,传统的将数据集中存储在单一数据节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足互联网的海量数据场景。

从性能方面来说,由于关系型数据库大多采用B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。

从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。

从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。

一、简介

Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

在这里插入图片描述

1. ShardingSphere-JDBC

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

  • 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  • 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。

在这里插入图片描述

2. ShardingSphere-Proxy

定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前提供 MySQL 和 PostgreSQL 版本,它可以使用任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench, Navicat 等)操作数据,对 DBA 更加友好。

  • 向应用程序完全透明,可直接当做 MySQL/PostgreSQL 使用。
  • 适用于任何兼容 MySQL/PostgreSQL 协议的的客户端。

在这里插入图片描述

3. ShardingSphere-Sidecar(TODO)

定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的啮合层,即 Database Mesh,又可称数据库网格
Database Mesh 的关注重点在于如何将分布式的数据访问应用与数据库有机串联起来,它更加关注的是交互,是将杂乱无章的应用与数据库之间的交互进行有效地梳理。 使用 Database Mesh,访问数据库的应用和数据库终将形成一个巨大的网格体系,应用和数据库只需在网格体系中对号入座即可,它们都是被啮合层所治理的对象。

4. 对比图

在这里插入图片描述

二、垂直拆分和水平拆分

数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。

1. 垂直拆分

按照业务拆分的方式称为垂直拆分,它的核心理念是专库专用。 在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。
在这里插入图片描述

垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。 垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。

2. 水平拆分

水平分片又称为横向拆分。 相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表)。
在这里插入图片描述
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。

三、SQL核心概念

  • 逻辑表: 水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order。
  • 真实表: 在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9。
  • 数据节点: 数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0。
  • 绑定表: 指分片规则一致的主表和子表。例如:t_order 表和 t_order_item 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
  • 广播表: 指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
  • 单表: 指所有的分片数据源中只存在唯一一张的表。适用于数据量不大且不需要做任何分片操作的场景。

四、分片相关概念

1 分片键

用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。

2. 分片算法

通过分片算法将数据分片,支持通过 =、>=、<=、>、<、BETWEEN 和 IN 分片。 分片算法需要应用方开发者自行实现,可实现的灵活度非常高。

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

  • 标准分片算法
    对应 StandardShardingAlgorithm,用于处理使用单一键作为分片键的 =、IN、BETWEEN AND、>、<、>=、<= 进行分片的场景。需要配合 StandardShardingStrategy 使用。
  • 复合分片算法
    对应 ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合 ComplexShardingStrategy 使用。
  • Hint分片算法
    对应 HintShardingAlgorithm,用于处理使用 Hint 行分片的场景。需要配合 HintShardingStrategy 使用。

3. 分片策略

包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供 4 种分片策略。

  • 标准分片策略
    对应 StandardShardingStrategy。提供对 SQL 语句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。 StandardShardingStrategy 只支持单分片键,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。 PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。 RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND, >, <, >=, <= 分片,如果不配置 RangeShardingAlgorithm,SQL 中的 BETWEEN AND 将按照全库路由处理。
  • 符合分片策略
    对应 ComplexShardingStrategy。复合分片策略。提供对 SQL 语句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。 ComplexShardingStrategy 支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
  • Hint分片策略
    对应 HintShardingStrategy。通过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略。
  • 不分片策略
    对应 NoneShardingStrategy。不分片的策略。

五、分布式主键

传统数据库软件开发中,主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持,比如 MySQL 的自增键,Oracle 的自增序列等。 数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。 虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。

  • UUID
    采用 UUID.randomUUID() 的方式产生分布式主键。
  • SNOWFLAKE
    在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法(snowflake)生成 64bit 的长整型数据。
  • 自定义扩展实现
    实现 KeyGenerateAlgorithm 自定义扩展。
    在这里插入图片描述

六、执行流程

ShardingSphere 的 3 个产品的数据分片主要流程是完全一致的。 核心由 SQL 解析 => 执行器优化 => SQL 路由 => SQL 改写 => SQL 执行 => 结果归并的流程组成。

在这里插入图片描述

  • SQL解析
    分为词法解析和语法解析。 先通过词法解析器将 SQL 拆分为一个个不可再分的单词。再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。 解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。
  • 执行器优化
    合并和优化分片条件,如 OR 等。
  • SQL路由
    根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
  • SQL改写
    将 SQL 改写为在真实数据库中可以正确执行的语句。SQL 改写分为正确性改写和优化改写。
  • SQL执行
    通过多线程执行器异步执行。
  • 结果归并
    将多个执行结果集归并以便于通过统一的 JDBC 接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。

七、ShardingJdbc配置

在这里插入图片描述
具体配置说明如下:

spring.shardingsphere.datasource.names= # 数据源名称,多数据源以逗号分隔

spring.shardingsphere.datasource.<data-source-name>.type= # 数据库连接池类名称
spring.shardingsphere.datasource.<data-source-name>.driver-class-name= # 数据库驱动类
名
spring.shardingsphere.datasource.<data-source-name>.url= # 数据库 url 连接
spring.shardingsphere.datasource.<data-source-name>.username= # 数据库用户名
spring.shardingsphere.datasource.<data-source-name>.password= # 数据库密码
spring.shardingsphere.datasource.<data-source-name>.xxx= # 数据库连接池的其它属性

spring.shardingsphere.sharding.tables.<logic-table-name>.actual-data-nodes= # 由数据
源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。缺省表示使用已知数据源与逻辑表
名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且
所有库的表结构完全一致的情况

# 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一

# 用于单分片键的标准分片场景
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.
standard.sharding-column= # 分片列名称
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.
standard.precise-algorithm-class-name= # 精确分片算法类名称,用于 = IN。该类需实现
PreciseShardingAlgorithm 接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.
standard.range-algorithm-class-name= # 范围分片算法类名称,用于 BETWEEN,可选。该类需实现
RangeShardingAlgorithm 接口并提供无参数的构造器

# 用于多分片键的复合分片场景
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.complex.
sharding-columns= # 分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.complex.
algorithm-class-name= # 复合分片算法类名称。该类需实现 ComplexKeysShardingAlgorithm 接口并
提供无参数的构造器

# 行表达式分片策略
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.inline.
sharding-column= # 分片列名称
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.inline.
algorithm-expression= # 分片算法行表达式,需符合 groovy 语法

#Hint 分片策略
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.hint.
algorithm-class-name= #Hint 分片算法类名称。该类需实现 HintShardingAlgorithm 接口并提供无
参数的构造器

# 分表策略,同分库策略
spring.shardingsphere.sharding.tables.<logic-table-name>.table-strategy.xxx= # 省略

spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.column= # 自
增列名称,缺省表示不使用自增主键生成器
spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.type= # 自增
列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:
SNOWFLAKE/UUID
spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.props.
<property-name>= # 属性配置, 注意:使用 SNOWFLAKE 算法,需要配置 worker.id 与 max.
tolerate.time.difference.milliseconds 属性。若使用此算法生成值作分片值,建议配置 max.
vibration.offset 属性

spring.shardingsphere.sharding.binding-tables[0]= # 绑定表规则列表
spring.shardingsphere.sharding.binding-tables[1]= # 绑定表规则列表
spring.shardingsphere.sharding.binding-tables[x]= # 绑定表规则列表
spring.shardingsphere.sharding.broadcast-tables[0]= # 广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[1]= # 广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[x]= # 广播表规则列表

spring.shardingsphere.sharding.default-data-source-name= # 未配置分片规则的表将通过默认
数据源定位
spring.shardingsphere.sharding.default-database-strategy.xxx= # 默认数据库分片策略,同分
库策略
spring.shardingsphere.sharding.default-table-strategy.xxx= # 默认表分片策略,同分表策略
spring.shardingsphere.sharding.default-key-generator.type= # 默认自增列值生成器类型,缺
省将使用 org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。
可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.default-key-generator.props.<property-name>= # 自增列
值生成器属性配置, 比如 SNOWFLAKE 算法的 worker.id 与 max.tolerate.time.difference.
milliseconds

spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
master-data-source-name= # 主数据源名称
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
slave-data-source-names[0]= # 从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
slave-data-source-names[1]= # 从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
slave-data-source-names[x]= # 从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
load-balance-algorithm-class-name= # 从库负载均衡算法类名称。该类需实现
MasterSlaveLoadBalanceAlgorithm 接口且提供无参数构造器
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.
load-balance-algorithm-type= # 从库负载均衡算法类型,可选值:ROUND_ROBIN,RANDOM。若`loadbalance-algorithm-class-name`存在则忽略该配置

spring.shardingsphere.props.sql.show= # 是否开启 SQL 显示,默认值: false
spring.shardingsphere.props.executor.size= # 工作线程数量,默认值: CPU 核数
折叠

八、案例

在同一个数据库中建立三张相同的表test_user0到test_user2,表结构如下:

CREATE TABLE `test_user0` (
  `id` int NOT NULL COMMENT '主键',
  `user_id` int DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(32) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `test_user1` (
  `id` int NOT NULL COMMENT '主键',
  `user_id` int DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(32) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `test_user2` (
  `id` int NOT NULL COMMENT '主键',
  `user_id` int DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(32) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

引入pom依赖

<dependency>
	<groupId>io.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>3.1.0.M1</version>
</dependency>

注释掉之前配置的数据源,使用sharding jdbc datasource

配置sharding jdbc

# 数据源
sharding:
    jdbc:
        datasource:
            names: ds0
            # 数据库
            ds0:
                type: com.zaxxer.hikari.HikariDataSource
                driver-class-name: com.mysql.cj.jdbc.Driver
                jdbc-url: jdbc:mysql://192.168.0.130:3306/starter?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
                username: root
                password: root
        # 配置相关策略
        config:
            sharding:
                tables:
                    test_user:
                        actual-data-nodes: ds0.test_user$->{0..2}
                        table-strategy:
                            standard:
                                precise-algorithm-class-name: com.chenvacloud.erp.uaa.server.controller.CustomRangeShardingAlgorithm
                                sharding-column: user_id
            # 打印执行的数据库
            props:
                sql:
                    show: true

编写 数据库表分片算法类 CustomRangeShardingAlgorithm 需要继承 PreciseShardingAlgorithm 。假设有六条数据,三张表存在。

public class CustomRangeShardingAlgorithm implements PreciseShardingAlgorithm<Integer> {

    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Integer> preciseShardingValue) {
        Double userId = Double.valueOf(preciseShardingValue.getValue());
        // 假设有6条数据 ,三张表
        Double temp = userId / 3;
        String tableName = "test_user" + (int) Math.ceil(temp);
        return tableName;
    }
}

编写新增数据接口,模拟数据新增

@GetMapping("/customer/insert")
public ResultBody insert() {
    for (int x=0;x<6;x++) {
        Map<String, Object> item = new HashMap<>();
        item.put("id", x+1000);
        item.put("userId", x );
        item.put("userName", x + "xxx");


        customerMapper.insertCus(item);
    }

    return ResultBody.ok();
}

对应sql

<insert id="insertCus">
 insert into test_user(id,user_id,user_name) values(#{id},#{userId},#{userName})
</insert>

表中的存储数据如下:

test_user0
在这里插入图片描述
test_user1
在这里插入图片描述
test_user2
在这里插入图片描述
执行的插入sql如下:

2021-04-20 16:10:22.942 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Rule Type: sharding 
2021-04-20 16:10:22.942 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Logic SQL: insert into test_user(id,user_id,user_name) values(?,?,?) 
2021-04-20 16:10:22.942 [XNIO-1 task-1] INFO  ShardingSphere-SQL - SQLStatement: InsertStatement(super=DMLStatement(super=io.shardingsphere.core.parsing.parser.sql.dml.insert.InsertStatement@6091347d), columns=[Column(name=id, tableName=test_user), Column(name=user_id, tableName=test_user), Column(name=user_name, tableName=test_user)], generatedKeyConditions=[], insertValues=InsertValues(insertValues=[InsertValue(type=VALUES, expression=(?,?,?), parametersCount=3)]), columnsListLastPosition=42, generateKeyColumnIndex=-1, insertValuesListLastPosition=57) 
2021-04-20 16:10:22.943 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: insert into test_user0(id,user_id,user_name) values(?,?,?) ::: [[1000, 0, 0xxx]] 

2021-04-20 16:10:23.075 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Rule Type: sharding 
2021-04-20 16:10:23.075 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Logic SQL: insert into test_user(id,user_id,user_name) values(?,?,?) 
2021-04-20 16:10:23.075 [XNIO-1 task-1] INFO  ShardingSphere-SQL - SQLStatement: InsertStatement(super=DMLStatement(super=io.shardingsphere.core.parsing.parser.sql.dml.insert.InsertStatement@6091347d), columns=[Column(name=id, tableName=test_user), Column(name=user_id, tableName=test_user), Column(name=user_name, tableName=test_user)], generatedKeyConditions=[], insertValues=InsertValues(insertValues=[InsertValue(type=VALUES, expression=(?,?,?), parametersCount=3)]), columnsListLastPosition=42, generateKeyColumnIndex=-1, insertValuesListLastPosition=57) 
2021-04-20 16:10:23.075 [XNIO-1 task-1] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: insert into test_user1(id,user_id,user_name) values(?,?,?) ::: [[1001, 1, 1xxx]] 

编写查询数据接口

@GetMapping("/customer/list")
 public ResultBody list() {

     List<Map<String, Object>> list = customerMapper.findList();

     return ResultBody.ok().data(list);
 }

对应sql

<select id="findList" resultType="java.util.Map">
  select id,user_id as userId,user_name userName from test_user where 1=1
</select>

查询结果如下:
在这里插入图片描述

对应sql如下:

2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - Rule Type: sharding 
2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - Logic SQL: select * from test_user where 1=1 
2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - SQLStatement: SelectStatement(super=DQLStatement(super=io.shardingsphere.core.parsing.parser.sql.dql.select.SelectStatement@4fbf233f), containStar=true, selectListLastPosition=9, groupByLastPosition=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subQueryStatement=null, subQueryStatements=[]) 
2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: select * from test_user0 where 1=1 
2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: select * from test_user1 where 1=1 
2021-04-20 16:17:45.709 [XNIO-1 task-2] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: select * from test_user2 where 1=1 
充满鲜花的世界到底在哪里
posted @ 2022-07-18 13:03  满Sir  阅读(748)  评论(0编辑  收藏  举报