SpringBoot 集成 Sharding-JDBC(一):数据分片

在深入探讨 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 Oracle SQL Server PostgreSQL DB2 MongoDB SequoiaDB MySQL Oracle SQLServer PostgreSQL 任何遵循 SQL92 标准的数据库 MySQL/PostgreSQL MySQL/PostgreSQL
连接数
应用语言 任意 Java 任意 任意
代码入侵 需要修改代码
性能 损耗略高 损耗低 损耗略高 损耗低
无中心化
静态入口
管理控制台 Mycat-web Sharding-UI Sharding-UI Sharding-UI
分库分表 单库多表/多库单表 ✔️ ✔️ ✔️
多租户方案 ✔️ -- -- --
读写分离 ✔️ ✔️ ✔️ ✔️
分片策略定制化 ✔️ ✔️ ✔️ ✔️
分布式主键 ✔️ ✔️ ✔️ ✔️
标准化事务接口 ✔️ ✔️ ✔️ ✔️
XA强一致事务 ✔️ ✔️ ✔️ ✔️
柔性事务 -- ✔️ ✔️ ✔️
配置动态化 开发中 ✔️ ✔️ ✔️
编排治理 开发中 ✔️ ✔️ ✔️
数据脱敏 -- ✔️ ✔️ ✔️
可视化链路追踪 -- ✔️ ✔️ ✔️
弹性伸缩 开发中 开发中 开发中 开发中
多节点操作 分页 去重 排序 分组 聚合 分页 去重 排序 分组 聚合 分页 去重 排序 分组 聚合 分页 去重 排序 分组 聚合
跨库关联 跨库 2 表 Join ER Join 基于 caltlet 的多表 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 驱动的增强,对应用程序来说是透明的,无需修改业务代码即可实现数据库分片。
  • 配置简单,通过配置文件或注解方式,可以方便地进行分片规则的配置。
  1. 高性能:
  • Sharding-JDBC 是基于 Java 的字节码增强技术,性能损耗较小,能够高效地处理高并发请求。
  • 支持并行执行多个分片上的查询,提高查询效率。
  1. 功能丰富:支持读写分离、分布式事务、数据加密。

1.6. Sharding-JDBC缺点

  1. 功能限制
  • 跨库操作存在限制,性能和效率可能受影响。
  • 不支持所有复杂的 SQL 语句,需要手动优化或改写。
  1. 维护成本
  • 分片后的数据库管理和维护复杂,需要更多运维工作。
  • 故障恢复面临更大挑战,需要完善的备份和恢复机制。
  1. 复杂性
  • 虽然配置相对简单,但对于复杂的分片规则和多表关联查询,配置和管理可能会变得复杂。
  • 对于初学者来说,理解和掌握 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为:

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条,它们呈现为笛卡尔积:

sql 复制代码
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条:

sql 复制代码
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用红色表示,灰色表示需要进一步拆分。

  1. 提取规则(Extraction Rules)
  • 作用:从解析生成的 AST 中提取关键信息,如表名、列名、条件等。
  • 实现:
    • 遍历 AST:通过递归或迭代的方式遍历 AST,提取所需的信息。
    • 信息提取:提取表名、列名、条件表达式等。
  • 示例:对于 SQL 语句 SELECT * FROM table WHERE id = 1;,提取规则会提取以下信息:
    • 表名:table
    • 条件:id = 1
  1. 填充规则(Filling Rules)
  • 作用:根据提取的关键信息和分片规则,填充路由信息。
  • 实现:
    • 分片键提取:从提取的信息中找到分片键。
    • 路由计算:根据分片规则计算出目标数据源和表。
  • 示例:假设分片规则是按 user_id 的奇偶性分片,对于 SQL 语句 SELECT * FROM order WHERE user_id = 1001;,填充规则会计算出目标表为 order_1。
  1. 优化规则(Optimization Rules)
  • 作用:对解析和填充后的 SQL 语句进行优化,提高执行效率。
  • 实现:
    • 子查询优化:将复杂的子查询转换为更高效的查询。
    • 索引优化:根据索引信息优化查询计划
    • 并行执行:将可以并行执行的查询拆分成多个子查询,提高执行速度。
  • 示例:对于 SQL 语句 SELECT * FROM order WHERE user_id IN (1001, 1002, 1003);,优化规则可能会将其拆分成多个子查询:
sql 复制代码
    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 时可能路由到多库(表)。
  • 示例:
sql 复制代码
SELECT * FROM t_order WHERE order_id IN (1, 2);

路由结果:

sql 复制代码
SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

3. 笛卡尔路由:

条件:非绑定表之间的关联查询。

特点:无法根据绑定表关系定位分片规则,需要拆解为笛卡尔积组合执行,性能较低。

示例:

sql 复制代码
SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

路由结果:

sql 复制代码
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。
  • 示例:
sql 复制代码
SELECT * FROM t_order WHERE good_prority IN (1, 10);

路由结果:

sql 复制代码
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 事务控制语句。
  • 示例:
sql 复制代码
SET autocommit=0;

路由结果:

sql 复制代码
SET autocommit=0; -- 在 t_order_0 上执行
SET autocommit=0; -- 在 t_order_1 上执行

3. 全实例路由:

  • 条件:处理 DCL 操作,如授权语句。
  • 示例:
sql 复制代码
CREATE USER customer@127.0.0.1 identified BY '123';

路由结果:

sql 复制代码
CREATE USER customer@127.0.0.1 identified BY '123'; -- 在所有实例上执行

4. 单播路由:

  • 条件:获取某一真实表信息的场景,仅需要从任意库中的任意真实表中获取数据。
  • 示例:
sql 复制代码
DESCRIBE t_order;

路由结果:

sql 复制代码
DESCRIBE t_order_0; -- 任意选择一个真实表执行

5. 阻断路由:

  • 条件:屏蔽 SQL 对数据库的操作,如 USE 命令。
  • 示例:
sql 复制代码
USE order_db;

路由结果:

sql 复制代码
-- 不执行该命令

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. 流式归并
  • 特点:逐条获取数据,减少内存消耗。
  • 适用场景:适用于大多数查询,尤其是大数据量的查询。
  • 类型:遍历归并、排序归并、流式分组归并。
  1. 内存归并
  • 特点:将所有数据加载到内存中进行处理。
  • 适用场景:适用于分组项与排序项不一致的查询。
  • 类型:内存分组归并。
  1. 装饰者归并
  • 特点:在流式归并和内存归并的基础上进行功能增强。
  • 类型:分页归并、聚合归并。
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 的依赖:

XML 复制代码
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>

​2.3.2. 添加配置文件

注意:

  1. 多个数据源的数据库连接池可以不同。
  2. 多个数据源的数据库驱动类型必须相同。
2.3.2.1. 精确分片

对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。

1. 编写yaml文件

Crystal 复制代码
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 方法。

java 复制代码
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) {
        // 自定义分片规则
        Long value = preciseShardingValue.getValue();
        String dbName = "ds" + (value % 2);
        if (!collection.contains(dbName)) {
            throw new UnsupportedOperationException("数据源" + dbName + "不存在");
        }
        return dbName;
    }
}

3. 添加分表规则

实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法。

java 复制代码
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) {
        // 自定义分表规则
        Long value = preciseShardingValue.getValue();
        String dbName = "t_user_" + (value % 2 + 1);
        if (!collection.contains(dbName)) {
            throw new UnsupportedOperationException("数据源" + dbName + "不存在");
        }
        return dbName;
    }
}
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文件

Crystal 复制代码
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. 添加分库范围查询和精确查询规则

  1. 范围查询:实现 RangeShardingAlgorithm 接口,重写 doSharding 方法。
  2. 精确查询:实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法实现。

可以用两个类分别实现,也可以一个类同时实现。

Crystal 复制代码
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           一切配置中可用的数据源集合:ds1,ds2    从这里获得[ds$->{1..2}.t_user_$->{1..2}]
     * @param preciseShardingValue 数据库对应分片键的值 ---> id
     * @return 返回一个具体的数据源名称
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {

        // 获取逻辑表名称
        String logicTableName = preciseShardingValue.getLogicTableName();

        // 1.获取分片键名称-->即数据库中列的名称 user_id
        String columnName = preciseShardingValue.getColumnName();

        // 2.获取分片键的值
        Long value = preciseShardingValue.getValue();

        // 3.根据分片键的值,计算出对应的数据源名称
        if (value < 10000000L) {
            return "ds0";
        } else {
            return "ds1";
        }
    }

    /**
     * 范围分片
     *
     * @param collection         数据源集合
     * @param rangeShardingValue 分片参数
     * @return 直接返回源
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
        // 获取逻辑表名
        String logicTableName = rangeShardingValue.getLogicTableName();
        // 1.获取分片键名
        String shardingColumn = rangeShardingValue.getColumnName();
        // 2.获取分片键值范围
        Range<Long> valueRange = rangeShardingValue.getValueRange();
        // 3.根据分片键值范围,进行范围匹配
        if (valueRange.hasLowerBound()) { //存在下限
            Long l = valueRange.lowerEndpoint();
        }
        if (valueRange.hasUpperBound()) { //存在上限
            Long u = valueRange.upperEndpoint();
        }
        return collection;
    }
}

3. 添加分表规则

  1. 范围查询:实现 RangeShardingAlgorithm 接口,重写 doSharding 方法。
  2. 精确查询:实现 PreciseShardingAlgorithm 接口,重写 doSharding 方法。

可以用两个类分别实现,也可以一个类同时实现。

java 复制代码
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> {
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        //获取逻辑表名称
        String logicTableName = preciseShardingValue.getLogicTableName();

        //1.获取分片键名称-->即数据库中列的名称 id
        String columnName = preciseShardingValue.getColumnName();

        //2.获取分片键的值
        Long value = preciseShardingValue.getValue();

        //3.根据分片键的值,计算出对应的数据源名称
        if (value < 10000000L) {
            return "t_user_1";
        } else {
            return "t_user_2";
        }
    }

    /**
     * 范围分片
     *
     * @param collection         数据源集合
     * @param rangeShardingValue 分片参数
     * @return 直接返回源
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
        //逻辑表名称
        String logicTableName = rangeShardingValue.getLogicTableName();
        return Arrays.asList(logicTableName + "_1", logicTableName + "_2");
    }
}
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。

Crystal 复制代码
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支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

1. 编写yaml文件

Crystal 复制代码
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 方法。

java 复制代码
package com.zjp.shadingjdbcdemo.strategy.database;

import com.google.common.collect.Lists;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;

import java.util.Collection;
import java.util.List;

public class DatabaseComplexAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
    /**
     * @param collection               数据源集合
     * @param complexKeysShardingValue 分片键的值集合
     * @return 需要查找的数据源集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
        // 自定义复合分片规则
        // 获取age的值
        Collection<Integer> ageValues = complexKeysShardingValue.getColumnNameAndShardingValuesMap().get("age");
        List<String> dbs = Lists.newArrayList();
        // 通过age取模
        ageValues.forEach(item -> {
            String dbName = "ds" + ((item + 3) % 2);
            dbs.add(dbName);
        });
        return dbs;
    }
}

3. 添加分表规则

实现 ComplexKeysShardingAlgorithm 接口,重写 doSharding 方法。

java 复制代码
package com.zjp.shadingjdbcdemo.strategy.table;

import com.google.common.collect.Lists;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;

import java.util.Collection;
import java.util.List;

public class TableComplexAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {

    @Override
    public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
        // 自定义复合分片规则
        // 获取age的值
        Collection<Integer> ageValues = complexKeysShardingValue.getColumnNameAndShardingValuesMap().get("age");
        List<String> dbs = Lists.newArrayList();
        // 通过age取模
        ageValues.forEach(item -> {
            String dbName = "t_user_" + ((item + 3) % 2 + 1);
            dbs.add(dbName);
        });
        return dbs;
    }
}
2.3.2.5. Hint分片

用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。

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

对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。

1. 编写yaml文件

Crystal 复制代码
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 方法。

java 复制代码
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> {
    @Override
    public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
        Collection<Integer> values = hintShardingValue.getValues();
        HashSet<String> dbNameSet = new HashSet<>();
        for (Integer value : values) {
            dbNameSet.add("ds" + value % 2);
        }
        return dbNameSet;
    }
}

3. 添加分表规则

实现 HintShardingAlgorithm 接口,重写 doSharding 方法。

java 复制代码
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.Arrays;
import java.util.Collection;

public class TableHintAlgorithm implements HintShardingAlgorithm<Integer> {
    /**
     * 强制路由
     *
     * @param collection        数据源集合
     * @param hintShardingValue 分片参数
     * @return 数据库集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
        String logicTableName = hintShardingValue.getLogicTableName();
        String dbName = logicTableName + "_" + hintShardingValue.getValues().toArray()[0];
        return Arrays.asList(dbName);
    }
}
2.3.2.6. 不分片

对应NoneShardingStrategy。不分片的策略。

Crystal 复制代码
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. 引入依赖

XML 复制代码
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.17</version>
</dependency>

2. 编写 BaseTypeHandler 的实现类:

  • LocalDate适配:
Crystal 复制代码
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适配:
Crystal 复制代码
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位 |

源码如下图:

通过源码可以看出,雪花算法可以额外配置三个参数:

  1. worker.id:工作机器标识位,表示一个唯一的工作进程id,取值范围(整数):0(包含)~1024(不包含),默认值为0。
  2. max.vibration.offset:序列位,同一毫秒内生成不同的ID,取值范围(整数):0(包含)~4095(包含),默认值为1。
  3. max.tolerate.time.difference.milliseconds:最大容忍的时钟回拨毫秒数,默认值为10。如下图源码所示,最后一次生成主键的时间 lastMilliseconds 与 当前时间currentMilliseconds 做比较,如果 lastMilliseconds > currentMilliseconds则意味着时钟回调了。那么接着判断两个时间的差值(timeDifferenceMilliseconds)是否在设置的最大容忍时间阈值 max.tolerate.time.difference.milliseconds内,在阈值内则线程休眠差值时间 Thread.sleep(timeDifferenceMilliseconds),否则大于差值直接报异常。

配置示例:

Crystal 复制代码
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. 创建数据表

sql 复制代码
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 复制代码
<?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配置部分)

Crystal 复制代码
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. 创建实体类

注意:

  1. @TableName 注解指定的表为虚拟表 t_user。
  2. @TableId 的 type 属性为 IdType.AUTO 则采用 Sharding-JDBC 数据源生成策略,否则采用 MyBatis Plus 主键生成策略。
  3. 主键的数据类型为包装类,否则主键赋值失败,为默认值0。
java 复制代码
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层

java 复制代码
package com.zjp.shadingjdbcdemo.service;

import com.zjp.shadingjdbcdemo.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {
}
java 复制代码
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 {
}
java 复制代码
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. 编写测试类

java 复制代码
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和雪花算法。

  1. 以UUID为例,通过实现 ShardingKeyGenerator 接口,重新 getType 方法指定配置文件的文件名称和重新 generateKey 方法自定义主键生成策略来实现主键生成。

2.4.2. 自定义主键生成策略步骤

1. 编写 ShardingKeyGenerator 实现类

java 复制代码
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文件,文件内容为自定义主键生成策略类的全限定名。

java 复制代码
com.zjp.shadingjdbcdemo.strategy.keygen.MyShardingKeyGenerator

3. 配置文件指定自定义主键生成策略

Crystal 复制代码
spring:
    shardingsphere:
        sharding:
            tables:
                key-generator:
                    column: id # 主键ID
                    type: MyKey  # 自定义主键生成策略
  1. 如果是MyBatis Plus,则需要将主键生成策略换成IdType.AUTO
java 复制代码
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;
}
  1. 编写测试类
java 复制代码
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);
    }
}
  1. 启动测试

通过日志和数据库可以看出主键生成成功。

2.5. 广播表

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

2.5.1. 配置步骤

编写配置文件,指定广播表:

Crystal 复制代码
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. 创建表结构

sql 复制代码
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. 创建实体类

sql 复制代码
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层

java 复制代码
package com.zjp.shadingjdbcdemo.service;

import com.zjp.shadingjdbcdemo.entity.TDict;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ITDictService extends IService<TDict> {
}
java 复制代码
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 {
}
java 复制代码
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. 编写测试类

java 复制代码
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为:

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条,它们呈现为笛卡尔积:

sql 复制代码
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条:

sql 复制代码
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. 配置步骤

编写配置文件,指定绑定表:

Crystal 复制代码
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 % 13 % 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 % 13 % 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. 创建表结构

sql 复制代码
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. 创建实体类

java 复制代码
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层

java 复制代码
<?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. 编写测试类

java 复制代码
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配置项时,日志为:

相关推荐
这个DBA有点耶5 小时前
DBA的AI助手:向量检索与NL2SQL入门
数据库·人工智能·postgresql·学习方法·dba
㳺三才人子5 小时前
初探 Flask
后端·python·flask·html
星栈独行5 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Lei活在当下5 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.5 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
basketball6166 小时前
SQL 常用数据格式化操作方法总结
数据库·sql
tongluowan0076 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶6 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
TE-茶叶蛋6 小时前
数据库-引用完整性(referential integrity)
数据库