shardingsphere-jdbc各版本分表组件使用

shardingsphere-jdbc各版本分表组件使用,第1张

shardingsphere-jdbc各版本分表组件使用

目录

一、背景

问题

垂直分片

水平分片

二、定义

三、快速入门

1.引入maven依赖

2.规则配置

3.创建数据源

四、核心概念

1 表

2 数据节点

3 分片

4 流程

5 行表达式

五、2.0.3版本使用配置

1 数据准备

2 规则配置

3 创建数据源

六、4.1.1版本使用配置

1 数据准备

2 规则配置

3 创建数据源 

七、5.0.0版本使用配置

1 数据准备

2 规则配置

3 创建数据源

4 注意

八、测试


 

一、背景
  1. 问题

    传统的将数据集中存储至单一节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足海量数据的场景。通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。 数据分片的拆分方式又分为垂直分片和水平分片。

  2. 垂直分片

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

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

  3. 水平分片

    水平分片又称为横向拆分。 相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。本文配置了水平分片。

    水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是数据分片的标准解决方案。

二、定义

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

  1. 适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  2. 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  3. 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
三、快速入门 1.引入maven依赖

注意:请将 ${latest.release.version} 更改为实际的版本号。


    org.apache.shardingsphere
    shardingsphere-jdbc-core
    ${latest.release.version}
2.规则配置

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

Java API 是 ShardingSphere-JDBC 中所有配置方式的基础,其他配置最终都将转化成为 Java API 的配置方式。

本文使用的是Java API。

Java API 是最繁琐也是最灵活的配置方式,适合需要通过编程进行动态配置的场景下使用。

3.创建数据源

通过 ShardingSphereDataSourceFactory 工厂和规则配置对象获取 ShardingSphereDataSource。 该对象实现自 JDBC 的标准 DataSource 接口,可用于原生 JDBC 开发,或使用 JPA, Hibernate, MyBatis 等 ORM 类库。

DataSource dataSource = ShardingSphereDataSourceFactory.createDataSource(schemaName, modeConfig, dataSourceMap, ruleConfigs, props);
四、核心概念 1 表
  1. 逻辑表(LogicTable):进行水平拆分的时候同一类型(逻辑、数据结构相同)的表的总称。例:test日志表据根据更新时间updateTime按年拆分为 10 张表,分别是test2020到 test2029,他们的逻辑表名为 test。
  2. 真实表(ActualTable):在分片的数据库中真实存在的物理表。 即上个示例中的 test2020到 test2029。
  3. 绑定表(BindingTable):指分片规则一致的主表和子表。使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。
  4. 广播表:指所有的分片数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表、省市区表。
  5. 单表:指所有的分片数据源中仅唯一存在的表。 适用于数据量不大且无需分片的表。
2 数据节点

数据分片的最小单元,由数据源名称和真实表组成。 例:ds_0.test2020。

3 分片
  1. 分片键(ShardingColumn):分片字段用于将数据库(表)水平拆分的字段,支持单字段及多字段分片。例:将日志表中的更新时间updateTime的按年分片,则更新时间为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。
  2. 分片算法(ShardingAlgorithm):进行水平拆分时采用的算法,分片算法需要应用方开发者自行实现,可实现的灵活度非常高。目前提供4种分片算法。由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
    1. 精确分片算法(PreciseSharding):必选,用于处理使用单一键作为分片键的=与IN进行分片的场景。
    2. 范围分片算法(RangeSharding):可选,用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
    3. 复合分片算法(ComplexKeysSharding):用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。
    4. Hint分片算法(HintSharding):用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
  3. 分片策略(ShardingStrategy):包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片 *** 作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
    1. 标准分片策略(StandardShardingStrategy):提供对SQL语句中的=, IN和BETWEEN AND的分片 *** 作支持。StandardShardingStrategy只支持单分片键,提供PreciseSharding和RangeSharding两个分片算法。PreciseSharding是必选的,用于处理=和IN的分片。RangeSharding是可选的,用于处理BETWEEN AND分片,如果不配置RangeSharding,SQL中的BETWEEN AND将按照全库路由处理。
    2. 复合分片策略(ComplexShardingStrategy):提供对SQL语句中的=, IN和BETWEEN AND的分片 *** 作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片 *** 作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
    3. Hint分片策略(HintShardingStrategy):通过Hint而非SQL解析的方式分片的策略。
    4. 行表达式分片策略(InlineShardingStrategy):使用Groovy的表达式,提供对SQL语句中的=和IN的分片 *** 作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。
    5. 不分片策略(NoneShardingStrategy):不分片的策略。
4 流程
  1. SQL解析:分为词法解析和语法解析。 先通过词法解析器将 SQL 拆分为一个个不可再分的单词。再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。 解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。
  2. SQL改写:将 SQL 改写为在真实数据库中可以正确执行的语句。SQL 改写分为正确性改写和优化改写。
  3. SQL路由:根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
  4. SQL执行:通过多线程执行器异步执行。
  5. 结果归并:将多个执行结果集归并以便于通过统一的 JDBC 接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。

 

5 行表达式

行表达式:本质上是一段 Groovy 代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。配置的简化与一体化是行表达式所希望解决的两个主要问题。

  1. 语法:使用 ${ expression } 或 $->{ expression } 标识行表达式。${begin..end} 表示范围区间;${[unit1, unit2, unit_x]} 表示枚举值。
  2. 应用
    数据节点
原始数据结构
ds0
  ├── test2020
  └── test2021
ds1
  ├── test2022
  └── test2023
  
转换为行表达式
ds0.test202${0..1},ds1.test202${2..3}

 分片算法:对于只有一个分片键的使用 = 和 IN 进行分片的 SQL,可以使用行表达式代替编码方式配置。

分为 10 个库,尾数为 0 的路由到后缀为 0 的数据源, 尾数为 1 的路由到后缀为 1 的数据源,以此类推。
ds${id % 10}
五、2.0.3版本使用配置 1 数据准备

提前在MySQL中建好表test2020、test2021、test2022等,规则:将updateTime定为分表列,时间戳转为年后分表。

2 规则配置

自定义分表算法,在其中的doSharding方法中确定分表规则。

1.标准分片策略StandardShardingStrategy

// 精确分表UpdateTimePreciseShardingAlgorithm
public class UpdateTimePreciseShardingAlgorithm implements PreciseShardingAlgorithm {
    @Override
    public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
      // 自定义分表规则...       
      return "分表后表名";
    }
}

// 范围分表UpdateTimeRangeShardingAlgorithm
public class UpdateTimeRangeShardingAlgorithm implements RangeShardingAlgorithm {
    @Override
    public Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) {
        // 根据范围自定义分表,分表后表名们
        return new ArrayList();
    }    
}

 2.复合分片策略ComplexShardingStrategy

public final class ComplexModuloTableShardingAlgorithm implements ComplexKeysShardingAlgorithm {

    @Override
    public Collection doSharding(Collection availableTargetNames, Collection shardingValues) {
        // 复合分片的规则,分表后表名们
        return new ArrayList();
    }
}

 3.Hint强制分片策略HintShardingStrategy

public final class HintModuloTableShardingAlgorithm implements HintShardingAlgorithm {

    @Override
    public Collection doSharding(Collection availableTargetNames, ShardingValue shardingValue) {
        // Hint分片的规则,分表后表名们
        return new ArrayList();
    }
}
3 创建数据源

自定义分表键关联数据源,此处以标准分片策略和行表达式为例。

1.标准分片策略

public class MySqlConfiguration {  
    
  public ShardingDataSource shardingDataSource(DataSource dataSource) throws SQLException {
      // 自定义逻辑节点和分表键
      TableRuleConfiguration testTableRuleConfig = new TableRuleConfiguration();
      testTableRuleConfig.setLogicTable("test");
      testTableRuleConfig.setKeyGeneratorColumnName("updateTime");
      
      ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
      shardingRuleConfig.getTableRuleConfigs().add(testTableRuleConfig);
      shardingRuleConfig.getBindingTableGroups().add("test");
      // 自定义分表策略PreciseModuloTableShardingAlgorithm和RangeModuloTableShardingAlgorithm
      shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("updateTime", UpdateTimePreciseShardingAlgorithm.class.getName(), UpdateTimeRangeShardingAlgorithm.class.getName()));
      Map result = new HashMap<>(2);
      result.put("ShardingDataSource", dataSource);
      
      return new ShardingDataSource(shardingRuleConfig.build(result));
  }    
}

 2.行表达式

public class MySqlConfiguration {  
      
  public ShardingDataSource shardingDataSource(DataSource dataSource) throws SQLException {
      // 自定义逻辑节点和分表键
      TableRuleConfiguration testTableRuleConfig = new TableRuleConfiguration();
      testTableRuleConfig.setLogicTable("test");
      testTableRuleConfig.setKeyGeneratorColumnName("id");
      
      ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
      shardingRuleConfig.getTableRuleConfigs().add(testTableRuleConfig);
      shardingRuleConfig.getBindingTableGroups().add("test");
      // 行表达式分表策略
      shardingRuleConfig.setDefaultTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "test${id % 2}"));
      
      Map result = new HashMap<>(2);
      result.put("ShardingDataSource", dataSource);
      
      return new ShardingDataSource(shardingRuleConfig.build(result));
  }    
}
六、4.1.1版本使用配置 1 数据准备

提前在MySQL中建好表test2020、test2021、test2022等,规则:将updateTime定为分表列,时间戳转为年后分表。

2 规则配置

自定义分表算法,在其中的doSharding方法中确定分表规则。

1.标准分片策略StandardShardingStrategy

// 精确分表UpdateTimePreciseShardingAlgorithm
public class UpdateTimePreciseShardingAlgorithm implements PreciseShardingAlgorithm {
    @Override
    public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
      // 自定义分表规则...       
      return "分表后表名";
    }
}

// 范围分表UpdateTimeRangeShardingAlgorithm
public class UpdateTimeRangeShardingAlgorithm implements RangeShardingAlgorithm {
    @Override
    public Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) {
        // 根据范围自定义分表...      
        return "分表后表名们";
    }    
}

 2.复合分片策略ComplexShardingStrategy

public class UpdateTimeComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {

    @Override
    public Collection doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        // 复合分片的规则,分表后表名们
        return new ArrayList();
    }
}

 3.Hint强制分片策略HintShardingStrategy

public class UpdateTimeHintShardingAlgorithm implements HintShardingAlgorithm {

    @Override
    public Collection doSharding(Collection availableTargetNames, HintShardingValue shardingValue) {
        // Hint分片的规则,分表后表名们
        return new ArrayList();
    }
}
3 创建数据源 

自定义分表键和数据源,此处以标准分片策略和行表达式为例

1.标准分片策略

public class MysqlConfiguration {    

    public DataSource shardingMysqlDataSource(DataSource dataSource) {
        // 自定义分表策略TimePreciseShardingAlgorithm和TimeRangeShardingAlgorithm,分片键updateTime
        ShardingStrategyConfiguration strategyConfiguration = new StandardShardingStrategyConfiguration("updateTime",
                new UpdateTimePreciseShardingAlgorithm(), new UpdateTimeRangeShardingAlgorithm());
        // 设定逻辑节点和真实节点
        ShardingTableRuleConfiguration testTableRule = new ShardingTableRuleConfiguration("test", "mysql.test202${2..9}");
        // 给节点添加分片规则
        testTableRule.setTableShardingStrategy(strategyConfiguration);

        // Sharding全局配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(testTableRule);

        Map dataSourceMap = new HashMap<>(1);
        dataSourceMap.put("mysql", dataSource);
        Properties properties = new Properties();
        properties.setProperty("sql.show", "false");
        // 创建数据源
        DataSource dataSource = null;
        try {
            dataSource = ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), properties);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dataSource;
    }  
}

 2.行表达式

public class MysqlConfiguration { 

    public DataSource shardingMysqlDataSource(DataSource dataSource) {        
        // 设定逻辑节点和真实节点
        ShardingTableRuleConfiguration testTableRule = new ShardingTableRuleConfiguration("test", "mysql.test202${2..9}");
        // 给节点添加分片规则
        alarmTableRule.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "test${id % 2}"));

        // Sharding全局配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(testTableRule);

        Map dataSourceMap = new HashMap<>(1);
        dataSourceMap.put("mysql", dataSource);
        Properties properties = new Properties();
        properties.setProperty("sql.show", "false");
        // 创建数据源
        DataSource dataSource = null;
        try {
            dataSource = ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), properties);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dataSource;
    }  
}
七、5.0.0版本使用配置 1 数据准备

提前在clickhouse中建表test2020、test2021、test2022,以updateTime为分表列。

2 规则配置

自定义分表算法,在其中的doSharding方法中确定分表规则。

1.标准分片策略StandardShardingStrategy

@Slf4j
public class UpdateTimePreciseShardingAlgorithm implements StandardShardingAlgorithm {    

    // 精确分表PreciseShardingValue
    @Override
    public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
        // 精确分表的规则
        return "分表后表名";
    }

    // 范围分表RangeShardingValue
    @Override
    public Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) {
        // 范围分表的规则,分表后表名们
        return new ArrayList();
    }

    @Override
    public void init() {
        log.info("初始化标准分片");
    }
    
    // 此处的StandardShardingStrategy需要和数据源中的new ShardingSphereAlgorithmConfiguration("StandardShardingStrategy", new Properties()))保持一致
    @Override
    public String getType() {
        return "StandardShardingStrategy";
    }
}

 2.复合分片策略ComplexShardingStrategy

@Slf4j
public class UpdateTimeComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {

    @Override
    public Collection doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        // 复合分片的规则,分表后表名们
        return new ArrayList();
    }

    @Override
    public void init() {
        log.info("初始化复合分片");
    }

    // 此处的ComplexShardingStrategy需要和数据源中的new ShardingSphereAlgorithmConfiguration("ComplexShardingStrategy", new Properties()))保持一致
    @Override
    public String getType() {
        return "ComplexShardingStrategy";
    }
}

 3.Hint分片策略HintShardingStrategy

@Slf4j
public class UpdateTimeHintShardingAlgorithm implements HintShardingAlgorithm {

    @Override
    public void init() {
        log.info("初始化Hint分片");
    }

    // 此处的HintShardingStrategy需要和数据源中的new ShardingSphereAlgorithmConfiguration("HintShardingStrategy", new Properties()))保持一致
    @Override
    public String getType() {
        return "HintShardingStrategy";
    }

    @Override
    public Collection doSharding(Collection availableTargetNames, HintShardingValue shardingValue) {
        // Hint分片的规则,分表后表名们
        return new ArrayList();
    }
}
3 创建数据源

自定义分表键和数据源,此处以配置标准分片策略和行表达式示例.

1.标准分片策略

public class ClickHouseConfiguration {

    public DataSource shardingClickHouseDataSource(DataSource clickHouseDataSource) {
        // 分表策略
        StandardShardingStrategyConfiguration strategyConfiguration = new StandardShardingStrategyConfiguration("updateTime", "tableShardingAlgorithm");
        // 配置 device 表规则
        ShardingTableRuleConfiguration testTableRule = new ShardingTableRuleConfiguration("test", "clickHouse.test202${0..9}");
        testTableRule .setTableShardingStrategy(strategyConfiguration);       
        // Sharding全局配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(testTableRule );
        // 配置分表算法 设置按月分表
        shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("StandardShardingStrategy", new Properties()));
        Map dataSourceMap = new HashMap<>(1);
        dataSourceMap.put("clickHouse", clickHouseDataSource);
        Properties properties = new Properties();
        properties.setProperty("sql.show", "false");
        // 创建数据源
        DataSource dataSource = null;
        try {
            dataSource = ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), properties);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dataSource;

    }

}

 2.行表达式

public class ClickHouseConfiguration {
   
    public DataSource shardingClickHouseDataSource(DataSource clickHouseDataSource) {
        // 分表策略
        StandardShardingStrategyConfiguration strategyConfiguration = new StandardShardingStrategyConfiguration("id", "tableShardingAlgorithm");
        // 配置 device 表规则
        ShardingTableRuleConfiguration testTableRule = new ShardingTableRuleConfiguration("test", "clickHouse.test202${0..9}");
        testTableRule .setTableShardingStrategy(strategyConfiguration);       
        // Sharding全局配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(testTableRule );
        
        // 配置行表达式分片
        Properties dbShardingAlgorithmrProps = new Properties();
        dbShardingAlgorithmrProps.setProperty("algorithm-expression", "test${id % 2}");
        shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", dbShardingAlgorithmrProps));
        
        Map dataSourceMap = new HashMap<>(1);
        dataSourceMap.put("clickHouse", clickHouseDataSource);
        Properties properties = new Properties();
        properties.setProperty("sql.show", "false");
        // 创建数据源
        DataSource dataSource = null;
        try {
            dataSource = ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), properties);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dataSource;

    }

}
4 注意

若自定义分片策略,由于5.0在创建分片规则new StandardShardingStrategyConfiguration时,不能直接指定自定义分片类,需要在配置文件中指定

在resourcesmeta-INFservices新建文件org.apache.shardingsphere.sharding.spi.ShardingAlgorithm,指定自定义分片类

com.main.UpdateTimePreciseShardingAlgorithm
八、测试

1.Mapper.java

@Repository
public interface test {
    void insertTest(Map map);
    List> getListTest(Map map);
}

2.Mapper.xml




  
      insert into test (
      id,
      updateTime
      )
      values (
      #{id},
      #{updateTime}
      )
  

  


3校验插入

@GetMapping(value = "/TestInsertOrder")
public void TestInsertOrder(){
    List> orderList = new ArrayList<>();
    Map map = new HashMap<>();
    map.put("id",1);
    map.put("updateTime",1577808000);
    orderList.add(map);
    map = new HashMap<>();
    map.put("id",2);
    map.put("updateTime",1609430400);
    orderList.add(map);

    for(Map maptmp:orderList){
        netMapper.insertTest(maptmp);
    }
}

4.校验查询

@GetMapping(value = "/TestListOrder")
public void TestListOrder(){
    Map map = new HashMap<>();
    map.put("updateTime",1577808000);
    List> orderList= netMapper.getListTest(map);
    for (Map o:orderList){
        System.out.println(o.toString());
    }
} 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5680640.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存