乐趣区

关于java:数据量大了一定要分表分库分表组件ShardingJDBC入门与项目实战

最近我的项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题。因而想借助一些分库分表的中间件,实现自动化分库分表实现。调研下来,发现 Sharding-JDBC 目前成熟度最高并且利用最广的Java 分库分表的客户端组件。本文次要介绍一些 Sharding-JDBC 外围概念以及生产环境下的实战指南,旨在帮忙组内成员疾速理解 Sharding-JDBC 并且可能疾速将其应用起来。Sharding-JDBC 官网文档

外围概念

在应用 Sharding-JDBC 之前,肯定是先了解分明上面几个外围概念。

逻辑表

程度拆分的数据库(表)的雷同逻辑和数据结构表的总称。例:订单数据依据主键尾数拆分为 10 张表,别离是 t_order_0t_order_9,他们的逻辑表名为t_order

实在表

在分片的数据库中实在存在的物理表。即上个示例中的 t_order_0t_order_9

数据节点

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

绑定表

指分片规定统一的主表和子表。例如:t_order表和 t_order_item 表,均依照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查问不会呈现笛卡尔积关联,关联查问效率将大大晋升。举例说明, 如果 SQL 为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

假如 t_ordert_order_item对应的实在表各有 2 个,那么实在表就有 t_order_0t_order_1t_order_item_0t_order_item_1。在不配置绑定表关系时,假如分片键order_id 将数值 10 路由至第 0 片,将数值 11 路由至第 1 片,那么路由后的 SQL 应该为 4 条,它们出现为笛卡尔积:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在配置绑定表关系后,路由的 SQL 应该为 2 条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

播送表

指所有的分片数据源中都存在的表,表构造和表中的数据在每个数据库中均完全一致。实用于数据量不大且须要与海量数据的表进行关联查问的场景,例如:字典表。

数据分片

分片键

用于分片的数据库字段,是将数据库 (表) 程度拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。SQL 中如果无分片字段,将执行全路由,性能较差。除了对单分片字段的反对,Sharding-JDBC 也反对依据多个字段进行分片。

分片算法

通过分片算法将数据分片,反对通过 =、>=、<=、>、<、BETWEEN 和 IN 分片。分片算法须要利用方开发者自行实现,可实现的灵便度十分高。

目前提供 4 种分片算法。因为分片算法和业务实现严密相干,因而并未提供内置分片算法,而是通过分片策略将各种场景提炼进去,提供更高层级的形象,并提供接口让利用开发者自行实现分片算法。

准确分片算法

对应 PreciseShardingAlgorithm用于解决应用繁多键作为分片键的 = 与 IN 进行分片的场景。须要配合 StandardShardingStrategy 应用。

范畴分片算法

对应 RangeShardingAlgorithm用于解决应用繁多键作为分片键的 BETWEEN AND、>、<、>=、<= 进行分片的场景。须要配合 StandardShardingStrategy 应用。

复合分片算法

对应 ComplexKeysShardingAlgorithm,用于解决应用多键作为分片键进行分片的场景,蕴含多个分片键的逻辑较简单,须要利用开发者自行处理其中的复杂度。须要配合 ComplexShardingStrategy 应用。

Hint 分片算法

对应 HintShardingAlgorithm用于解决通过 Hint 指定分片值而非从 SQL 中提取分片值的场景。须要配合 HintShardingStrategy 应用。

分片策略

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

规范分片策略

对应 StandardShardingStrategy。提供对 SQ L 语句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作反对。StandardShardingStrategy 只反对单分片键,提供 PreciseShardingAlgorithmRangeShardingAlgorithm 两个分片算法。PreciseShardingAlgorithm 是必选的 ,用于解决 = 和 IN 的分片。RangeShardingAlgorithm 是可选的,用于解决 BETWEEN AND, >, <, >=, <= 分片,如果不配置 RangeShardingAlgorithm,SQL 中的 BETWEEN AND 将依照全库路由解决。

复合分片策略

对应 ComplexShardingStrategy。复合分片策略。提供对 SQL 语句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作反对。ComplexShardingStrategy 反对多分片键,因为多分片键之间的关系简单,因而并未进行过多的封装,而是间接将分片键值组合以及分片操作符透传至分片算法,齐全由利用开发者实现,提供最大的灵便度。

行表达式分片策略

对应 InlineShardingStrategy。应用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN的分片操作反对,只反对单分片键。对于简略的分片算法,能够通过简略的配置应用,从而防止繁琐的 Java 代码开发,如: t_user_$->{u_id % 8} 示意 t_user 表依据 u_id 模 8,而分成 8 张表,表名称为 t_user_0t_user_7能够认为是准确分片算法的繁难实现

Hint 分片策略

对应 HintShardingStrategy。通过 Hint 指定分片值而非从 SQL 中提取分片值的形式进行分片的策略。

分布式主键

用于在分布式环境下,生成全局惟一的 id。Sharding-JDBC 提供了内置的分布式主键生成器,例如 UUIDSNOWFLAKE。还抽离出分布式主键生成器的接口,不便用户自行实现自定义的自增主键生成器。为了保障数据库性能,主键 id 还必须趋势递增,防止造成频繁的数据页面决裂。

读写拆散

提供一主多从的读写拆散配置,可独立应用,也可配合分库分表应用。

  • 同一线程且同一数据库连贯内,如有写入操作,当前的读操作均从主库读取,用于保证数据一致性
  • 基于 Hint 的强制主库路由。
  • 主从模型中,事务中读写均用主库。

执行流程

Sharding-JDBC 的原理总结起来很简略: 外围由 SQL 解析 => 执行器优化 => SQL 路由 => SQL 改写 => SQL 执行 => 后果归并 的流程组成。

我的项目实战

spring-boot 我的项目实战

引入依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.1</version>
</dependency>

数据源配置

如果应用sharding-jdbc-spring-boot-starter, 并且数据源以及数据分片都应用 shardingsphere 进行配置,对应的数据源会主动创立并注入到 spring 容器中。

spring.shardingsphere.datasource.names=ds0,ds1

spring.shardingsphere.datasource.ds0.type=org.apache.commons.dbcp.BasicDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=

spring.shardingsphere.datasource.ds1.type=org.apache.commons.dbcp.BasicDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost:3306/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=

# 其它分片配置

然而在咱们已有的我的项目中,数据源配置是独自的。因而要禁用 sharding-jdbc-spring-boot-starter 外面的主动拆卸,而是参考源码本人重写数据源配置 。须要在启动类上加上@SpringBootApplication(exclude = {org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration.class}) 来排除。而后自定义配置类来拆卸DataSource

@Configuration
@Slf4j
@EnableConfigurationProperties({
        SpringBootShardingRuleConfigurationProperties.class,
        SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class})
@AutoConfigureBefore(DataSourceConfiguration.class)
public class DataSourceConfig implements ApplicationContextAware {

    @Autowired
    private SpringBootShardingRuleConfigurationProperties shardingRule;

    @Autowired
    private SpringBootPropertiesConfigurationProperties props;

    private ApplicationContext applicationContext;

    @Bean("shardingDataSource")
    @Conditional(ShardingRuleCondition.class)
    public DataSource shardingDataSource() throws SQLException {
        // 获取其它形式配置的数据源
        Map<String, DruidDataSourceWrapper> beans = applicationContext.getBeansOfType(DruidDataSourceWrapper.class);
        Map<String, DataSource> dataSourceMap = new HashMap<>(4);
        beans.forEach(dataSourceMap::put);
        // 创立 shardingDataSource
        return ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws SQLException {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 将 shardingDataSource 设置到 SqlSessionFactory 中
        sqlSessionFactoryBean.setDataSource(shardingDataSource());
        // 其它设置
        return sqlSessionFactoryBean.getObject();}
}

分布式 id 生成器配置

Sharding-JDBC 提供了 UUIDSNOWFLAKE 生成器,还反对用户实现自定义 id 生成器。比方能够实现了 type 为 SEQ 的分布式 id 生成器,调用对立的 分布式 id 服务 获取 id。

@Data
public class SeqShardingKeyGenerator implements ShardingKeyGenerator {private Properties properties = new Properties();

    @Override
    public String getType() {return "SEQ";}

    @Override
    public synchronized Comparable<?> generateKey() {// 获取分布式 id 逻辑}
}

因为扩大 ShardingKeyGenerator 是通过 JDK 的 serviceloader 的 SPI 机制实现的,因而还须要在 resources/META-INF/services 目录下配置 org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator 文件。 文件内容就是 SeqShardingKeyGenerator 类的全路径名。这样应用的时候,指定分布式主键生成器的 type 为 SEQ 就好了。

至此,Sharding-JDBC 就整合进 spring-boot 我的项目中了,前面就能够进行数据分片相干的配置了。

数据分片实战

如果我的项目初期就能预估出表的数据量级,当然能够一开始就依照这个预估值进行分库分表处理。然而大多数状况下,咱们一开始并不能筹备预估出数量级。这时候通常的做法是:

  1. 线上数据某张表查问性能开始降落,排查下来是因为数据量过大导致的。
  2. 依据历史数据量预估出将来的数据量级,并联合具体业务场景确定分库分表策略。
  3. 主动分库分表代码实现。

上面就以一个具体事例,论述具体数据分片实战。比方有张表数据结构如下:

CREATE TABLE `hc_question_reply_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID',
  `reply_text` varchar(500) NOT NULL DEFAULT ''COMMENT' 回复内容 ',
  `reply_wheel_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '回复工夫',

  `ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创立工夫',
  `mtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新工夫',
  PRIMARY KEY (`id`),
  INDEX `idx_reply_wheel_time` (`reply_wheel_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='回复明细记录';

分片计划确定

先查问目前指标表月新增趋势:

SELECT count(*), date_format(ctime, '%Y-%m') AS ` 日期 `
FROM hc_question_reply_record
GROUP BY date_format(ctime, '%Y-%m');

目前月新增在 180w 左右,预估将来达到 300w(根本以 2 倍计算)以上。冀望单表数据量不超过 1000w,可应用 reply_wheel_time 作为分片键按季度归档。

分片配置

spring:
  # sharing-jdbc 配置
  shardingsphere:
    # 数据源名称
    datasource:
      names: defaultDataSource,slaveDataSource
    sharding:
      # 主从节点配置
      master-slave-rules:
        defaultDataSource:
          # maser 数据源
          master-data-source-name: defaultDataSource
          # slave 数据源
          slave-data-source-names: slaveDataSource
      tables:
        # hc_question_reply_record 分库分表配置
        hc_question_reply_record:
          # 实在数据节点  hc_question_reply_record_2020_q1
          actual-data-nodes: defaultDataSource.hc_question_reply_record_$->{2020..2025}_q$->{1..4}
          # 表分片策略
          table-strategy:
            standard:
              # 分片键
              sharding-column: reply_wheel_time
              # 准确分片算法 全路径名
              preciseAlgorithmClassName: com.xx.QuestionRecordPreciseShardingAlgorithm
              # 范畴分片算法,用于 BETWEEN,可选。。该类需实现 RangeShardingAlgorithm 接口并提供无参数的结构器
              rangeAlgorithmClassName: com.xx.QuestionRecordRangeShardingAlgorithm

      # 默认分布式 id 生成器
      default-key-generator:
        type: SEQ
        column: id

分片算法实现

  • 准确分片算法:QuestionRecordPreciseShardingAlgorithm

     public class QuestionRecordPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
       @Override
       public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {return ShardingUtils.quarterPreciseSharding(availableTargetNames, shardingValue);
       }
  • 范畴分片算法:QuestionRecordRangeShardingAlgorithm

     public class QuestionRecordRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {
       @Override
       public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {return ShardingUtils.quarterRangeSharding(availableTargetNames, shardingValue);
       }
     }
  • 具体分片实现逻辑:ShardingUtils

     @UtilityClass
     public class ShardingUtils {
         public static final String QUARTER_SHARDING_PATTERN = "%s_%d_q%d";
         public Collection<String> quarterRangeSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {// 这里就是依据范畴查问条件,筛选出匹配的实在表汇合}
    
         public static String quarterPreciseSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {// 这里就是依据等值查问条件,计算出匹配的实在表}
     }
    

到这里,针对 hc_question_reply_record 表,应用 reply_wheel_time 作为分片键,依照季度分片的解决就实现了。还有一点要留神的就是,分库分表之后,查问的时候最好都带上分片键作为查问条件 ,否则就会应用全库路由,性能很低。还有就是Sharing-JDBCmysql的全文索引反对的不是很好,我的项目有应用到的中央也要留神一下。总结来说整个过程还是比较简单的,后续碰到其它业务场景,置信大家依照这个思路必定都能解决的。

退出移动版