最近项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题。因此想借助一些分库分表的中间件,实现自动化分库分表实现。调研下来,发现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 种分片算法。由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。

搜索公纵号:MarkerHub,关注回复[ vue ]获取前后端入门教程!

精确分片算法

对应 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,ds1spring.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 {@Autowiredprivate SpringBootShardingRuleConfigurationProperties shardingRule;@Autowiredprivate 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);// 创建shardingDataSourcereturn ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());}@Beanpublic 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();@Overridepublic String getType() {return "SEQ";}@Overridepublic 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_ciCOMMENT='回复明细记录';

分片方案确定

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

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

最近项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题。因此想借助一些分库分表的中间件,实现自动化分库分表实现。调研下来,发现Sharding-JDBC目前成熟度最高并且应用最广的Java分库分表的客户端组件。本文主要介绍一些 Sharding-JDBC 核心概念以及生产环境下的实战指南,旨在帮助组内成员快速了解 Sharding-JDBC 并且能够快速将其使用起来。Sharding-JDBC 官方文档:https://shardingsphere.apache.org/document/current/cn/overview/

核心概念

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

搜索公纵号:MarkerHub,关注回复[ vue ]获取前后端入门教程!

精确分片算法

对应 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,ds1spring.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 {@Autowiredprivate SpringBootShardingRuleConfigurationProperties shardingRule;@Autowiredprivate 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);// 创建shardingDataSourcereturn ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());}@Beanpublic 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();@Overridepublic String getType() {return "SEQ";}@Overridepublic 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_ciCOMMENT='回复明细记录';

分片方案确定

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

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,slaveDataSourcesharding:# 主从节点配置master-slave-rules:defaultDataSource:# maser数据源master-data-source-name: defaultDataSource# slave数据源slave-data-source-names: slaveDataSourcetables:# hc_question_reply_record 分库分表配置hc_question_reply_record:# 真实数据节点  hc_question_reply_record_2020_q1actual-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: SEQcolumn: id

分片算法实现

  • 精确分片算法:QuestionRecordPreciseShardingAlgorithm
public class QuestionRecordPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {/*** Sharding.** @param availableTargetNames available data sources or tables's names* @param shardingValue        sharding value* @return sharding result for data source or table's name*/@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {return ShardingUtils.quarterPreciseSharding(availableTargetNames, shardingValue);}
}

  • 范围分片算法:QuestionRecordRangeShardingAlgorithm
public class QuestionRecordRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {/*** Sharding.** @param availableTargetNames available data sources or tables's names* @param shardingValue        sharding value* @return sharding results for data sources or tables's names*/@Overridepublic 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";/*** logicTableName_{year}_q{quarter}* 按季度范围分片* @param availableTargetNames 可用的真实表集合* @param shardingValue 分片值* @return*/public Collection<String> quarterRangeSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {Range<Date> valueRange = shardingValue.getValueRange();Date lower = valueRange.lowerEndpoint();Date upper = valueRange.upperEndpoint();LocalDate lowerLocalDate =lower.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();LocalDate upperLocalDate =upper.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();int lowerYear = lowerLocalDate.getYear();int lowerMonth = lowerLocalDate.getMonthValue();int lowerQuarter = getQuarter(lowerMonth);int upperYear = upperLocalDate.getYear();int upperMonth = upperLocalDate.getMonthValue();int upperQuarter = getQuarter(upperMonth);String logicTableName = shardingValue.getLogicTableName();List<String> actualTableNames = new ArrayList<>();if (lowerYear == upperYear) {// 不跨年for (int quarter = lowerQuarter; quarter <= upperQuarter; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);actualTableNames.add(actualTableName);}} else {// 跨年for (int quarter = lowerQuarter; quarter <= Constant.INT_4; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);actualTableNames.add(actualTableName);}for (int year = lowerYear + 1; year < upperYear; year++) {for (int quarter = 1; quarter <= Constant.INT_4; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);actualTableNames.add(actualTableName);}}for (int quarter = 1; quarter <= upperQuarter; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, upperYear, quarter);actualTableNames.add(actualTableName);}}if (!availableTargetNames.containsAll(actualTableNames)) {throw new IllegalArgumentException("分片异常!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);}return actualTableNames;}/*** logicTableName_{year}_q{quarter}* 按季度精确分片* @param availableTargetNames 可用的真实表集合* @param shardingValue 分片值* @return*/public static String quarterPreciseSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {String logicTableName = shardingValue.getLogicTableName();Date value = shardingValue.getValue();LocalDate localDate =value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();int year = localDate.getYear();int month = localDate.getMonthValue();int quarter = getQuarter(month);String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);if (availableTargetNames.contains(actualTableName)) {return actualTableName;}throw new IllegalArgumentException("分片异常!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);}private static int getQuarter(int month) {if (month >= Constant.INT_1 && month <= Constant.INT_3) {return Constant.INT_1;} else if (month >= Constant.INT_4 && month <= Constant.INT_6) {return Constant.INT_2;} else if (month >= Constant.INT_7 && month <= Constant.INT_9) {return Constant.INT_3;} else if (month >= Constant.INT_10 && month <= Constant.INT_12) {return Constant.INT_4;} else {throw new IllegalArgumentException("month非法!month=" + month);}}
}

到这里,针对hc_question_reply_record表,使用reply_wheel_time作为分片键,按照季度分片的处理就完成了。还有一点要注意的就是,分库分表之后,查询的时候最好都带上分片键作为查询条件,否则就会使用全库路由,性能很低。还有就是 Sharing-JDBC 对 mysql 的全文索引支持的不是很好,项目有使用到的地方也要注意一下。总结来说整个过程还是比较简单的,后续碰到其它业务场景,相信大家按照这个思路肯定都能解决的。

目前月新增在 180w 左右,预估未来达到 300w(基本以 2 倍计算) 以上。期望单表数据量不超过 1000w,可使用

reply_wheel_time

作为分片键按季度归档。

分片配置

spring:# sharing-jdbc配置shardingsphere:# 数据源名称datasource:names: defaultDataSource,slaveDataSourcesharding:# 主从节点配置master-slave-rules:defaultDataSource:# maser数据源master-data-source-name: defaultDataSource# slave数据源slave-data-source-names: slaveDataSourcetables:# hc_question_reply_record 分库分表配置hc_question_reply_record:# 真实数据节点  hc_question_reply_record_2020_q1actual-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: SEQcolumn: id

分片算法实现

  • 精确分片算法:QuestionRecordPreciseShardingAlgorithm
public class QuestionRecordPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {/*** Sharding.** @param availableTargetNames available data sources or tables's names* @param shardingValue        sharding value* @return sharding result for data source or table's name*/@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {return ShardingUtils.quarterPreciseSharding(availableTargetNames, shardingValue);}
}

  • 范围分片算法:QuestionRecordRangeShardingAlgorithm
public class QuestionRecordRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {/*** Sharding.** @param availableTargetNames available data sources or tables's names* @param shardingValue        sharding value* @return sharding results for data sources or tables's names*/@Overridepublic 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";/*** logicTableName_{year}_q{quarter}* 按季度范围分片* @param availableTargetNames 可用的真实表集合* @param shardingValue 分片值* @return*/public Collection<String> quarterRangeSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {Range<Date> valueRange = shardingValue.getValueRange();Date lower = valueRange.lowerEndpoint();Date upper = valueRange.upperEndpoint();LocalDate lowerLocalDate =lower.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();LocalDate upperLocalDate =upper.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();int lowerYear = lowerLocalDate.getYear();int lowerMonth = lowerLocalDate.getMonthValue();int lowerQuarter = getQuarter(lowerMonth);int upperYear = upperLocalDate.getYear();int upperMonth = upperLocalDate.getMonthValue();int upperQuarter = getQuarter(upperMonth);String logicTableName = shardingValue.getLogicTableName();List<String> actualTableNames = new ArrayList<>();if (lowerYear == upperYear) {// 不跨年for (int quarter = lowerQuarter; quarter <= upperQuarter; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);actualTableNames.add(actualTableName);}} else {// 跨年for (int quarter = lowerQuarter; quarter <= Constant.INT_4; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);actualTableNames.add(actualTableName);}for (int year = lowerYear + 1; year < upperYear; year++) {for (int quarter = 1; quarter <= Constant.INT_4; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);actualTableNames.add(actualTableName);}}for (int quarter = 1; quarter <= upperQuarter; quarter++) {String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, upperYear, quarter);actualTableNames.add(actualTableName);}}if (!availableTargetNames.containsAll(actualTableNames)) {throw new IllegalArgumentException("分片异常!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);}return actualTableNames;}/*** logicTableName_{year}_q{quarter}* 按季度精确分片* @param availableTargetNames 可用的真实表集合* @param shardingValue 分片值* @return*/public static String quarterPreciseSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {String logicTableName = shardingValue.getLogicTableName();Date value = shardingValue.getValue();LocalDate localDate =value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();int year = localDate.getYear();int month = localDate.getMonthValue();int quarter = getQuarter(month);String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);if (availableTargetNames.contains(actualTableName)) {return actualTableName;}throw new IllegalArgumentException("分片异常!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);}private static int getQuarter(int month) {if (month >= Constant.INT_1 && month <= Constant.INT_3) {return Constant.INT_1;} else if (month >= Constant.INT_4 && month <= Constant.INT_6) {return Constant.INT_2;} else if (month >= Constant.INT_7 && month <= Constant.INT_9) {return Constant.INT_3;} else if (month >= Constant.INT_10 && month <= Constant.INT_12) {return Constant.INT_4;} else {throw new IllegalArgumentException("month非法!month=" + month);}}
}

到这里,针对hc_question_reply_record表,使用reply_wheel_time作为分片键,按照季度分片的处理就完成了。还有一点要注意的就是,分库分表之后,查询的时候最好都带上分片键作为查询条件,否则就会使用全库路由,性能很低。还有就是 Sharing-JDBC 对 mysql 的全文索引支持的不是很好,项目有使用到的地方也要注意一下。总结来说整个过程还是比较简单的,后续碰到其它业务场景,相信大家按照这个思路肯定都能解决的。

sharding分表后主键_分库分表【Sharding-JDBC】入门与项目实战相关推荐

  1. python从入门到项目实战李兴华网盘_贺胜军Python轻松入门到项目实战【经典完整版】...

    贺胜军Python轻松入门到项目实战课程目录 01_Python基本概述 01_计算机组成_操作系统.avi 02_计算机的进制.avi 03_数据存储单位1.avi 04_编码和解码.avi 05_ ...

  2. 分库分表之_分库分表 + 复杂查询

    前言 Github:https://github.com/HealerJean 博客:http://blog.healerjean.com 代码配置暂时和和分库分表之_分库分表相同.但是为了测试下面的 ...

  3. java hash 分库分表_分库分表方案

    摘自:Java技术栈 一.数据库瓶颈 不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值. 在业务Service来看就是,可用数据库连接少 ...

  4. mysql 分表 好处_分库分表浅谈

    什么是分库分表 ​顾名思义,分库分表就是按照一定的规则,对原有的数据库和表进行拆分,把原本存储于一个库的数据分块存储到多个库上,把原本存储于一个表的数据分块存储到多个表上. 为什么需要分库分表 ​随着 ...

  5. 数据量大了一定要分表,分库分表 Sharding-JDBC 入门与项目实战

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:juejin.im/post/684490418236581 ...

  6. mysql分片库分页查询_Mysql分库分表

    一.数据库瓶颈 不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值.在业务Service来看就是,可用数据库连接少甚至无连接可用.接下来就 ...

  7. 数据量大了一定要分表,分库分表Sharding-JDBC入门与项目实战

    作者:六点半起床 juejin.im/post/6844904182365814797 最近项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题.因此想借助一些分库分表的中间件,实现自动化分库 ...

  8. 什么是分库分表?为什么需要分表?什么时候分库分表

    不急于上手实战 ShardingSphere 框架,先来复习下分库分表的基础概念,技术名词大多晦涩难懂,不要死记硬背理解最重要,当你捅破那层窗户纸,发现其实它也就那么回事. 什么是分库分表 分库分表是 ...

  9. 如何分库分表,怎样分库分表,为什么要分库分表?

    如何分库分表,怎样分库分表,为什么要分库分表? ◆ 数据库瓶颈 ◆分库分表 1. 水平分库 2. 水平分表 3. 垂直分库 4. 垂直分表 ◆ 分库分表工具 ◆ 分库分表带来的问题 ■ 事务一致性问题 ...

最新文章

  1. SQL老司机,在SQL中计算 array map json数据
  2. Functional Language
  3. Java程序员从阿里、京东、美团面试回来,这些面试题你会吗?
  4. html搜索结果 重置,搜索结果和后退按钮/ HTML表格
  5. 【论文阅读】A Gentle Introduction to Graph Neural Networks [图神经网络入门](6)
  6. SSH (Secure Shell)详解
  7. 基于java银行ATM管理系统设计(含源文件)
  8. Confluence 6 CSS 指南:修改顶部背景
  9. c++ 模板类实现堆栈实验报告_5分钟学会C/C++多线程编程进程和线程
  10. 【服务器】【个人网盘】宝塔安装NextCloud
  11. 【BZOJ】3495: PA2010 Riddle
  12. 台式机linux系统安装教程,台式机Linux/Unix多系统安装详细教程
  13. 干货分享|Contrast essay写作步骤分析
  14. 多方向如何选择数据分析之营业额不一定可以决定方向
  15. excel学习-文本函数(left+right+len)
  16. 两台win10电脑网线直连,使用Synergy实现鼠标键盘共享设置
  17. windows客户端开发--如何测量一个字符串显示的物理长度
  18. 80老翁谈人生(40):加快步伐,坚定信念,继续前行
  19. 微信公众号 创建菜单post数据格式
  20. “求答案?一筐鸡蛋:几个几个拿”的Python解答

热门文章

  1. 算法高级(10)-如何实现浏览器的前进、后退功能
  2. c语言编程题一空几分,C语言编程规范试题
  3. HTML5中拖动功能的添加属性,html5中可拖动dragable属性及其他成员的讲解
  4. java 柯里化_函数式编程(Java描述)——Java中的函数及其柯里化
  5. shell实战训练营Day1
  6. 第七章:nginx的rewrite规则详解
  7. 正则表达式【第二卷】
  8. 【spring源码学习】spring的aop目标对象中进行自我调用,且需要实施相应的事务定义的解决方案...
  9. 分享40佳非常有创意的社交网络图标集
  10. python函数sn_Python plist.SndRcvList方法代码示例