在深入探讨 Sharding-JDBC 之前,建议读者先了解数据库分库分表的基本概念和应用场景。如果您还没有阅读过相关的内容,可以先阅读我们之前的文章:
关系型数据库海量数据存储策略-CSDN博客
这篇文章将帮助您更好地理解分库分表的基本原理和实现方法。
1. Sharding-JDBC 介绍
1.1. 背景
Sharding-JDBC 最初是由当当网内部开发的一款分库分表框架,于2017年开始对外开源。经过社区贡献者的不断迭代,功能逐渐完善,并于2020年4月16日正式成为 Apache 软件基金会的顶级项目,更名为 ShardingSphere。
Sharding-Sphere官网:Apache ShardingSphere
Sharding-Sphere官方文档:Overview :: ShardingSphere
Sharding-Sphere中文文档:概览 :: ShardingSphere
Sharding-Sphere中文文档2:概览 :: ShardingSphere
1.2. 生态圈
现在的 ShardingSphere 不再仅仅指某个框架,而是一个完整的生态圈,包括以下三个主要组件:
- Sharding-JDBC: 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供额外服务。
- Sharding-Proxy: 提供数据库代理服务,支持多种数据库协议。
- Sharding-Sidecar: 以容器化方式部署,支持 Kubernetes 环境。
1.3. Sharding-Sphere 与 MyCat 的区别
Mycat | Sharding-JDBC | Sharding-Proxy | Sharding-Sidecar | |
---|---|---|---|---|
官方网站 | 官方网站 | 官方网站 | 官方网站 | 官方网站 |
源码地址 | GitHub | GitHub | GitHub | GitHub |
官方文档 | Mycat 权威指南 | 官方文档 | 官方文档 | 官方文档 |
开发语言 | Java | Java | Java | Java |
开源协议 | GPL-2.0/GPL-3.0 | Apache-2.0 | Apache-2.0 | Apache-2.0 |
数据库 | MySQL | MySQL | MySQL/PostgreSQL | MySQL/PostgreSQL |
连接数 | 低 | 高 | 低 | 高 |
应用语言 | 任意 | Java | 任意 | 任意 |
代码入侵 | 无 | 需要修改代码 | 无 | 无 |
性能 | 损耗略高 | 损耗低 | 损耗略高 | 损耗低 |
无中心化 | 否 | 是 | 否 | 是 |
静态入口 | 有 | 无 | 有 | 无 |
管理控制台 | Mycat-web | Sharding-UI | Sharding-UI | Sharding-UI |
分库分表 | 单库多表/多库单表 | ✔️ | ✔️ | ✔️ |
多租户方案 | ✔️ | -- | -- | -- |
读写分离 | ✔️ | ✔️ | ✔️ | ✔️ |
分片策略定制化 | ✔️ | ✔️ | ✔️ | ✔️ |
分布式主键 | ✔️ | ✔️ | ✔️ | ✔️ |
标准化事务接口 | ✔️ | ✔️ | ✔️ | ✔️ |
XA强一致事务 | ✔️ | ✔️ | ✔️ | ✔️ |
柔性事务 | -- | ✔️ | ✔️ | ✔️ |
配置动态化 | 开发中 | ✔️ | ✔️ | ✔️ |
编排治理 | 开发中 | ✔️ | ✔️ | ✔️ |
数据脱敏 | -- | ✔️ | ✔️ | ✔️ |
可视化链路追踪 | -- | ✔️ | ✔️ | ✔️ |
弹性伸缩 | 开发中 | 开发中 | 开发中 | 开发中 |
多节点操作 | 分页 | 分页 | 分页 | 分页 |
跨库关联 | 跨库 2 表 Join | -- | -- | -- |
IP 白名单 | ✔️ | -- | -- | -- |
SQL 黑名单 | ✔️ | -- | -- | -- |
1.4. Sharding-JDBC 特点
- 轻量级框架: 在 Java 的 JDBC 层提供额外服务,使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖。
- 兼容性强: 完全兼容 JDBC 和各种 ORM 框架,如 JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
- 支持多种数据库连接池: 如 DBCP, C3P0, BoneCP, Druid, HikariCP 等。
- 广泛支持数据库: 支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。
1.5. Sharding-JDBC优点
1. 透明性:
- Sharding-JDBC 作为 JDBC 驱动的增强,对应用程序来说是透明的,无需修改业务代码即可实现数据库分片。
- 配置简单,通过配置文件或注解方式,可以方便地进行分片规则的配置。
2. 高性能:
- Sharding-JDBC 是基于 Java 的字节码增强技术,性能损耗较小,能够高效地处理高并发请求。
- 支持并行执行多个分片上的查询,提高查询效率。
3. 功能丰富:支持读写分离、分布式事务、数据加密。
1.6. Sharding-JDBC缺点
1. 功能限制
- 跨库操作存在限制,性能和效率可能受影响。
- 不支持所有复杂的 SQL 语句,需要手动优化或改写。
2. 维护成本
- 分片后的数据库管理和维护复杂,需要更多运维工作。
- 故障恢复面临更大挑战,需要完善的备份和恢复机制。
3. 复杂性
- 虽然配置相对简单,但对于复杂的分片规则和多表关联查询,配置和管理可能会变得复杂。
- 对于初学者来说,理解和掌握 Sharding-JDBC 的全部功能和配置可能需要一定的时间。
2. 数据分片
2.1. 核心概念
- 逻辑表:水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为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);
在不配置绑定表关系时,假设分片键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);
其中t_order在FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么t_order_item表的分片计算将会使用t_order的条件。故绑定表之间的分区键要完全相同。
- 广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
2.2. Sharding-JDBC 数据分片执行原理
2.2.1. SQL解析
1. 解析规则(Parsing Rules)
- 作用:定义如何将 SQL 语句解析成抽象语法树(AST)。
- 实现:
- 词法分析:将 SQL 语句分解成一个个的词法单元(Token)。
- 语法分析:将词法单元组合成抽象语法树(AST)。
- 示例:对于 SQL 语句 SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18;,解析规则会生成如下的 AST:
为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。
2. 提取规则(Extraction Rules)
- 作用:从解析生成的 AST 中提取关键信息,如表名、列名、条件等。
- 实现:
- 遍历 AST:通过递归或迭代的方式遍历 AST,提取所需的信息。
- 信息提取:提取表名、列名、条件表达式等。
- 示例:对于 SQL 语句 SELECT * FROM table WHERE id = 1;,提取规则会提取以下信息:
- 表名:table
- 条件:id = 1
3. 填充规则(Filling Rules)
- 作用:根据提取的关键信息和分片规则,填充路由信息。
- 实现:
- 分片键提取:从提取的信息中找到分片键。
- 路由计算:根据分片规则计算出目标数据源和表。
- 示例:假设分片规则是按 user_id 的奇偶性分片,对于 SQL 语句 SELECT * FROM order WHERE user_id = 1001;,填充规则会计算出目标表为 order_1。
4. 优化规则(Optimization Rules)
- 作用:对解析和填充后的 SQL 语句进行优化,提高执行效率。
- 实现:
- 子查询优化:将复杂的子查询转换为更高效的查询。
- 索引优化:根据索引信息优化查询计划
- 并行执行:将可以并行执行的查询拆分成多个子查询,提高执行速度。
- 示例:对于 SQL 语句 SELECT * FROM order WHERE user_id IN (1001, 1002, 1003);,优化规则可能会将其拆分成多个子查询:
SELECT * FROM order_1 WHERE user_id = 1001;
SELECT * FROM order_0 WHERE user_id = 1002;
SELECT * FROM order_1 WHERE user_id = 1003;
2.2.2. SQL路由
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN)。 不携带分片键的SQL则采用广播路由。路由引擎的整体结构划分如下图。
2.2.2.1. 分片路由
分片路由根据分片键进行路由,将 SQL 请求路由到特定的数据库和表中。根据分片键的不同操作符,分片路由可以进一步划分为以下几种类型:
1. 直接路由:
- 条件:通过 Hint API 直接指定路由至库表,且只分库不分表。
- 特点:避免 SQL 解析和结果归并,兼容性强,支持复杂 SQL。
2. 标准路由:
- 条件:不包含关联查询或仅包含绑定表之间的关联查询。
- 特点:分片键操作符为等号时路由到单库(表),操作符为 BETWEEN 或 IN 时可能路由到多库(表)。
- 示例:
SELECT * FROM t_order WHERE order_id IN (1, 2);
路由结果:
SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
3. 笛卡尔路由:
条件:非绑定表之间的关联查询。
特点:无法根据绑定表关系定位分片规则,需要拆解为笛卡尔积组合执行,性能较低。
示例:
SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
路由结果:
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
2.2.2.2. 广播路由
广播路由将 SQL 请求路由到所有配置的数据源或表中,适用于不携带分片键的 SQL。根据 SQL 类型,广播路由可以进一步划分为以下几种类型:
1. 全库表路由:
- 条件:处理对数据库中与其逻辑表相关的所有真实表的操作,如不带分片键的 DQL 和 DML,以及 DDL。
- 示例:
SELECT * FROM t_order WHERE good_prority IN (1, 10);
路由结果:
SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
2. 全库路由:
- 条件:处理对数据库的操作,如 SET 类型的数据库管理命令和 TCL 事务控制语句。
- 示例:
SET autocommit=0;
路由结果:
SET autocommit=0; -- 在 t_order_0 上执行
SET autocommit=0; -- 在 t_order_1 上执行
3. 全实例路由:
- 条件:处理 DCL 操作,如授权语句。
- 示例:
CREATE USER customer@127.0.0.1 identified BY '123';
路由结果:
CREATE USER customer@127.0.0.1 identified BY '123'; -- 在所有实例上执行
4. 单播路由:
- 条件:获取某一真实表信息的场景,仅需要从任意库中的任意真实表中获取数据。
- 示例:
DESCRIBE t_order;
路由结果:
DESCRIBE t_order_0; -- 任意选择一个真实表执行
5. 阻断路由:
- 条件:屏蔽 SQL 对数据库的操作,如 USE 命令。
- 示例:
USE order_db;
路由结果:
-- 不执行该命令
2.2.3. SQL改写
SQL 改写是将面向逻辑库与逻辑表编写的 SQL 转换为在真实数据库中可执行的 SQL。它主要包括正确性改写和优化改写两部分。
2.2.3.1. 正确性改写
将面向逻辑库与逻辑表编写的 SQL 转换为在真实数据库中可以正确执行的 SQL。
场景 | 逻辑 SQL | 改写后的 SQL |
简单场景 | SELECT order_id FROM t_order WHERE order_id=1; | SELECT order_id FROM t_order_1 WHERE order_id=1; |
复杂场景 | SELECT order_id FROM t_order WHERE order_id=1 AND remarks=' t_order xxx'; | SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks=' t_order xxx'; |
表名作为字段标识符 | SELECT t_order.order_id FROM t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx'; | SELECT t_order_1.order_id FROM t_order_1 WHERE t_order_1.order_id=1 AND remarks=' t_order xxx'; |
表别名 | SELECT t_order.order_id FROM t_order AS t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx'; | SELECT t_order.order_id FROM t_order_1 AS t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx'; |
2.2.3.1.1. 标识符改写
标识符 | 描述 |
表名称 | 将逻辑表名称改写为真实表名称。 |
索引 名称 | 在某些数据库中,索引以表为维度创建,可以重名;在另一些数据库中,索引以数据库为维度创建,要求名称唯一。 |
Schema 名称 | 将逻辑 Schema 替换为真实 Schema。 |
2.2.3.1.2. 补列
场景 | 逻辑 SQL | 改写后的 SQL |
GROUP BY 和 ORDER BY | SELECT order_id FROM t_order ORDER BY user_id; | SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id; |
复杂场景 | SELECT o.* FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id; | SELECT o.*, order_item_id AS ORDER_BY_DERIVED_0 FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id; |
AVG 聚合函数 | SELECT AVG(price) FROM t_order WHERE user_id=1; | SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_SUM_0 FROM t_order WHERE user_id=1; |
自增主键 | INSERT INTO t_order (field1,field2) VALUES (10, 1); | INSERT INTO t_order (field1,field2, order_id) VALUES (10, 1, xxxxx); |
2.2.3.1.3. 分页修正
场景 | 逻辑 SQL | 改写后的 SQL |
分页查询 | SELECT score FROM t_score ORDER BY score DESC LIMIT 1, 2; | SELECT score FROM t_score_0 ORDER BY score DESC LIMIT 0, 3; SELECT score FROM t_score_1 ORDER BY score DESC LIMIT 0, 3; |
2.2.3.1.4. 批量拆分
场景 | 逻辑 SQL | 改写后的 SQL(order_id 按奇偶分片) |
批量插入 | INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx'); | INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx'); INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx'); |
IN 查询 | SELECT * FROM t_order WHERE order_id IN (1, 2, 3); | SELECT * FROM t_order_0 WHERE order_id IN (2); SELECT * FROM t_order_1 WHERE order_id IN (1, 3); |
2.2.3.2. 优化改写
类型 | 描述 |
单节点优化 | 如果 SQL 路由到单个节点,无需进行额外的改写,以减少不必要的开销。 |
流式归并优化 | 为包含 GROUP BY 的 SQL 增加 ORDER BY 以实现流式归并,提高性能。 |
2.2.4. SQL执行
2.2.4.1. 连接模式
ShardingSphere 提出了两种连接模式:内存限制模式(MEMORY_STRICTLY) 和 连接限制模式(CONNECTION_STRICTLY)。
1. 内存限制模式(MEMORY_STRICTLY)
- 特点:不对数据库连接数量做限制。
- 适用场景:适合 OLAP 操作,通过多线程并发处理多个表的查询,提升执行效率。
- 优点:最大化执行效率,优先选择流式归并,节省内存。
- 缺点:可能占用大量数据库连接资源。
2. 连接限制模式(CONNECTION_STRICTLY)
- 特点:严格控制数据库连接数量。
- 适用场景:适合 OLTP 操作,通常带有分片键,路由到单一的分片。
- 优点:防止过多占用数据库连接资源,保证在线系统数据库资源的充分利用。
- 缺点:可能牺牲一定的执行效率,采用内存归并。
2.2.4.2. 自动化执行引擎
ShardingSphere 的自动化执行引擎在内部消化了连接模式的概念,用户无需手动选择模式,执行引擎会根据当前场景自动选择最优的执行方案。
1. 准备阶段
- 结果集分组:根据 maxConnectionSizePerQuery 配置项,将 SQL 的路由结果按数据源名称分组。
- 连接模式计算:
- 计算每个数据库实例在 maxConnectionSizePerQuery 允许范围内,每个连接需要执行的 SQL 路由结果组。
- 如果一个连接需要执行的请求数量大于1,采用内存归并;否则,采用流式归并。
- 执行单元创建:创建执行单元时,以原子性方式一次性获取所需的所有数据库连接,避免死锁
2. 执行阶段
- 分组执行:将准备阶段生成的执行单元分组下发至底层并发执行引擎,并发送执行事件。
- 归并结果集生成:根据连接模式生成内存归并结果集或流式归并结果集,并传递至结果归并引擎。
2.2.4.3. 关键优化
- 避免锁定:对于只需要获取1个数据库连接的操作,不进行锁定,提升并发效率。
- 资源锁定:仅在内存限制模式下进行资源锁定,连接限制模式下不会产生死锁。
2.2.5. 结果归并
ShardingSphere 支持的结果归并类型从功能上分为五种:遍历、排序、分组、分页和聚合。从结构上划分,可分为流式归并、内存归并和装饰者归并。
2.2.5.1. 结构划分
1. 流式归并
- 特点:逐条获取数据,减少内存消耗。
- 适用场景:适用于大多数查询,尤其是大数据量的查询。
- 类型:遍历归并、排序归并、流式分组归并。
2. 内存归并
- 特点:将所有数据加载到内存中进行处理。
- 适用场景:适用于分组项与排序项不一致的查询。
- 类型:内存分组归并。
3. 装饰者归并
- 特点:在流式归并和内存归并的基础上进行功能增强。
- 类型:分页归并、聚合归并。
2.2.5.2. 功能划分
1. 遍历归并
- 特点:最简单的归并方式,将多个数据结果集合并为一个单向链表。
- 实现:遍历完当前数据结果集后,链表元素后移一位,继续遍历下一个数据结果集。
2. 排序归并
- 特点:适用于包含 ORDER BY 语句的查询。
- 实现:使用优先级队列对多个有序结果集进行归并排序。
- 示例:
- 假设有3个数据结果集,每个结果集已经根据分数排序。
- 使用优先级队列将每个结果集的当前数据值进行排序。
- 每次 next 调用时,弹出队列首位的数据值,并将游标下移一位,重新加入队列。
3. 分组归并
- 特点:分为流式分组归并和内存分组归并。
- 流式分组归并:
- 适用场景:分组项与排序项一致。
- 实现:一次将多个数据结果集中分组项相同的数据全数取出,并进行聚合计算。
- 内存分组归并:
- 适用场景:分组项与排序项不一致。
- 实现:将所有数据加载到内存中进行分组和聚合。
4. 聚合归并
- 特点:处理聚合函数,如 MAX、MIN、SUM、COUNT 和 AVG。
- 实现:
- 比较类型:MAX 和 MIN,直接返回最大或最小值。
- 累加类型:SUM 和 COUNT,将同组的数据进行累加。
- 求平均值:AVG,通过 SUM 和 COUNT 计算。
5. 分页归并
- 特点:在其他归并类型基础上追加分页功能。
- 实现:通过 LIMIT 语句进行分页,但不会将大量无意义的数据加载到内存中。
- 示例:
- 使用 ID 进行分页,例如:SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;
- 记录上次查询结果的最后一条记录的 ID 进行下一页查询,例如:SELECT * FROM t_order WHERE id > 10000000 LIMIT 10;
2.3. 使用步骤
2.3.1. 引入依赖
在 pom.xml 文件中添加 Sharding-JDBC 的依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
2.3.2. 添加配置文件
注意:
- 多个数据源的数据库连接池可以不同。
- 多个数据源的数据库驱动类型必须相同。
2.3.2.1. 精确分片
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
1. 编写yaml文件
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
standard: # 用于单分片键的标准分片场景
# 添加数据分库字段(根据字段插入数据到那个表)
sharding-column: id
# 精确分片
precise-algorithm-class-name: ${common.algorithm.db}
# 分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
standard: # 用于单分片键的标准分片场景
# 分片列名称
sharding-column: id
# 精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
precise-algorithm-class-name: ${common.algorithm.tb}
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
# 配置分片策略
common:
algorithm:
db: com.zjp.shadingjdbcdemo.strategy.database.DatabasePreciseAlgorithm
tb: com.zjp.shadingjdbcdemo.strategy.table.TablePreciseAlgorithm
2. 添加分库规则
实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.database;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
public class DatabasePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
/**
* 精确分片
*
* @param collection 数据源集合
* @param preciseShardingValue 分片参数
* @return 数据库
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 自定义分片方法
// 1.获取分片键的值
Long value = preciseShardingValue.getValue();
// 2.获取逻辑表名
String logicTableName = preciseShardingValue.getLogicTableName();
// 3.获取数据库名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 4.根据分片键的值,计算出对应的数据源名称
return dbNames.get((int) (value % 2));
}
}
3. 添加分表规则
实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.table;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
public class TablePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
/**
* 精确分片
*
* @param collection 数据源集合
* @param preciseShardingValue 分片参数
* @return 数据库
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 自定义分片方法
// 1.获取分片键值
Long value = preciseShardingValue.getValue();
// 2.获取逻辑表名
String logicTableName = preciseShardingValue.getLogicTableName();
// 3.获取真实表总数
int size = collection.size();
// 4.计算表名
int v;
long n;
do {
v = (int) (Math.pow(2, Math.ceil(Math.log(size) / Math.log(2))) - 1);
n = value & v;
} while (n >= size);
// 5.获取真实表名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 6.返回真实表
return dbNames.get((int) n);
}
}
2.3.2.2. 范围分片
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用。
标准分片策略(StandardShardingStrategy)提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
1. 编写yaml文件
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
standard: # 用于单分片键的标准分片场景
# 添加数据分库字段(根据字段插入数据到那个表)
sharding-column: id
# 精确分片
precise-algorithm-class-name: ${common.algorithm.db}
# 范围分片
range-algorithm-class-name: ${common.algorithm.db}
#分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
standard: # 用于单分片键的标准分片场景
# 分片列名称
sharding-column: id
# 精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
precise-algorithm-class-name: ${common.algorithm.tb}
# 范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
range-algorithm-class-name: ${common.algorithm.tb}
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
# 配置分片策略
common:
algorithm:
db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseRangeAlgorithm
tb: com.zjp.shadingjdbcdemo.strategy.table.TableRangeAlgorithm
2. 添加分库范围查询和精确查询规则
- 范围查询:实现 RangeShardingAlgorithm 接口,重写 doSharding 方法。
- 精确查询:实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法实现。
可以用两个类分别实现,也可以一个类同时实现。
package com.zjp.shadingjdbcdemo.strategy.database;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
public class DatabaseRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
/**
* 精确分片
*
* @param collection 数据源集合
* @param preciseShardingValue 分片参数
* @return 数据库
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 自定义分片方法
// 1.获取分片键的值
Long value = preciseShardingValue.getValue();
// 2.获取逻辑表名
String logicTableName = preciseShardingValue.getLogicTableName();
// 3.获取数据库名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 4.根据分片键的值,计算出对应的数据源名称
return dbNames.get((int) (value % 2));
}
/**
* 范围分片
*
* @param collection 数据源集合
* @param rangeShardingValue 分片参数
* @return 直接返回源
*/
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
// 1.获取分片键名
String shardingColumn = rangeShardingValue.getColumnName();
// 2.获取逻辑表名
String logicTableName = rangeShardingValue.getLogicTableName();
// 3.获取分片键值范围
Range<Long> valueRange = rangeShardingValue.getValueRange();
// 4.根据分片键值范围,进行范围匹配
long lower = 0L;
long upper = Long.MAX_VALUE;
// 存在下限
if (valueRange.hasLowerBound()) {
lower = valueRange.lowerEndpoint();
}
// 存在上限
if (valueRange.hasUpperBound()) {
upper = valueRange.upperEndpoint();
}
// 5.过滤并返回
return collection;
}
}
3. 添加分表规则
- 范围查询:实现 RangeShardingAlgorithm 接口,重写 doSharding 方法。
- 精确查询:实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法。
可以用两个类分别实现,也可以一个类同时实现。
package com.zjp.shadingjdbcdemo.strategy.table;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Arrays;
import java.util.Collection;
public class TableRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
/**
* 精确分片
*
* @param collection 数据源集合
* @param preciseShardingValue 分片参数
* @return 数据库
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 自定义分片方法
// 1.获取分片键值
Long value = preciseShardingValue.getValue();
// 2.获取逻辑表名
String logicTableName = preciseShardingValue.getLogicTableName();
// 3.获取真实表总数
int size = collection.size();
// 4.计算表名
long n;
int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
do {
n = value & v;
v = v / 2;
} while (n >= size);
// 5.获取真实表名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 6.返回真实表
return dbNames.get((int) n);
}
/**
* 范围分片
*
* @param collection 数据源集合
* @param rangeShardingValue 分片参数
* @return 直接返回源
*/
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
//逻辑表名称
// 1.获取分片键名
String shardingColumn = rangeShardingValue.getColumnName();
// 2.获取逻辑表名
String logicTableName = rangeShardingValue.getLogicTableName();
// 3.获取分片键值范围
Range<Long> valueRange = rangeShardingValue.getValueRange();
// 4.根据分片键值范围,进行范围匹配
long lower = 0L;
long upper = Long.MAX_VALUE;
// 存在下限
if (valueRange.hasLowerBound()) {
lower = valueRange.lowerEndpoint();
}
// 存在上限
if (valueRange.hasUpperBound()) {
upper = valueRange.upperEndpoint();
}
return collection;
}
}
2.3.2.3. 行表达式分片
对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 2} 表示t_user表根据u_id模2,而分成2张表,表名称为t_user_0到t_user_1。
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
inline: # 行表达式分片策略
# 添加数据分库字段(根据字段插入数据到那个表)
sharding-column: id
# 分片算法表达式 => 通过id取余
algorithm-expression: ds$->{id % 2}
# 分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
inline: # 行表达式分片策略
# 分片列名称
sharding-column: id
# 分片算法表达式 => 通过id取余
algorithm-expression: t_user_$->{id % 2 + 1}
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
2.3.2.4. 复合分片
用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。
复合分片策略(ComplexShardingStrategy)提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
注意:
分片键只支持Integer类型,虽然能传其他类型,但是值是无法处理的。
1. 编写yaml文件
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
complex: #用于多分片键的复合分片场景
# 添加数据分库字段(根据字段插入数据到那个表)
sharding-columns: id,age
algorithm-class-name: ${common.algorithm.db}
#分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
complex: #用于多分片键的复合分片场景
sharding-columns: id,age
algorithm-class-name: ${common.algorithm.tb}
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
# 配置分片策略
common:
algorithm:
db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseComplexAlgorithm
tb: com.zjp.shadingjdbcdemo.strategy.table.TableComplexAlgorithm
2. 添加分库范围查询和精确查询规则
实现 ComplexKeysShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.database;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class DatabaseComplexAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
/**
* 复合分片
*
* @param collection 数据源集合
* @param complexKeysShardingValue 分片键的值集合
* @return 需要查找的数据源集合
*/
@Override
public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
// 自定义复合分片规则
// 1.获取分片键的值
// 等于号 in 的值
Map<String, Collection<Integer>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
// 实现 >,>=, <=,< 和 BETWEEN AND 等操作
Map<String, Range<Integer>> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
// 2. 获取逻辑表名
String logicTableName = complexKeysShardingValue.getLogicTableName();
// 获取age的值
Collection<Integer> ageValues = columnNameAndShardingValuesMap.get("age");
Range<Integer> ageRange = columnNameAndRangeValuesMap.get("age");
// 获取id的值
Collection<Integer> idValues = columnNameAndShardingValuesMap.get("id");
Range<Integer> idRange = columnNameAndRangeValuesMap.get("id");
// 3.获取数据库名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 4.根据分片键的值,计算出对应的数据源名称
if (CollectionUtils.isNotEmpty(ageValues)) {
// 如果 ageValues 不为空,根据 age 值对 dbNames 进行取模操作
return ageValues.stream()
.map(age -> dbNames.get(age % 2))
.collect(Collectors.toSet());
} else if (ageRange != null) {
// 如果 ageRange 不为空,根据 age 范围判断属于哪个分区
int numPartitions = dbNames.size();
int maxAge = 150;
int partitionSize = (maxAge + 1) / numPartitions;
// 遍历每个分区
Set<String> resultDbs = new HashSet<>();
for (int i = 0; i < numPartitions; i++) {
int start = i * partitionSize;
int end = (i + 1) * partitionSize - 1;
if (i == numPartitions - 1) {
end = maxAge;
}
try {
// 检查 ageRange 是否与当前分区范围有交集,如果两个区间没有交集,该方法将抛出IllegalArgumentException。
ageRange.intersection(Range.closed(start, end));
resultDbs.add(dbNames.get(i));
} catch (IllegalArgumentException e) {
}
}
return resultDbs;
}
// 默认情况下返回 collection
return collection;
}
}
3. 添加分表规则
实现 ComplexKeysShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.table;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class TableRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
/**
* 精确分片
*
* @param collection 数据源集合
* @param preciseShardingValue 分片参数
* @return 数据库
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 自定义分片方法
// 1.获取分片键值
Long value = preciseShardingValue.getValue();
// 2.获取逻辑表名
String logicTableName = preciseShardingValue.getLogicTableName();
// 3.获取真实表总数
int size = collection.size();
// 4.计算表名
long n;
int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
do {
n = value & v;
v = v / 2;
} while (n >= size);
// 5.获取真实表名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 6.返回真实表
return dbNames.get((int) n);
}
/**
* 范围分片
*
* @param collection 数据源集合
* @param rangeShardingValue 分片参数
* @return 直接返回源
*/
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
//逻辑表名称
// 1.获取分片键名
String shardingColumn = rangeShardingValue.getColumnName();
// 2.获取逻辑表名
String logicTableName = rangeShardingValue.getLogicTableName();
// 3.获取分片键值范围
Range<Long> valueRange = rangeShardingValue.getValueRange();
// 4.根据分片键值范围,进行范围匹配
long lower = 0L;
long upper = Long.MAX_VALUE;
// 存在下限
if (valueRange.hasLowerBound()) {
lower = valueRange.lowerEndpoint();
}
// 存在上限
if (valueRange.hasUpperBound()) {
upper = valueRange.upperEndpoint();
}
return collection;
}
}
拓展:
Range 方法:
示例代码:
方法名
参数
返回值
说明
是否静态
closed(T lower, T upper)
T lower, T upper
Range<T>
创建一个闭区间 [lower, upper],包括下界和上界。
是
open(T lower, T upper)
T lower, T upper
Range<T>
创建一个开区间 (lower, upper),不包括下界和上界。
是
closedOpen(T lower, T upper)
T lower, T upper
Range<T>
创建一个半闭区间 [lower, upper),包括下界但不包括上界。
是
openClosed(T lower, T upper)
T lower, T upper
Range<T>
创建一个半闭区间 (lower, upper],不包括下界但包括上界。
是
greaterThan(T lower)
T lower
Range<T>
创建一个大于给定值的区间 (lower, +∞)。
是
lessThan(T upper)
T upper
Range<T>
创建一个小于给定值的区间 (-∞, upper)。
是
atLeast(T lower)
T lower
Range<T>
创建一个大于或等于给定值的区间 [lower, +∞)。
是
atMost(T upper)
T upper
Range<T>
创建一个小于或等于给定值的区间 (-∞, upper]。
是
all()
无
Range<T>
创建一个包含所有可能值的区间 (-∞, +∞)。
是
contains(T value)
T value
boolean
检查区间是否包含给定值。
否
encloses(Range<T> other)
Range<T> other
boolean
检查当前区间是否完全包含另一个区间。
否
isConnected(Range<T> other)
Range<T> other
boolean
检查两个区间是否有交集或相邻。
否
intersection(Range<T> other)
Range<T> other
Range<T>
返回两个区间的交集。如果两个区间没有交集,该方法将抛出IllegalArgumentException。
否
span(Range<T> other)
Range<T> other
Range<T>
返回两个区间的并集。
否
hasLowerBound()
无
boolean
检查区间是否有下界。
否
hasUpperBound()
无
boolean
检查区间是否有上界。
否
lowerEndpoint()
无
T
返回区间的下界。
否
upperEndpoint()
无
T
返回区间的上界。
否
lowerBoundType()
无
BoundType
返回区间的下界类型(闭合或开放)。
否
upperBoundType()
无
BoundType
返回区间的上界类型(闭合或开放)。
否
import com.google.common.collect.BoundType; import com.google.common.collect.Range; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest public class ShardingJdbcDemoApplicationTests { public static void main(String[] args) { // 创建一个闭区间 [1, 5] Range<Integer> closedRange = Range.closed(1, 5); System.out.println("Closed range: " + closedRange); // 创建一个开区间 (1, 5) Range<Integer> openRange = Range.open(1, 5); System.out.println("Open range: " + openRange); // 创建一个半闭区间 [1, 5) Range<Integer> closedOpenRange = Range.closedOpen(1, 5); System.out.println("Closed-open range: " + closedOpenRange); // 创建一个半闭区间 (1, 5] Range<Integer> openClosedRange = Range.openClosed(1, 5); System.out.println("Open-closed range: " + openClosedRange); // 创建一个大于给定值的区间 (3, +∞) Range<Integer> greaterThanRange = Range.greaterThan(3); System.out.println("Greater than range: " + greaterThanRange); // 创建一个小于给定值的区间 (-∞, 3) Range<Integer> lessThanRange = Range.lessThan(3); System.out.println("Less than range: " + lessThanRange); // 创建一个大于或等于给定值的区间 [3, +∞) Range<Integer> atLeastRange = Range.atLeast(3); System.out.println("At least range: " + atLeastRange); // 创建一个小于或等于给定值的区间 (-∞, 3] Range<Integer> atMostRange = Range.atMost(3); System.out.println("At most range: " + atMostRange); // 创建一个包含所有可能值的区间 (-∞, +∞) Range<Integer> allRange = Range.all(); System.out.println("All range: " + allRange); // 检查区间是否包含给定值 boolean contains = closedRange.contains(3); System.out.println("Contains 3: " + contains); // 检查一个区间是否完全包含另一个区间 boolean encloses = closedRange.encloses(openRange); System.out.println("Encloses open range: " + encloses); // 检查两个区间是否有交集或相邻 boolean isConnected = closedRange.isConnected(openRange); System.out.println("Is connected: " + isConnected); // 返回两个区间的交集 Range<Integer> intersection = closedRange.intersection(openRange); System.out.println("Intersection: " + intersection); // 返回两个区间的并集 Range<Integer> span = closedRange.span(openRange); System.out.println("Span: " + span); // 检查区间是否有下界和上界 boolean hasLowerBound = closedRange.hasLowerBound(); boolean hasUpperBound = closedRange.hasUpperBound(); System.out.println("Has lower bound: " + hasLowerBound); System.out.println("Has upper bound: " + hasUpperBound); // 返回区间的下界和上界 int lowerEndpoint = closedRange.lowerEndpoint(); int upperEndpoint = closedRange.upperEndpoint(); System.out.println("Lower endpoint: " + lowerEndpoint); System.out.println("Upper endpoint: " + upperEndpoint); // 返回区间的下界类型和上界类型 BoundType lowerBoundType = closedRange.lowerBoundType(); BoundType upperBoundType = closedRange.upperBoundType(); System.out.println("Lower bound type: " + lowerBoundType); System.out.println("Upper bound type: " + upperBoundType); } }
2.3.2.5. Hint分片
用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
对应HintShardingStrategy。通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。
对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。
1. 编写yaml文件
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
hint: #Hint分片策略
algorithm-class-name: ${common.algorithm.db}
# 分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
hint: #Hint分片策略
algorithm-class-name: ${common.algorithm.tb}
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
# 配置分片策略
common:
algorithm:
db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseHintAlgorithm
tb: com.zjp.shadingjdbcdemo.strategy.table.TableHintAlgorithm
2. 添加分库范围查询和精确查询规则
实现 HintShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.database;
import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;
import java.util.Collection;
import java.util.HashSet;
public class DatabaseHintAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* 执行自定义Hint分片逻辑。
*
* @param collection 数据库名称集合
* @param hintShardingValue 分片值对象,包含分片键和逻辑表名
* @return 分片后的数据源名称集合
*/
@Override
public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
// 自定义Hint分片方法
// 1.获取分片键的值
Collection<Integer> values = hintShardingValue.getValues();
// 2.获取逻辑表名
String logicTableName = hintShardingValue.getLogicTableName();
// 3.获取数据库名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 4.根据分片键的值,计算出对应的数据源名称
return values.stream()
.map(value -> dbNames.get((int) (value % 2)))
.collect(Collectors.toSet());
}
}
3. 添加分表规则
实现 HintShardingAlgorithm 接口,重写 doSharding 方法。
package com.zjp.shadingjdbcdemo.strategy.table;
import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class TableHintAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* 强制路由
*
* @param collection 数据源集合
* @param hintShardingValue 分片参数
* @return 数据库集合
*/
@Override
public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
// 自定义Hint分片方法
// 1.获取分片键的值
Collection<Integer> values = hintShardingValue.getValues();
// 2.获取逻辑表名
String logicTableName = hintShardingValue.getLogicTableName();
// 3.获取数据库名称,并排序
List<String> dbNames = collection.stream()
.sorted(String::compareTo)
.collect(Collectors.toList());
// 4.获取真实表总数
int size = collection.size();
// 5.根据分片键的值,计算出对应的数据源名称
return values.stream()
.map(value -> {
long n;
int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
do {
n = value & v;
v = v / 2;
} while (n >= size);
return dbNames.get((int) n);
})
.collect(Collectors.toSet());
}
}
2.3.2.6. 不分片
对应NoneShardingStrategy。不分片的策略。
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource: # 数据源配置,可配置多个data_source_name
names: ds0,ds1
ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
username: root # 数据库用户名
password: root # 数据库密码
sharding:
# 唯一库数据
default-data-source-name: ds0
# 分库
default-database-strategy:
none: # 不分片
# 分表
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
key-generator:
column: id # 自增列名称,缺省表示不使用自增主键生成器
type: SNOWFLAKE #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
# props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
table-strategy: # 分表策略,同分库策略
none: # 不分片
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
2.3.3. 解决jdk8新时间类与Sharding-Sphere兼容问题
以 LocalDate 和 LocalDateTime 为例:
1. 引入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
2. 编写 BaseTypeHandler 的实现类:
- LocalDate适配:
package com.zjp.shadingjdbcdemo.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
@Component
@MappedTypes(LocalDate.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType)
throws SQLException {
ps.setObject(i, parameter);
}
@Override
public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
return Convert.convert(new TypeReference<LocalDate>() {
@Override
public String getTypeName() {
return super.getTypeName();
}
},rs.getObject(columnName));
}
@Override
public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return Convert.convert(new TypeReference<LocalDate>() {
@Override
public String getTypeName() {
return super.getTypeName();
}
},rs.getObject(columnIndex));
}
@Override
public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return Convert.convert(new TypeReference<LocalDate>() {
@Override
public String getTypeName() {
return super.getTypeName();
}
},cs.getObject(columnIndex));
}
}
- LocalDateTime适配:
package com.zjp.shadingjdbcdemo.handler;
import cn.hutool.core.convert.Convert;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
@Component
@MappedTypes(LocalDateTime.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setObject(i, parameter);
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
return Convert.toLocalDateTime(rs.getObject(columnName));
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return Convert.toLocalDateTime(rs.getObject(columnIndex));
}
@Override
public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return Convert.toLocalDateTime(cs.getObject(columnIndex));
}
}
其他时间类可仿照上述方案仿写。
2.3.4. 关于雪花算法配置
雪花算法生成的id总共64位8个字节,结构如下:
符号位 | 时间位 | 工作机器标识位 | 序列位 |
1位(固定位0) | 41位 | 10位 | 12位 |
源码如下图:
通过源码可以看出,雪花算法可以额外配置三个参数:
- worker.id:工作机器标识位,表示一个唯一的工作进程id,取值范围(整数):0(包含)~1024(不包含),默认值为0。
- max.vibration.offset:序列位,同一毫秒内生成不同的ID,取值范围(整数):0(包含)~4095(包含),默认值为1。
- max.tolerate.time.difference.milliseconds:最大容忍的时钟回拨毫秒数,默认值为10。如下图源码所示,最后一次生成主键的时间 lastMilliseconds 与 当前时间currentMilliseconds 做比较,如果 lastMilliseconds > currentMilliseconds则意味着时钟回调了。那么接着判断两个时间的差值(timeDifferenceMilliseconds)是否在设置的最大容忍时间阈值 max.tolerate.time.difference.milliseconds内,在阈值内则线程休眠差值时间 Thread.sleep(timeDifferenceMilliseconds),否则大于差值直接报异常。
配置示例:
spring:
shardingsphere:
sharding:
tables:
key-generator:
column: id # 主键ID
type: SNOWFLAKE
props:
work:
id: 0
max:
vibration:
offset: 1
tolerate:
time:
difference:
milliseconds: 10
2.3.5. 测试
1. 创建数据表
CREATE DATABASE IF NOT EXISTS database1;
USE database1;
DROP TABLE IF EXISTS `t_user_1`;
DROP TABLE IF EXISTS `t_user_2`;
CREATE TABLE `t_user_1` (
`id` bigint(20) NOT NULL COMMENT '用户编号',
`name` varchar(255) DEFAULT NULL COMMENT '用户名',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`salary` double DEFAULT NULL COMMENT '用户薪资',
`birthday` datetime DEFAULT NULL COMMENT '用户生日',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_2` (
`id` bigint(20) NOT NULL COMMENT '用户编号',
`name` varchar(255) DEFAULT NULL COMMENT '用户名',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`salary` double DEFAULT NULL COMMENT '用户薪资',
`birthday` datetime DEFAULT NULL COMMENT '用户生日',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE DATABASE IF NOT EXISTS database2;
USE database2;
DROP TABLE IF EXISTS `t_user_1`;
DROP TABLE IF EXISTS `t_user_2`;
CREATE TABLE `t_user_1` (
`id` bigint(20) NOT NULL COMMENT '用户编号',
`name` varchar(255) DEFAULT NULL COMMENT '用户名',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`salary` double DEFAULT NULL COMMENT '用户薪资',
`birthday` datetime DEFAULT NULL COMMENT '用户生日',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_2` (
`id` bigint(20) NOT NULL COMMENT '用户编号',
`name` varchar(255) DEFAULT NULL COMMENT '用户名',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`salary` double DEFAULT NULL COMMENT '用户薪资',
`birthday` datetime DEFAULT NULL COMMENT '用户生日',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zjp</groupId>
<artifactId>shading-jdbc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shading-jdbc-demo</name>
<description>shading-jdbc-demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--sharding-jdbc-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<!--hutool工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<!--测试数据-->
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.zjp.shadingjdbcdemo.ShadingJdbcDemoApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3. 编写配置文件(非Sharding-JDBC配置部分)
server:
port: 18080
spring:
application:
name: sharding-jdbc-demo
logging:
level:
com.zjp.shadingjdbcdemo: debug
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# 设置当查询结果值为null时,同样映射该查询字段给实体(Mybatis-Plus默认会忽略查询为空的实体字段返回)。
call-setters-on-nulls: true
4. 创建实体类
注意:
- @TableName 注解指定的表为虚拟表 t_user。
- @TableId 的 type 属性为 IdType.AUTO 则采用 Sharding-JDBC 数据源生成策略,否则采用 MyBatis Plus 主键生成策略。
- 主键的数据类型为包装类,否则主键赋值失败,为默认值0。
package com.zjp.shadingjdbcdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户编号
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String name;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户薪资
*/
private Double salary;
/**
* 用户生日
*/
private LocalDateTime birthday;
}
5. 编写Service层和mapper层
package com.zjp.shadingjdbcdemo.service;
import com.zjp.shadingjdbcdemo.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
}
package com.zjp.shadingjdbcdemo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.mapper.UserMapper;
import com.zjp.shadingjdbcdemo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
package com.zjp.shadingjdbcdemo.mapper;
import com.zjp.shadingjdbcdemo.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
6. 编写测试类
package com.zjp.shadingjdbcdemo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.javafaker.Faker;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.hint.HintManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Random;
@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
@Autowired
private UserService userService;
private static final Faker FAKER = new Faker();
/**
* 测试精确分片、行表达式分片、复合分片和不分片
*/
@Test
public void testSaveUser() {
Date birthday = FAKER.date().birthday(18, 70);
User user = new User()
.setName(FAKER.name().fullName())
.setAge(Year.now().getValue() - birthday.getYear() - 1900)
.setSalary(10000.0)
.setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
userService.save(user);
}
/**
* 测试范围分片
*/
@Test
public void testGetUser() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getId,1L);
List<User> list = userService.list(wrapper);
list.forEach(System.out::println);
}
/**
* 测试hint分片
*/
@Test
public void testHint() {
Date birthday = FAKER.date().birthday(18, 100);
// 创建一个HintManager对象, 确保线程内只存在一个HintManager对象,否则会抛出异常"Hint has previous value, please clear first."
HintManager.clear();
HintManager hintManager = HintManager.getInstance();
// 根据数据库hint分片规则选取数据库
hintManager.addDatabaseShardingValue("t_user", 3);
// 根据数据表hint分片规则选取数据表
hintManager.addTableShardingValue("t_user", 1);
User user = new User()
.setName(FAKER.name().fullName())
.setAge(18)
.setSalary(10000.0)
.setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
userService.save(user);
// HintManager存放在线程变量中, 所以需要清除
HintManager.clear();
}
}
7. 测试
在对应的策略上打断点,以debug方式启动测试方法,查看是否走断点。
2.4. 自定义主键生成策略
2.4.1. 源码参考
1. Ctrl + 左键点击 type,点击定位文件位置,如下图所示,这三个文件即为主键生成策略的配置文件。org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件指定了两种生成策略,分别为UUID和雪花算法。
2. 以UUID为例,通过实现 ShardingKeyGenerator 接口,重新 getType 方法指定配置文件的文件名称和重新 generateKey 方法自定义主键生成策略来实现主键生成。
2.4.2. 自定义主键生成策略步骤
1. 编写 ShardingKeyGenerator 实现类
package com.zjp.shadingjdbcdemo.strategy.keygen;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
import java.util.Properties;
import java.util.Random;
@Slf4j
@Getter
@Setter
public class MyShardingKeyGenerator implements ShardingKeyGenerator {
private Properties properties = new Properties();
/**
* 重写generateKey方法以生成唯一的键值
* 此方法用于生成一个随机的长整型数字作为键值,
* 键值范围为1到1000,旨在用于需要唯一标识符的场景
*
* @return 生成的键值作为一个Comparable对象返回,由于键值为Long类型,
* 而Long实现了Comparable接口,这使得返回值可以与其它对象进行比较
*/
@Override
public Comparable<?> generateKey() {
Random random = new Random();
Long id = (long) (random.nextInt(1000) + 1);
log.info("自定义主键生成策略,主键为:{}", id);
return id;
}
/**
* 重写getType方法
* 返回一个预定义的键值,用于标识特定的类型或功能这个方法总是返回"MyKey",
* 表示当前对象或方法的特定类型或功能
*
* @return String 表示当前对象或方法类型的预定义键值
*/
@Override
public String getType() {
return "MyKey";
}
}
2. 在resource目录下创建META-INF/services/org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件,文件内容为自定义主键生成策略类的全限定名。
com.zjp.shadingjdbcdemo.strategy.keygen.MyShardingKeyGenerator
3. 配置文件指定自定义主键生成策略
spring:
shardingsphere:
sharding:
tables:
key-generator:
column: id # 主键ID
type: MyKey # 自定义主键生成策略
4. 如果是MyBatis Plus,则需要将主键生成策略换成IdType.AUTO
package com.zjp.shadingjdbcdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户编号
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String name;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户薪资
*/
private Double salary;
/**
* 用户生日
*/
private LocalDateTime birthday;
}
5. 编写测试类
package com.zjp.shadingjdbcdemo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.javafaker.Faker;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.hint.HintManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Random;
@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
@Autowired
private UserService userService;
private static final Faker FAKER = new Faker();
@Test
public void testSaveUser() {
Date birthday = FAKER.date().birthday(18, 70);
User user = new User()
.setName(FAKER.name().fullName())
.setAge(Year.now().getValue() - birthday.getYear() - 1900)
.setSalary(10000.0)
.setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
userService.save(user);
}
/**
* 测试范围分片
*/
@Test
public void testGetUser() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getId,1L);
List<User> list = userService.list(wrapper);
list.forEach(System.out::println);
}
}
6. 启动测试
通过日志和数据库可以看出主键生成成功。
2.5. 广播表
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
2.5.1. 配置步骤
编写配置文件,指定广播表:
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
sharding:
broadcast-tables: # 广播表规则列表
- t_dict
tables:
t_dict:
key-generator:
column: dict_id
type: SNOWFLAKE
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
2.5.2. 测试
1. 创建表结构
CREATE DATABASE IF NOT EXISTS database1;
USE database1;
DROP TABLE IF EXISTS `t_dict`;
CREATE TABLE `t_dict` (
`dict_id` bigint(20) NOT NULL COMMENT '字典id',
`type` varchar(50) NOT NULL COMMENT '字典类型',
`code` varchar(50) NOT NULL COMMENT '字典编码',
`value` varchar(50) NOT NULL COMMENT '字典值',
PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
CREATE DATABASE IF NOT EXISTS database2;
USE database2;
DROP TABLE IF EXISTS `t_dict`;
CREATE TABLE `t_dict` (
`dict_id` bigint(20) NOT NULL COMMENT '字典id',
`type` varchar(50) NOT NULL COMMENT '字典类型',
`code` varchar(50) NOT NULL COMMENT '字典编码',
`value` varchar(50) NOT NULL COMMENT '字典值',
PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
2. 创建实体类
package com.zjp.shadingjdbcdemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_dict")
public class TDict implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 字典id
*/
private Long dictId;
/**
* 字典类型
*/
private String type;
/**
* 字典编码
*/
private String code;
/**
* 字典值
*/
private String value;
}
3. 编写sevice层和mapper层
package com.zjp.shadingjdbcdemo.service;
import com.zjp.shadingjdbcdemo.entity.TDict;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ITDictService extends IService<TDict> {
}
package com.zjp.shadingjdbcdemo.service.impl;
import com.zjp.shadingjdbcdemo.entity.TDict;
import com.zjp.shadingjdbcdemo.mapper.TDictMapper;
import com.zjp.shadingjdbcdemo.service.ITDictService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class TDictServiceImpl extends ServiceImpl<TDictMapper, TDict> implements ITDictService {
}
package com.zjp.shadingjdbcdemo.mapper;
import com.zjp.shadingjdbcdemo.entity.TDict;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TDictMapper extends BaseMapper<TDict> {
}
4. 编写测试类
package com.zjp.shadingjdbcdemo;
import com.zjp.shadingjdbcdemo.entity.TDict;
import com.zjp.shadingjdbcdemo.service.ITDictService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class ShadingJdbcDemoTests {
@Autowired
private ITDictService itDictService;
@Test
public void testSaveDict() {
TDict dict = new TDict()
.setCode("001")
.setValue("男")
.setType("sex");
itDictService.save(dict);
}
}
7. 测试
运行发现两个库的 t_dict 表均插入成功。
2.6. 绑定表
指分片规则一致的主表和子表。例如:t_user表和t_user_item表,均按照user_id分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果SQL为:
SELECT i.*, o.* FROM t_user o JOIN t_user_item i ON o.id = i.user_id WHERE o.id IN (10, 11);
在不配置绑定表关系时,假设分片键user_id将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:
SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);
在配置绑定表关系后,路由的SQL应该为2条:
SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);
其中t_user在FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么t_user_item表的分片计算将会使用t_user的条件。故绑定表之间的分区键要完全相同。
2.6.1. 配置步骤
编写配置文件,指定绑定表:
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
sharding:
default-data-source-name: ds0
default-database-strategy:
inline:
sharding-column: id
algorithm-expression: ds$->{id % 2}
tables:
t_user:
actual-data-nodes: ds$->{0..1}.t_user_$->{1..2}
key-generator:
column: id
type: SNOWFLAKE
table-strategy:
inline:
sharding-column: id
algorithm-expression: t_user_$->{id % 2 + 1}
t_user_item:
actual-data-nodes: ds$->{0..1}.t_user_item_$->{1..2}
key-generator:
column: item_id
type: SNOWFLAKE
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_user_item_$->{user_id % 2 + 1}
binding-tables: # 绑定表规则列表
- t_user,t_user_item
props: # 属性配置
sql:
show: true #是否开启SQL显示,默认值: false
# executor.size: #工作线程数量,默认值: CPU核数
# max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
# check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
2.5.2. 测试
1. 创建表结构
CREATE DATABASE IF NOT EXISTS database1;
USE database1;
DROP TABLE IF EXISTS `t_user_item_1`;
DROP TABLE IF EXISTS `t_user_item_2`;
CREATE TABLE `t_user_item_1` (
`item_id` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_item_2` (
`item_id` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE DATABASE IF NOT EXISTS database2;
USE database2;
DROP TABLE IF EXISTS `t_user_item_1`;
DROP TABLE IF EXISTS `t_user_item_2`;
CREATE TABLE `t_user_item_1` (
`item_id` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_item_2` (
`item_id` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 创建实体类
package com.zjp.shadingjdbcdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
public class UserItemVo {
/**
* 用户编号
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String name;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户薪资
*/
private Double salary;
/**
* 用户生日
*/
private LocalDateTime birthday;
private Long itemId;
}
3. 编写mapper层
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zjp.shadingjdbcdemo.mapper.UserItemMapper">
<select id="getList" resultType="com.zjp.shadingjdbcdemo.entity.UserItemVo">
SELECT i.*, o.*
FROM t_user o
JOIN t_user_item i ON o.id = i.user_id
WHERE o.id IN (10, 11);
</select>
</mapper>
4. 编写测试类
package com.zjp.shadingjdbcdemo;
import com.zjp.shadingjdbcdemo.entity.UserItemVo;
import com.zjp.shadingjdbcdemo.service.UserItemService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
@Autowired
private UserItemService userItemService;
@Test
public void test() {
UserItemVo result = userItemService.selectList();
log.info("查询结果为:{}", result);
}
}
7. 测试
在注释掉spring.shardingsphere.sharding.binding-tables配置项时,日志为:
在配置 spring.shardingsphere.sharding.binding-tables配置项时,日志为:
本篇文章示例代码:GitHub - kerrsixy/sharding-jdbc-demo