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配置项时,日志为:

相关推荐
程序员黄同学1 小时前
如何使用 Python 连接 MySQL 数据库?
数据库·python·mysql
运维&陈同学1 小时前
【Elasticsearch05】企业级日志分析系统ELK之集群工作原理
运维·开发语言·后端·python·elasticsearch·自动化·jenkins·哈希算法
新手小袁_J2 小时前
实现Python将csv数据导入到Neo4j
数据库·python·neo4j·《我是刑警》·python连接neo4j·python导入csv·csv数据集导入neo4j
ZHOUPUYU2 小时前
最新 neo4j 5.26版本下载安装配置步骤【附安装包】
java·后端·jdk·nosql·数据库开发·neo4j·图形数据库
シ風箏2 小时前
Neo4j【环境部署 02】图形数据库Neo4j在Linux系统ARM架构下的安装使用
linux·数据库·arm·neo4j
张声录12 小时前
【ETCD】【实操篇(四)】etcd常见问题快问快答FAQ
数据库·etcd
Q_19284999063 小时前
基于Spring Boot的找律师系统
java·spring boot·后端
ZVAyIVqt0UFji4 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
CherishTaoTao4 小时前
sqlite基础
数据库·oracle·sqlite
谢家小布柔4 小时前
Git图形界面以及idea中集合Git使用
java·git