作者:六点半起床

juejin.im/post/6844904182365814797

最近项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题。因此想借助一些分库分表的中间件,实现自动化分库分表实现。调研下来,发现Sharding-JDBC目前成熟度最高并且应用最广的Java分库分表的客户端组件。

本文主要介绍一些Sharding-JDBC核心概念以及生产环境下的实战指南,旨在帮助组内成员快速了解Sharding-JDBC并且能够快速将其使用起来。

核心概念

在使用Sharding-JDBC之前,一定是先理解清楚下面几个核心概念。

逻辑表

水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为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分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。

举例说明,如果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_order和t_order_item对应的真实表各有2个,那么真实表就有t_order_0、t_order_1、t_order_item_0、t_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 只支持单分片键,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。

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_0 到 t_user_7。可以认为是精确分片算法的简易实现

Hint分片策略

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

分布式主键

用于在分布式环境下,生成全局唯一的id。Sharding-JDBC 提供了内置的分布式主键生成器,例如 UUID、SNOWFLAKE。还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。为了保证数据库性能,主键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提供了UUID、SNOWFLAKE生成器,还支持用户实现自定义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项目中了,后面就可以进行数据分片相关的配置了。

搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典

数据分片实战

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

  • 线上数据某张表查询性能开始下降,排查下来是因为数据量过大导致的。

  • 根据历史数据量预估出未来的数据量级,并结合具体业务场景确定分库分表策略。

  • 自动分库分表代码实现。

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

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) {// 这里就是根据范围查询条件,筛选出匹配的真实表集合}/*** logicTableName_{year}_q{quarter}* 按季度精确分片* @param availableTargetNames 可用的真实表集合* @param shardingValue 分片值* @return*/public static String quarterPreciseSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {// 这里就是根据等值查询条件,计算出匹配的真实表}
}

到这里,针对hc_question_reply_record表,使用reply_wheel_time作为分片键,按照季度分片的处理就完成了。还有一点要注意的就是,分库分表之后,查询的时候最好都带上分片键作为查询条件,否则就会使用全库路由,性能很低。

还有就是Sharing-JDBC对mysql的全文索引支持的不是很好,项目有使用到的地方也要注意一下。总结来说整个过程还是比较简单的,后续碰到其它业务场景,相信大家按照这个思路肯定都能解决的。

琐碎时间想看一些技术文章,可以去公众号菜单栏翻一翻我分类好的内容,应该对部分童鞋有帮助。同时看的过程中发现问题欢迎留言指出,不胜感谢~。另外,有想多了解哪些方面内容的可以留言(什么时候,哪篇文章下留言都行),附菜单栏截图(PS:很多人不知道公众号菜单栏是什么)

END

我知道你 “在看”

数据量大了一定要分表,分库分表Sharding-JDBC入门与项目实战相关推荐

  1. 亿级大表分库分表实战总结(万字干货,实战复盘)

    亿级大表分库分表实战总结(万字干货,实战复盘) 以下文章来源于阿丸笔记 ,作者阿丸笔记 阿丸笔记 分库分表的文章网上非常多,但是大多内容比较零散,以讲解知识点为主,没有完整地说明一个大表的切分.新架构 ...

  2. 2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储

    第 6 章 数据存储 本章介绍Android 4种存储方式的用法,包括共享参数SharedPreferences.数据库SQLite.存储卡文 件.App的全局内存,另外介绍Android重要组件-应 ...

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

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

  4. sharding分表后主键_分库分表【Sharding-JDBC】入门与项目实战

    最近项目中不少表的数据量越来越大,并且导致了一些数据库的性能问题.因此想借助一些分库分表的中间件,实现自动化分库分表实现.调研下来,发现Sharding-JDBC目前成熟度最高并且应用最广的Java分 ...

  5. mysql 分区 分表 分库分表

    分区 把一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上 mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面(可以通过my.ini中的d ...

  6. 数据分析TB级别数据量大了怎么办,不会代码模型训练怎么办?

    数据分析经常会遇到数据量大的问题,比如用R语言和Python语言时经常会遇到内存溢出的问题,即使把整个机器内存全部使用,达到最大使用率,还是无济于事,比如数据量是10T,并且在大数据量下,既要保证数据 ...

  7. 大数据开发基础入门与项目实战(三)Hadoop核心及生态圈技术栈之5.即席查询Impala介绍及入门使用

    文章目录 前言 1.Impala概述 (1)Impala的概念和优势 (2)Impala的缺点及适用场景 2.Impala的安装与入门 (1)准备工作 (2)制作本地yum源 (3)安装Impala ...

  8. 大数据开发基础入门与项目实战(三)Hadoop核心及生态圈技术栈之3.数据仓库工具Hive基础

    文章目录 1.Hive概述 (1)数仓工具Hive的产生背景 (2)数仓工具Hive与RDBMS对比 (3)数仓工具Hive的优缺点 (4)数仓工具Hive的架构原理 2.Hive安装与配置 (1)安 ...

  9. 大数据开发基础入门与项目实战(三)Hadoop核心及生态圈技术栈之2.HDFS分布式文件系统

    文章目录 前言 1.HDFS特点 2.命令行和API操作HDFS (1)Shell命令行客户端 (2)API客户端连接HDFS的两种方式 (3)API客户端上传下载文件 (4)API客户端文件详情及文 ...

最新文章

  1. [云炬小程序实战笔记] 第1章 全新版:初识微信小程序
  2. 云南计算机专业知识真题,2014年云南省事业单位考试专计算机专业知识模拟真题.doc...
  3. 建站倒计时html,网页短时间维护倒计时js代码
  4. 20140617 数组和链表的区别
  5. 携号转网全面启动后,新诈骗套路也跟上了!一招教你识别!
  6. 黄章爆料魅族16s/16s Plus更多信息 无线充电已做到24W
  7. 怎么关闭虚拟机服务器,ESXI | 命令行 | 强行关闭虚拟机进程 | 无法远程访问服务器...
  8. C++ 操作sqlite
  9. Python之进程、线程、锁
  10. Java 中 Comparable 和 Comparator 比较(转)
  11. 在公司网络中如何手动为apt-get设置代理
  12. Eclipse主题设置
  13. linux系统中jdk的卸载及安装
  14. Eclipse的下载与安装
  15. 高效数字音频功率放大器NTP8928
  16. 武书连2022中国大学综合实力排行榜发布!
  17. 【百度OCR 文字识别篇】好奇怪的SDK108错误,换种方法解决
  18. 用poi 给word文档添加水印
  19. 世界第一台电子计算机到底是谁?
  20. b站服务器崩溃大会员自动续费,[财经]B站因服务器故障赠送用户1天大会员!回应补偿会自动续费会退款 - 南方财富网...

热门文章

  1. 三星Galaxy Note10+最后的爆料:配备更大的S-Pen手写笔
  2. 巴菲特:伯克希尔没有所谓的5G核心能力 子公司会涉足相关行业
  3. 又炸了!三星5G旗舰手机首燃 官方如此回应...
  4. 没有黑色版本?AirPods 2广告泄露:外型无变化
  5. 真假难辨!全球首位AI合成女主播正式上岗 引发全球媒体圈关注
  6. TKDragView_TKCalendarView:页面curl的动画日历
  7. MFC小笔记:系统托盘实现
  8. linux5.5 里dns,linux red hat 5.5 dns 问题求解
  9. 【java】java Integer 缓存 一定是 -128~127 吗
  10. 【java】java JVM如何科学的设置内存参数 虚拟机调优案例