通过ShardingSphere实现分库分表
本章基于项目演示分库分表的具体实现
分库分表的概述
分库是将原本的单库拆分为多个库,分表是将原来的单表拆分为多个表。
分库有两种模式:
- 垂直拆库:电商库MallDB,业务拆分后就是UserDB、OrderDB、PayDB等。
- 分片拆库:用户库UserDB,分片库后就是UserDB_0、UserDB_1、UserDB_xx。
分表也有两种模式:
- 垂直拆表: 订单表 OrderTable,拆分后就是 OrderTable 以及 OrderExtTable。
- 水平拆表: 订单表 OrderTable,拆分后就是 OrderTable_0、 OrderTable_xxx。
📣什么场景分库分表?
当出现以下两种场景下,需要进行分库又分表:高并发写入和海量数据:
高并发写入场景:当应用面临高并发的写入请求时,单一数据库可能无法满足写入压力,此时可以将数据按照一定规则拆分到多个数据库中,每个数据库处理部分数据的写入请求,从而提高写入性能。
海量数据场景:随着数据量的不断增加,单一数据库的存储和查询性能可能逐渐下降。此时,可以将数据按照一定的规则拆分到多个表中,每个表存储部分数据,从而分散数据的存储压力,提高查询性能。
分库分表设计
如何选择分片键?
- 数据均匀性: 分片键应该保证数据的均匀分布在各个分片上,避免出现热点数据集中在某个分片上的情况。
- 业务关联性: 分片键应该与业务关联紧密,这样可以避免跨分片查询和跨库事务的复杂性。
- 数据不可变: 一旦选择了分片键,它应该是不可变的,不能随着业务的变化而频繁修改。
分库分表算法?
分库分表的算法会根据业务的不同而变化,所以并没有固定算法。在业界里用的比较多的有两种:
- HashMod:通过对分片键进行哈希取模的分片算法(本次所使用)
- 时间范围:基于时间范围分片算法
📣优惠券模板如何分库?
1.优惠券模板分多少表?
根据上面数据估算,30亿数据量需要分多少个表?这其实又会涉及到一个知识点,那就是SQL复杂么?
- SQL复杂,拆分百万级别。
- SQL不复杂,全部走索引,千万甚至亿级别。
我们以优惠券模板举例,不涉及复杂 SQL,但是依然不建议大家数据量到达亿级别,总归要留有余量。在这里我们取经验值 2000 万,30 亿数据就是拆分 150 张表即可。
为什么取2000万?其实数据量不是特别多的情况下,基本上3次磁盘IO就能获取到数据。再多的话可能磁盘IO会增加,但是还好。考虑到数据库表备份等其他操作,不建议单表太多数据。
2.优惠券模板是否需要分库?
不需要,因为并发不高。
如果需要分析一个业务场景如何分库,那就需要知道单个MySQL Server的瓶颈是多少?通过之前压测得知,单台MySQL Server的写瓶颈大概在 4000-5000 /TPS,查询可能更高一些。
如果我们的场景业务每秒TPS在1万,那么就需要至少分两个库,然后将上面的150张表分别放入即可。
后面优惠券的分发和领取是需要分库的,所以我们为了不重复创建表,在这里直接通过分库的形式展示。
3.优惠券模板表分片键如何选择?
参考国内某电商平台优惠券管理页面,需要根据当前店铺创建的优惠券分页查询。
答案呼之欲出,那就是店铺编号字段。
ShardingSphere 项目实战
1.初始化数据库&表
创建两个数据库:
CREATE DATABASE IF NOT EXISTS one_coupon_rebuild_0;
CREATE DATABASE IF NOT EXISTS one_coupon_rebuild_1;
分别在两个数据库中创建优惠券模板表:
CREATE TABLE `t_coupon_template_0`
(`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',`name` varchar(256) DEFAULT NULL COMMENT '优惠券名称',`shop_number` bigint(20) DEFAULT NULL COMMENT '店铺编号',`source` tinyint(1) DEFAULT NULL COMMENT '优惠券来源 0:店铺券 1:平台券',`target` tinyint(1) DEFAULT NULL COMMENT '优惠对象 0:商品专属 1:全店通用',`goods` varchar(64) DEFAULT NULL COMMENT '优惠商品编码',`type` tinyint(1) DEFAULT NULL COMMENT '优惠类型 0:立减券 1:满减券 2:折扣券',`valid_start_time` datetime DEFAULT NULL COMMENT '有效期开始时间',`valid_end_time` datetime DEFAULT NULL COMMENT '有效期结束时间',`stock` int(11) DEFAULT NULL COMMENT '库存',`receive_rule` json DEFAULT NULL COMMENT '领取规则',`consume_rule` json DEFAULT NULL COMMENT '消耗规则',`status` tinyint(1) DEFAULT NULL COMMENT '优惠券状态 0:生效中 1:已结束',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '修改时间',`del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',PRIMARY KEY (`id`),KEY `idx_shop_number` (`shop_number`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1810967816300515330 DEFAULT CHARSET=utf8mb4 COMMENT='优惠券模板表';
2.引入 ShardingSphere Maven Jar 依赖
<dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core</artifactId><version>5.3.2</version>
</dependency>
3.变更Application.yaml
和创建ShardingSphere配置文件
# Application.yaml 文件
spring:application:name: oneCoupon-merchant-admindatasource:url: jdbc:mysql://127.0.0.1:3306/one_coupon_rebuild?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: root# 修改为 ShardingSphere 自定义驱动spring:application:name: oneCoupon-merchant-admindatasource:driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriverurl: jdbc:shardingsphere:classpath:shardingsphere-config.yaml
创建 shardingsphere-config.yaml
数据库分片配置文件 。
# 数据源集合
dataSources:# 自定义数据源名称,可以是 ds_0 也可以叫 datasource_0 都可以ds_0:dataSourceClassName: com.zaxxer.hikari.HikariDataSourcedriverClassName: com.mysql.cj.jdbc.DriverjdbcUrl: jdbc:mysql://127.0.0.1:3306/one_coupon_rebuild_0?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: rootds_1:dataSourceClassName: com.zaxxer.hikari.HikariDataSourcedriverClassName: com.mysql.cj.jdbc.DriverjdbcUrl: jdbc:mysql://127.0.0.1:3306/one_coupon_rebuild_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: rootrules:- !SHARDINGtables: # 需要分片的数据库表集合t_coupon_template: # 优惠券模板表# 真实存在数据库中的物理表actualDataNodes: ds_${0..1}.t_coupon_template_${0..8}databaseStrategy: # 分库策略standard: # 单分片键分库shardingColumn: shop_number # 分片键shardingAlgorithmName: coupon_template_database_mod # 库分片算法名称,对应 rules[0].shardingAlgorithmstableStrategy: # 分表策略standard: # 单分片键分表shardingColumn: shop_number # 分片键shardingAlgorithmName: coupon_template_table_mod # 表分片算法名称,对应 rules[0].shardingAlgorithmsshardingAlgorithms: # 分片算法定义集合coupon_template_database_mod: # 优惠券分库算法定义type: HASH_MOD # 基于 Hash 方式分片props:sharding-count: 2 # 一共有 2 个库coupon_template_table_mod: # 优惠券分表算法定义type: HASH_MOD # 基于 Hash 方式分片props:sharding-count: 8 # 单库 8 张表props:# 配置 ShardingSphere 默认打印 SQL 执行语句sql-show: true
📢在项目使用中,出现了数据分片不均匀的问题
因为我们的分片键shop_number
经过数据库Hash后已经确定是奇数还是偶数了,所以哪怕Hash的数值(库和表数量)变了,但是仍然只能是奇数和偶数。
所以这里我们需要变更Hash算法,通过自定义的Hash算法扰动分片结果。
5.修改数据库表和分片设置
我们将 one_coupon_rebuild_1
数据库中的 t_coupon_template_0-7 表删除,创建更多的数据库表 。
分片配置有两个变更点,那就是数据库表的分片从每个数据库的0..8变更为所有数据库里的表0..16,以及从框架自带的HashMod分片算法修改为自定义分片算法。分片算法见下文所示。
rules:- !SHARDINGtables: # 需要分片的数据库表集合t_coupon_template: # 优惠券模板表# 真实存在数据库中的物理表actualDataNodes: ds_${0..1}.t_coupon_template_${0..15}databaseStrategy: # 分库策略standard: # 单分片键分库shardingColumn: shop_number # 分片键shardingAlgorithmName: coupon_template_database_mod # 库分片算法名称,对应 rules[0].shardingAlgorithmstableStrategy: # 分表策略standard: # 单分片键分表shardingColumn: shop_number # 分片键shardingAlgorithmName: coupon_template_table_mod # 表分片算法名称,对应 rules[0].shardingAlgorithmsshardingAlgorithms: # 分片算法定义集合coupon_template_database_mod: # 优惠券分库算法定义type: CLASS_BASED # 根据自定义库分片算法类进行分片props: # 分片相关属性# 自定义库分片算法ClassalgorithmClassName: com.nageoffer.onecoupon.merchant.admin.dao.sharding.DBHashModShardingAlgorithmsharding-count: 16 # 分片总数量strategy: standard # 分片类型,单字段分片coupon_template_table_mod: # 优惠券分表算法定义type: CLASS_BASED # 根据自定义库分片算法类进行分片props: # 分片相关属性# 自定义表分片算法ClassalgorithmClassName: com.nageoffer.onecoupon.merchant.admin.dao.sharding.TableHashModShardingAlgorithmstrategy: standard # 分片类型,单字段分片
6.创建自定义分片算法
分库和分表算法类似,我们以分库算法解析:
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {long id = shardingValue.getValue(); // 分片键值,也就是商家店铺编号int dbSize = availableTargetNames.size(); // 一共有多少个真实的数据库,咱们就两个 ds_0、ds_1int mod = (int) hashShardingValue(id) % shardingCount / (shardingCount / dbSize); // 取模int index = 0;// 通过刚才的数据库下标,获取到数据库逻辑名称 ds_0 或者 ds_1for (String targetName : availableTargetNames) {if (index == mod) {return targetName;}index++;}throw new IllegalArgumentException("No target found for value: " + id);
}
修改完分片算法之后 ,再次测试: