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

news2024/11/23 17:24:26

在深入探讨 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 的区别

MycatSharding-JDBCSharding-ProxySharding-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 驱动的增强,对应用程序来说是透明的,无需修改业务代码即可实现数据库分片。
  • 配置简单,通过配置文件或注解方式,可以方便地进行分片规则的配置。

2. 高性能:

  • Sharding-JDBC 是基于 Java 的字节码增强技术,性能损耗较小,能够高效地处理高并发请求。
  • 支持并行执行多个分片上的查询,提高查询效率。

3. 功能丰富:支持读写分离、分布式事务、数据加密。

1.6. Sharding-JDBC缺点

1. 功能限制

  • 跨库操作存在限制,性能和效率可能受影响。
  • 不支持所有复杂的 SQL 语句,需要手动优化或改写。

2. 维护成本

  • 分片后的数据库管理和维护复杂,需要更多运维工作。
  • 故障恢复面临更大挑战,需要完善的备份和恢复机制。

3. 复杂性

  • 虽然配置相对简单,但对于复杂的分片规则和多表关联查询,配置和管理可能会变得复杂。
  • 对于初学者来说,理解和掌握 Sharding-JDBC 的全部功能和配置可能需要一定的时间。

2. 数据分片

2.1. 核心概念

  • 逻辑表:水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。
  • 真实表:在分片的数据库中真实存在的物理表。即上个示例中的t_order_0到t_order_9。

  • 数据节点:数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0。

  • 绑定表:指分片规则一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果SQL为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在不配置绑定表关系时,假设分片键order_id将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

 在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

其中t_order在FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么t_order_item表的分片计算将会使用t_order的条件。故绑定表之间的分区键要完全相同。

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

2.2. Sharding-JDBC 数据分片执行原理

2.2.1. SQL解析

1. 解析规则(Parsing Rules)

  • 作用:定义如何将 SQL 语句解析成抽象语法树(AST)。
  • 实现:
    • 词法分析:将 SQL 语句分解成一个个的词法单元(Token)。
    • 语法分析:将词法单元组合成抽象语法树(AST)。
  • 示例:对于 SQL 语句 SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18;,解析规则会生成如下的 AST:

为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。 

2. 提取规则(Extraction Rules)

  • 作用:从解析生成的 AST 中提取关键信息,如表名、列名、条件等。
  • 实现:
    • 遍历 AST:通过递归或迭代的方式遍历 AST,提取所需的信息。
    • 信息提取:提取表名、列名、条件表达式等。
  • 示例:对于 SQL 语句 SELECT * FROM table WHERE id = 1;,提取规则会提取以下信息:
    • 表名:table
    • 条件:id = 1

3. 填充规则(Filling Rules)

  • 作用:根据提取的关键信息和分片规则,填充路由信息。
  • 实现:
    • 分片键提取:从提取的信息中找到分片键。
    • 路由计算:根据分片规则计算出目标数据源和表。
  • 示例:假设分片规则是按 user_id 的奇偶性分片,对于 SQL 语句 SELECT * FROM order WHERE user_id = 1001;,填充规则会计算出目标表为 order_1。

4. 优化规则(Optimization Rules)

  • 作用:对解析和填充后的 SQL 语句进行优化,提高执行效率。
  • 实现:
    • 子查询优化:将复杂的子查询转换为更高效的查询。
    • 索引优化:根据索引信息优化查询计划
    • 并行执行:将可以并行执行的查询拆分成多个子查询,提高执行速度。
  • 示例:对于 SQL 语句 SELECT * FROM order WHERE user_id IN (1001, 1002, 1003);,优化规则可能会将其拆分成多个子查询:
    SELECT * FROM order_1 WHERE user_id = 1001;
    SELECT * FROM order_0 WHERE user_id = 1002;
    SELECT * FROM order_1 WHERE user_id = 1003;

 2.2.2. SQL路由

根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN)。 不携带分片键的SQL则采用广播路由。路由引擎的整体结构划分如下图。

2.2.2.1. 分片路由

分片路由根据分片键进行路由,将 SQL 请求路由到特定的数据库和表中。根据分片键的不同操作符,分片路由可以进一步划分为以下几种类型:

1. 直接路由:

  • 条件:通过 Hint API 直接指定路由至库表,且只分库不分表。
  • 特点:避免 SQL 解析和结果归并,兼容性强,支持复杂 SQL。

2. 标准路由:

  • 条件:不包含关联查询或仅包含绑定表之间的关联查询。
  • 特点:分片键操作符为等号时路由到单库(表),操作符为 BETWEEN 或 IN 时可能路由到多库(表)。
  • 示例:
SELECT * FROM t_order WHERE order_id IN (1, 2);

路由结果:

SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

3. 笛卡尔路由:

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

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

示例:

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

路由结果:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);    
2.2.2.2. 广播路由

广播路由将 SQL 请求路由到所有配置的数据源或表中,适用于不携带分片键的 SQL。根据 SQL 类型,广播路由可以进一步划分为以下几种类型:

1. 全库表路由:

  • 条件:处理对数据库中与其逻辑表相关的所有真实表的操作,如不带分片键的 DQL 和 DML,以及 DDL。
  • 示例:
SELECT * FROM t_order WHERE good_prority IN (1, 10);

路由结果:

SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);

 2. 全库路由:

  • 条件:处理对数据库的操作,如 SET 类型的数据库管理命令和 TCL 事务控制语句。
  • 示例:
SET autocommit=0;

路由结果:

SET autocommit=0; -- 在 t_order_0 上执行
SET autocommit=0; -- 在 t_order_1 上执行

3. 全实例路由:

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

路由结果:

CREATE USER customer@127.0.0.1 identified BY '123'; -- 在所有实例上执行

4. 单播路由: 

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

路由结果:

DESCRIBE t_order_0; -- 任意选择一个真实表执行

 5. 阻断路由:

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

 路由结果:

-- 不执行该命令

2.2.3.  SQL改写

SQL 改写是将面向逻辑库与逻辑表编写的 SQL 转换为在真实数据库中可执行的 SQL。它主要包括正确性改写和优化改写两部分。

2.2.3.1. 正确性改写

将面向逻辑库与逻辑表编写的 SQL 转换为在真实数据库中可以正确执行的 SQL。

场景

逻辑 SQL

改写后的 SQL

简单场景

SELECT order_id

FROM t_order

WHERE order_id=1;

SELECT order_id

FROM t_order_1

WHERE order_id=1;

复杂场景

SELECT order_id

FROM t_order

WHERE order_id=1

AND remarks=' t_order xxx';

SELECT order_id

FROM t_order_1

WHERE order_id=1

AND remarks=' t_order xxx';

表名作为字段标识符

SELECT t_order.order_id

FROM t_order

WHERE t_order.order_id=1

AND remarks=' t_order xxx';

SELECT t_order_1.order_id

FROM t_order_1

WHERE t_order_1.order_id=1

AND remarks=' t_order xxx';

表别名

SELECT t_order.order_id

FROM t_order AS t_order

WHERE t_order.order_id=1

AND remarks=' t_order xxx';

SELECT t_order.order_id

FROM t_order_1 AS t_order

WHERE t_order.order_id=1

AND remarks=' t_order xxx';

2.2.3.1.1. 标识符改写

标识符

描述

表名称

将逻辑表名称改写为真实表名称。

索引

名称

在某些数据库中,索引以表为维度创建,可以重名;在另一些数据库中,索引以数据库为维度创建,要求名称唯一。

Schema

名称

将逻辑 Schema 替换为真实 Schema。

2.2.3.1.2. 补列

场景

逻辑 SQL

改写后的 SQL

GROUP BY 和

ORDER BY

SELECT order_id

FROM t_order

ORDER BY user_id;

SELECT order_id, user_id AS ORDER_BY_DERIVED_0

FROM t_order

ORDER BY user_id;

复杂场景

SELECT o.*

FROM t_order o, t_order_item i

WHERE o.order_id=i.order_id

ORDER BY user_id, order_item_id;

SELECT o.*, order_item_id AS ORDER_BY_DERIVED_0

FROM t_order o, t_order_item i

WHERE o.order_id=i.order_id

ORDER BY user_id, order_item_id;

AVG 聚合函数

SELECT AVG(price)

FROM t_order

WHERE user_id=1;

SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_SUM_0

FROM t_order

WHERE user_id=1;

自增主键

INSERT INTO t_order (field1,field2)

VALUES (10, 1);

INSERT INTO t_order (field1,field2, order_id)

VALUES (10, 1, xxxxx);

2.2.3.1.3. 分页修正

场景

逻辑 SQL

改写后的 SQL

分页查询

SELECT score

FROM t_score

ORDER BY score DESC

LIMIT 1, 2;

SELECT score FROM t_score_0 ORDER BY score DESC LIMIT 0, 3;

SELECT score FROM t_score_1 ORDER BY score DESC LIMIT 0, 3;

 2.2.3.1.4. 批量拆分

场景

逻辑 SQL

改写后的 SQL(order_id 按奇偶分片)

批量插入

INSERT INTO t_order (order_id, xxx)

VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx');

INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx');

INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');

IN

查询

SELECT *

FROM t_order

WHERE order_id IN (1, 2, 3);

SELECT * FROM t_order_0 WHERE order_id IN (2);

SELECT * FROM t_order_1 WHERE order_id IN (1, 3);

2.2.3.2. 优化改写

类型

描述

单节点优化

如果 SQL 路由到单个节点,无需进行额外的改写,以减少不必要的开销。

流式归并优化

为包含 GROUP BY 的 SQL 增加 ORDER BY 以实现流式归并,提高性能。

2.2.4. SQL执行

 2.2.4.1. 连接模式

ShardingSphere 提出了两种连接模式:内存限制模式(MEMORY_STRICTLY) 和 连接限制模式(CONNECTION_STRICTLY)。

1. 内存限制模式(MEMORY_STRICTLY)

  • 特点:不对数据库连接数量做限制。
  • 适用场景:适合 OLAP 操作,通过多线程并发处理多个表的查询,提升执行效率。
  • 优点:最大化执行效率,优先选择流式归并,节省内存。
  • 缺点:可能占用大量数据库连接资源。

2. 连接限制模式(CONNECTION_STRICTLY)

  • 特点:严格控制数据库连接数量。
  • 适用场景:适合 OLTP 操作,通常带有分片键,路由到单一的分片。
  • 优点:防止过多占用数据库连接资源,保证在线系统数据库资源的充分利用。
  • 缺点:可能牺牲一定的执行效率,采用内存归并。
2.2.4.2. 自动化执行引擎

ShardingSphere 的自动化执行引擎在内部消化了连接模式的概念,用户无需手动选择模式,执行引擎会根据当前场景自动选择最优的执行方案。

1.  准备阶段

  • 结果集分组:根据 maxConnectionSizePerQuery 配置项,将 SQL 的路由结果按数据源名称分组。
  • 连接模式计算:
    • 计算每个数据库实例在 maxConnectionSizePerQuery 允许范围内,每个连接需要执行的 SQL 路由结果组。
    • 如果一个连接需要执行的请求数量大于1,采用内存归并;否则,采用流式归并。

  • 执行单元创建:创建执行单元时,以原子性方式一次性获取所需的所有数据库连接,避免死锁

2. 执行阶段

  • 分组执行:将准备阶段生成的执行单元分组下发至底层并发执行引擎,并发送执行事件。
  • 归并结果集生成:根据连接模式生成内存归并结果集或流式归并结果集,并传递至结果归并引擎。
2.2.4.3. 关键优化
  • 避免锁定:对于只需要获取1个数据库连接的操作,不进行锁定,提升并发效率。
  • 资源锁定:仅在内存限制模式下进行资源锁定,连接限制模式下不会产生死锁。

2.2.5. 结果归并

ShardingSphere 支持的结果归并类型从功能上分为五种:遍历、排序、分组、分页和聚合。从结构上划分,可分为流式归并、内存归并和装饰者归并。

 2.2.5.1. 结构划分

1. 流式归并

  • 特点:逐条获取数据,减少内存消耗。
  • 适用场景:适用于大多数查询,尤其是大数据量的查询。
  • 类型:遍历归并、排序归并、流式分组归并。

2. 内存归并

  • 特点:将所有数据加载到内存中进行处理。
  • 适用场景:适用于分组项与排序项不一致的查询。
  • 类型:内存分组归并。

3. 装饰者归并

  • 特点:在流式归并和内存归并的基础上进行功能增强。
  • 类型:分页归并、聚合归并。
2.2.5.2. 功能划分

1. 遍历归并

  • 特点:最简单的归并方式,将多个数据结果集合并为一个单向链表。
  • 实现:遍历完当前数据结果集后,链表元素后移一位,继续遍历下一个数据结果集。

2. 排序归并

  • 特点:适用于包含 ORDER BY 语句的查询。
  • 实现:使用优先级队列对多个有序结果集进行归并排序。
  • 示例:
    • 假设有3个数据结果集,每个结果集已经根据分数排序。
    • 使用优先级队列将每个结果集的当前数据值进行排序。
    • 每次 next 调用时,弹出队列首位的数据值,并将游标下移一位,重新加入队列。

3.  分组归并

  • 特点:分为流式分组归并和内存分组归并。
  • 流式分组归并:
    • 适用场景:分组项与排序项一致。
    • 实现:一次将多个数据结果集中分组项相同的数据全数取出,并进行聚合计算。
  • 内存分组归并:
    • 适用场景:分组项与排序项不一致。
    • 实现:将所有数据加载到内存中进行分组和聚合。

 4. 聚合归并

  • 特点:处理聚合函数,如 MAX、MIN、SUM、COUNT 和 AVG。
  • 实现:
    • 比较类型:MAX 和 MIN,直接返回最大或最小值。
    • 累加类型:SUM 和 COUNT,将同组的数据进行累加。
    • 求平均值:AVG,通过 SUM 和 COUNT 计算。

 5. 分页归并

  • 特点:在其他归并类型基础上追加分页功能。
  • 实现:通过 LIMIT 语句进行分页,但不会将大量无意义的数据加载到内存中。
  • 示例:
    • 使用 ID 进行分页,例如:SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;
    • 记录上次查询结果的最后一条记录的 ID 进行下一页查询,例如:SELECT * FROM t_order WHERE id > 10000000 LIMIT 10;

2.3. 使用步骤

2.3.1. 引入依赖

在 pom.xml 文件中添加 Sharding-JDBC 的依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>

​2.3.2. 添加配置文件

注意:

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

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

1. 编写yaml文件

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        standard: # 用于单分片键的标准分片场景
          # 添加数据分库字段(根据字段插入数据到那个表)
          sharding-column: id
          # 精确分片
          precise-algorithm-class-name: ${common.algorithm.db}
      # 分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            standard: # 用于单分片键的标准分片场景
              # 分片列名称
              sharding-column: id
              # 精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
              precise-algorithm-class-name: ${common.algorithm.tb}
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

# 配置分片策略
common:
  algorithm:
    db: com.zjp.shadingjdbcdemo.strategy.database.DatabasePreciseAlgorithm
    tb: com.zjp.shadingjdbcdemo.strategy.table.TablePreciseAlgorithm

2. 添加分库规则

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

package com.zjp.shadingjdbcdemo.strategy.database;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

public class DatabasePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
    /**
     * 精确分片
     *
     * @param collection           数据源集合
     * @param preciseShardingValue 分片参数
     * @return 数据库
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        // 自定义分片方法
        // 1.获取分片键的值
        Long value = preciseShardingValue.getValue();
        // 2.获取逻辑表名
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 3.获取数据库名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 4.根据分片键的值,计算出对应的数据源名称
        return dbNames.get((int) (value % 2));
    }
}

3. 添加分表规则

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

package com.zjp.shadingjdbcdemo.strategy.table;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

public class TablePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
    /**
     * 精确分片
     *
     * @param collection           数据源集合
     * @param preciseShardingValue 分片参数
     * @return 数据库
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        // 自定义分片方法
        // 1.获取分片键值
        Long value = preciseShardingValue.getValue();
        // 2.获取逻辑表名
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 3.获取真实表总数
        int size = collection.size();
        // 4.计算表名
        int v;
        long n;
        do {
            v = (int) (Math.pow(2, Math.ceil(Math.log(size) / Math.log(2))) - 1);
            n = value & v;
        } while (n >= size);
        // 5.获取真实表名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 6.返回真实表
        return dbNames.get((int) n);
    }
}
2.3.2.2. 范围分片

对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用。

标准分片策略(StandardShardingStrategy)提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

1. 编写yaml文件

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        standard: # 用于单分片键的标准分片场景
          # 添加数据分库字段(根据字段插入数据到那个表)
          sharding-column: id
          # 精确分片
          precise-algorithm-class-name: ${common.algorithm.db}
          # 范围分片
          range-algorithm-class-name: ${common.algorithm.db}
      #分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            standard: # 用于单分片键的标准分片场景
              # 分片列名称
              sharding-column: id
              # 精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
              precise-algorithm-class-name: ${common.algorithm.tb}
              # 范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
              range-algorithm-class-name: ${common.algorithm.tb}
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

# 配置分片策略
common:
  algorithm:
    db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseRangeAlgorithm
    tb: com.zjp.shadingjdbcdemo.strategy.table.TableRangeAlgorithm

2. 添加分库范围查询和精确查询规则

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

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

package com.zjp.shadingjdbcdemo.strategy.database;

import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;

import java.util.Collection;

public class DatabaseRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
    /**
     * 精确分片
     *
     * @param collection           数据源集合
     * @param preciseShardingValue 分片参数
     * @return 数据库
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        // 自定义分片方法
        // 1.获取分片键的值
        Long value = preciseShardingValue.getValue();
        // 2.获取逻辑表名
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 3.获取数据库名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 4.根据分片键的值,计算出对应的数据源名称
        return dbNames.get((int) (value % 2));
    }

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

3. 添加分表规则

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

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

package com.zjp.shadingjdbcdemo.strategy.table;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;

import java.util.Arrays;
import java.util.Collection;

public class TableRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
    /**
     * 精确分片
     *
     * @param collection           数据源集合
     * @param preciseShardingValue 分片参数
     * @return 数据库
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        // 自定义分片方法
        // 1.获取分片键值
        Long value = preciseShardingValue.getValue();
        // 2.获取逻辑表名
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 3.获取真实表总数
        int size = collection.size();
        // 4.计算表名
        long n;
        int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
        do {
            n = value & v;
            v = v / 2;
        } while (n >= size);
        // 5.获取真实表名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 6.返回真实表
        return dbNames.get((int) n);
    }

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

对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 2} 表示t_user表根据u_id模2,而分成2张表,表名称为t_user_0到t_user_1。

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        inline: # 行表达式分片策略
          # 添加数据分库字段(根据字段插入数据到那个表)
          sharding-column: id
          # 分片算法表达式 => 通过id取余
          algorithm-expression: ds$->{id % 2}
      # 分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            inline: # 行表达式分片策略
              # 分片列名称
              sharding-column: id
              # 分片算法表达式 => 通过id取余
              algorithm-expression: t_user_$->{id % 2 + 1}
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
2.3.2.4. 复合分片

用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。

复合分片策略(ComplexShardingStrategy)提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

注意:

分片键只支持Integer类型,虽然能传其他类型,但是值是无法处理的。

1. 编写yaml文件

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        complex: #用于多分片键的复合分片场景
          # 添加数据分库字段(根据字段插入数据到那个表)
          sharding-columns: id,age
          algorithm-class-name: ${common.algorithm.db}
      #分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            complex: #用于多分片键的复合分片场景
              sharding-columns: id,age
              algorithm-class-name: ${common.algorithm.tb}
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

# 配置分片策略
common:
  algorithm:
    db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseComplexAlgorithm
    tb: com.zjp.shadingjdbcdemo.strategy.table.TableComplexAlgorithm

2. 添加分库范围查询和精确查询规则

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

package com.zjp.shadingjdbcdemo.strategy.database;

import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class DatabaseComplexAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
    /**
     * 复合分片
     *
     * @param collection               数据源集合
     * @param complexKeysShardingValue 分片键的值集合
     * @return 需要查找的数据源集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
        // 自定义复合分片规则
        // 1.获取分片键的值
        // 等于号 in 的值
        Map<String, Collection<Integer>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        // 实现  >,>=, <=,<  和 BETWEEN AND 等操作
        Map<String, Range<Integer>> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
        // 2. 获取逻辑表名
        String logicTableName = complexKeysShardingValue.getLogicTableName();
        // 获取age的值
        Collection<Integer> ageValues = columnNameAndShardingValuesMap.get("age");
        Range<Integer> ageRange = columnNameAndRangeValuesMap.get("age");
        // 获取id的值
        Collection<Integer> idValues = columnNameAndShardingValuesMap.get("id");
        Range<Integer> idRange = columnNameAndRangeValuesMap.get("id");
        // 3.获取数据库名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 4.根据分片键的值,计算出对应的数据源名称
        if (CollectionUtils.isNotEmpty(ageValues)) {
            // 如果 ageValues 不为空,根据 age 值对 dbNames 进行取模操作
            return ageValues.stream()
                    .map(age -> dbNames.get(age % 2))
                    .collect(Collectors.toSet());
        } else if (ageRange != null) {
            // 如果 ageRange 不为空,根据 age 范围判断属于哪个分区
            int numPartitions = dbNames.size();
            int maxAge = 150;
            int partitionSize = (maxAge + 1) / numPartitions;

            // 遍历每个分区
            Set<String> resultDbs = new HashSet<>();
            for (int i = 0; i < numPartitions; i++) {
                int start = i * partitionSize;
                int end = (i + 1) * partitionSize - 1;
                if (i == numPartitions - 1) {
                    end = maxAge;
                }
                try {
                    // 检查 ageRange 是否与当前分区范围有交集,如果两个区间没有交集,该方法将抛出IllegalArgumentException。
                    ageRange.intersection(Range.closed(start, end));
                    resultDbs.add(dbNames.get(i));
                } catch (IllegalArgumentException e) {
                }
            }
            return resultDbs;
        }

        // 默认情况下返回 collection
        return collection;
    }
}

3. 添加分表规则

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

package com.zjp.shadingjdbcdemo.strategy.table;

import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class TableRangeAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
    /**
     * 精确分片
     *
     * @param collection           数据源集合
     * @param preciseShardingValue 分片参数
     * @return 数据库
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        // 自定义分片方法
        // 1.获取分片键值
        Long value = preciseShardingValue.getValue();
        // 2.获取逻辑表名
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 3.获取真实表总数
        int size = collection.size();
        // 4.计算表名
        long n;
        int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
        do {
            n = value & v;
            v = v / 2;
        } while (n >= size);
        // 5.获取真实表名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 6.返回真实表
        return dbNames.get((int) n);
    }

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

拓展:

Range 方法:

方法名

参数

返回值

说明

是否静态

closed(T lower, T upper)

T lower, T upper

Range<T>

创建一个闭区间 [lower, upper],包括下界和上界。

open(T lower, T upper)

T lower, T upper

Range<T>

创建一个开区间 (lower, upper),不包括下界和上界。

closedOpen(T lower, T upper)

T lower, T upper

Range<T>

创建一个半闭区间 [lower, upper),包括下界但不包括上界。

openClosed(T lower, T upper)

T lower, T upper

Range<T>

创建一个半闭区间 (lower, upper],不包括下界但包括上界。

greaterThan(T lower)

T lower

Range<T>

创建一个大于给定值的区间 (lower, +∞)。

lessThan(T upper)

T upper

Range<T>

创建一个小于给定值的区间 (-∞, upper)。

atLeast(T lower)

T lower

Range<T>

创建一个大于或等于给定值的区间 [lower, +∞)。

atMost(T upper)

T upper

Range<T>

创建一个小于或等于给定值的区间 (-∞, upper]。

all()

Range<T>

创建一个包含所有可能值的区间 (-∞, +∞)。

contains(T value)

T value

boolean

检查区间是否包含给定值。

encloses(Range<T> other)

Range<T> other

boolean

检查当前区间是否完全包含另一个区间。

isConnected(Range<T> other)

Range<T> other

boolean

检查两个区间是否有交集或相邻。

intersection(Range<T> other)

Range<T> other

Range<T>

返回两个区间的交集。如果两个区间没有交集,该方法将抛出IllegalArgumentException。

span(Range<T> other)

Range<T> other

Range<T>

返回两个区间的并集。

hasLowerBound()

boolean

检查区间是否有下界。

hasUpperBound()

boolean

检查区间是否有上界。

lowerEndpoint()

T

返回区间的下界。

upperEndpoint()

T

返回区间的上界。

lowerBoundType()

BoundType

返回区间的下界类型(闭合或开放)。

upperBoundType()

BoundType

返回区间的上界类型(闭合或开放)。

示例代码:
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class ShardingJdbcDemoApplicationTests {
    public static void main(String[] args) {
        // 创建一个闭区间 [1, 5]
        Range<Integer> closedRange = Range.closed(1, 5);
        System.out.println("Closed range: " + closedRange);

        // 创建一个开区间 (1, 5)
        Range<Integer> openRange = Range.open(1, 5);
        System.out.println("Open range: " + openRange);

        // 创建一个半闭区间 [1, 5)
        Range<Integer> closedOpenRange = Range.closedOpen(1, 5);
        System.out.println("Closed-open range: " + closedOpenRange);

        // 创建一个半闭区间 (1, 5]
        Range<Integer> openClosedRange = Range.openClosed(1, 5);
        System.out.println("Open-closed range: " + openClosedRange);

        // 创建一个大于给定值的区间 (3, +∞)
        Range<Integer> greaterThanRange = Range.greaterThan(3);
        System.out.println("Greater than range: " + greaterThanRange);

        // 创建一个小于给定值的区间 (-∞, 3)
        Range<Integer> lessThanRange = Range.lessThan(3);
        System.out.println("Less than range: " + lessThanRange);

        // 创建一个大于或等于给定值的区间 [3, +∞)
        Range<Integer> atLeastRange = Range.atLeast(3);
        System.out.println("At least range: " + atLeastRange);

        // 创建一个小于或等于给定值的区间 (-∞, 3]
        Range<Integer> atMostRange = Range.atMost(3);
        System.out.println("At most range: " + atMostRange);

        // 创建一个包含所有可能值的区间 (-∞, +∞)
        Range<Integer> allRange = Range.all();
        System.out.println("All range: " + allRange);

        // 检查区间是否包含给定值
        boolean contains = closedRange.contains(3);
        System.out.println("Contains 3: " + contains);

        // 检查一个区间是否完全包含另一个区间
        boolean encloses = closedRange.encloses(openRange);
        System.out.println("Encloses open range: " + encloses);

        // 检查两个区间是否有交集或相邻
        boolean isConnected = closedRange.isConnected(openRange);
        System.out.println("Is connected: " + isConnected);

        // 返回两个区间的交集
        Range<Integer> intersection = closedRange.intersection(openRange);
        System.out.println("Intersection: " + intersection);

        // 返回两个区间的并集
        Range<Integer> span = closedRange.span(openRange);
        System.out.println("Span: " + span);

        // 检查区间是否有下界和上界
        boolean hasLowerBound = closedRange.hasLowerBound();
        boolean hasUpperBound = closedRange.hasUpperBound();
        System.out.println("Has lower bound: " + hasLowerBound);
        System.out.println("Has upper bound: " + hasUpperBound);

        // 返回区间的下界和上界
        int lowerEndpoint = closedRange.lowerEndpoint();
        int upperEndpoint = closedRange.upperEndpoint();
        System.out.println("Lower endpoint: " + lowerEndpoint);
        System.out.println("Upper endpoint: " + upperEndpoint);

        // 返回区间的下界类型和上界类型
        BoundType lowerBoundType = closedRange.lowerBoundType();
        BoundType upperBoundType = closedRange.upperBoundType();
        System.out.println("Lower bound type: " + lowerBoundType);
        System.out.println("Upper bound type: " + upperBoundType);
    }
}
2.3.2.5. Hint分片

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

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

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

1. 编写yaml文件

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        hint: #Hint分片策略
          algorithm-class-name: ${common.algorithm.db}
      # 分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            hint: #Hint分片策略
              algorithm-class-name: ${common.algorithm.tb}
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

# 配置分片策略
common:
  algorithm:
    db: com.zjp.shadingjdbcdemo.strategy.database.DatabaseHintAlgorithm
    tb: com.zjp.shadingjdbcdemo.strategy.table.TableHintAlgorithm

2. 添加分库范围查询和精确查询规则

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

package com.zjp.shadingjdbcdemo.strategy.database;

import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;

import java.util.Collection;
import java.util.HashSet;

public class DatabaseHintAlgorithm implements HintShardingAlgorithm<Integer> {
    /**
     * 执行自定义Hint分片逻辑。
     *
     * @param collection        数据库名称集合
     * @param hintShardingValue 分片值对象,包含分片键和逻辑表名
     * @return 分片后的数据源名称集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
        // 自定义Hint分片方法
        // 1.获取分片键的值
        Collection<Integer> values = hintShardingValue.getValues();
        // 2.获取逻辑表名
        String logicTableName = hintShardingValue.getLogicTableName();
        // 3.获取数据库名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 4.根据分片键的值,计算出对应的数据源名称
        return values.stream()
                .map(value -> dbNames.get((int) (value % 2)))
                .collect(Collectors.toSet());
    }
}

3. 添加分表规则

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

package com.zjp.shadingjdbcdemo.strategy.table;

import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class TableHintAlgorithm implements HintShardingAlgorithm<Integer> {
    /**
     * 强制路由
     *
     * @param collection        数据源集合
     * @param hintShardingValue 分片参数
     * @return 数据库集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
        // 自定义Hint分片方法
        // 1.获取分片键的值
        Collection<Integer> values = hintShardingValue.getValues();
        // 2.获取逻辑表名
        String logicTableName = hintShardingValue.getLogicTableName();
        // 3.获取数据库名称,并排序
        List<String> dbNames = collection.stream()
                .sorted(String::compareTo)
                .collect(Collectors.toList());
        // 4.获取真实表总数
        int size = collection.size();
        // 5.根据分片键的值,计算出对应的数据源名称
        return values.stream()
                .map(value -> {
                    long n;
                    int v = (int) (Math.pow(13, Math.ceil(Math.log(size * 7) / Math.log(2))) - 1);
                    do {
                        n = value & v;
                        v = v / 2;
                    } while (n >= size);
                    return dbNames.get((int) n);
                })
                .collect(Collectors.toSet());
    }
}
2.3.2.6. 不分片

对应NoneShardingStrategy。不分片的策略。

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource: # 数据源配置,可配置多个data_source_name
      names: ds0,ds1
      ds0: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
      ds1: # <!!数据库连接池实现类> `!!`表示实例化该类
        type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false # 数据库url连接,如果是HikariCP连接池,需要换成jdbcurl
        username: root # 数据库用户名
        password: root # 数据库密码
    sharding:
      # 唯一库数据
      default-data-source-name: ds0
      # 分库
      default-database-strategy:
        none: # 不分片
      # 分表
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2} #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          key-generator:
            column: id # 自增列名称,缺省表示不使用自增主键生成器
            type: SNOWFLAKE  #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            # props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          table-strategy: # 分表策略,同分库策略
            none: # 不分片
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

 2.3.3. 解决jdk8新时间类与Sharding-Sphere兼容问题

以 LocalDate 和 LocalDateTime 为例:

1. 引入依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.17</version>
</dependency>

2. 编写 BaseTypeHandler 的实现类:

  • LocalDate适配:
package com.zjp.shadingjdbcdemo.handler;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;


@Component
@MappedTypes(LocalDate.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setObject(i, parameter);
    }

    @Override
    public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return Convert.convert(new TypeReference<LocalDate>() {
            @Override
            public String getTypeName() {
                return super.getTypeName();
            }
        },rs.getObject(columnName));
    }

    @Override
    public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return Convert.convert(new TypeReference<LocalDate>() {
            @Override
            public String getTypeName() {
                return super.getTypeName();
            }
        },rs.getObject(columnIndex));
    }

    @Override
    public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return Convert.convert(new TypeReference<LocalDate>() {
            @Override
            public String getTypeName() {
                return super.getTypeName();
            }
        },cs.getObject(columnIndex));
    }
}
  • LocalDateTime适配:
package com.zjp.shadingjdbcdemo.handler;

import cn.hutool.core.convert.Convert;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;

@Component
@MappedTypes(LocalDateTime.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setObject(i, parameter);
    }

    @Override
    public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return Convert.toLocalDateTime(rs.getObject(columnName));
    }

    @Override
    public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return Convert.toLocalDateTime(rs.getObject(columnIndex));
    }

    @Override
    public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return Convert.toLocalDateTime(cs.getObject(columnIndex));
    }
}

其他时间类可仿照上述方案仿写。

2.3.4. 关于雪花算法配置

雪花算法生成的id总共64位8个字节,结构如下:

符号位时间位工作机器标识位序列位
1位(固定位0)41位10位12位

源码如下图:

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

  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),否则大于差值直接报异常。

配置示例: 

spring:
    shardingsphere:
        sharding:
            tables:
                key-generator:
                    column: id # 主键ID
                    type: SNOWFLAKE
                    props:
                      work:
                        id: 0
                      max:
                        vibration:
                          offset: 1
                        tolerate:
                          time:
                            difference:
                              milliseconds: 10

2.3.5. 测试

1. 创建数据表

CREATE DATABASE IF NOT EXISTS database1;

USE database1;

DROP TABLE IF EXISTS `t_user_1`;
DROP TABLE IF EXISTS `t_user_2`;

CREATE TABLE `t_user_1` (
  `id` bigint(20) NOT NULL COMMENT '用户编号',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '用户年龄',
  `salary` double DEFAULT NULL COMMENT '用户薪资',
  `birthday` datetime DEFAULT NULL COMMENT '用户生日',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_user_2` (
  `id` bigint(20) NOT NULL COMMENT '用户编号',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '用户年龄',
  `salary` double DEFAULT NULL COMMENT '用户薪资',
  `birthday` datetime DEFAULT NULL COMMENT '用户生日',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE DATABASE IF NOT EXISTS database2;

USE database2;

DROP TABLE IF EXISTS `t_user_1`;
DROP TABLE IF EXISTS `t_user_2`;

CREATE TABLE `t_user_1` (
  `id` bigint(20) NOT NULL COMMENT '用户编号',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '用户年龄',
  `salary` double DEFAULT NULL COMMENT '用户薪资',
  `birthday` datetime DEFAULT NULL COMMENT '用户生日',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_user_2` (
  `id` bigint(20) NOT NULL COMMENT '用户编号',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '用户年龄',
  `salary` double DEFAULT NULL COMMENT '用户薪资',
  `birthday` datetime DEFAULT NULL COMMENT '用户生日',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zjp</groupId>
    <artifactId>shading-jdbc-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shading-jdbc-demo</name>
    <description>shading-jdbc-demo</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
    </properties>

    <dependencies>
        <!--springboot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.20</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--sharding-jdbc-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.1.1</version>
        </dependency>

        <!--hutool工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

        <!--测试数据-->
        <dependency>
            <groupId>com.github.javafaker</groupId>
            <artifactId>javafaker</artifactId>
            <version>1.0.2</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.zjp.shadingjdbcdemo.ShadingJdbcDemoApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

3. 编写配置文件(非Sharding-JDBC配置部分)

server:
  port: 18080
spring:
  application:
    name: sharding-jdbc-demo

logging:
  level:
    com.zjp.shadingjdbcdemo: debug
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    # 设置当查询结果值为null时,同样映射该查询字段给实体(Mybatis-Plus默认会忽略查询为空的实体字段返回)。
    call-setters-on-nulls: true

4. 创建实体类

注意:

  1. @TableName 注解指定的表为虚拟表 t_user。
  2. @TableId 的 type 属性为 IdType.AUTO 则采用 Sharding-JDBC 数据源生成策略,否则采用 MyBatis Plus 主键生成策略。
  3. 主键的数据类型为包装类,否则主键赋值失败,为默认值0。
package com.zjp.shadingjdbcdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户编号
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String name;

    /**
     * 用户年龄
     */
    private Integer age;

    /**
     * 用户薪资
     */
    private Double salary;

    /**
     * 用户生日
     */

    private LocalDateTime birthday;
}

5. 编写Service层和mapper层

package com.zjp.shadingjdbcdemo.service;

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

public interface UserService extends IService<User> {
}
package com.zjp.shadingjdbcdemo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.mapper.UserMapper;
import com.zjp.shadingjdbcdemo.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
package com.zjp.shadingjdbcdemo.mapper;

import com.zjp.shadingjdbcdemo.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

6. 编写测试类

package com.zjp.shadingjdbcdemo;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.javafaker.Faker;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.hint.HintManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.time.Year;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Random;

@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
   
    @Autowired
    private UserService userService;

    private static final Faker FAKER = new Faker();

    /**
     * 测试精确分片、行表达式分片、复合分片和不分片
     */
    @Test
    public void testSaveUser() {
        Date birthday = FAKER.date().birthday(18, 70);
        User user = new User()
                .setName(FAKER.name().fullName())
                .setAge(Year.now().getValue() - birthday.getYear() - 1900)
                .setSalary(10000.0)
                .setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
        userService.save(user);
    }

    /**
     * 测试范围分片
     */
    @Test
    public void testGetUser() {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.ge(User::getId,1L);
        List<User> list = userService.list(wrapper);
        list.forEach(System.out::println);
    }

    /**
     * 测试hint分片
     */
    @Test
    public void testHint() {
        Date birthday = FAKER.date().birthday(18, 100);
        // 创建一个HintManager对象, 确保线程内只存在一个HintManager对象,否则会抛出异常"Hint has previous value, please clear first."
        HintManager.clear();
        HintManager hintManager = HintManager.getInstance();
        // 根据数据库hint分片规则选取数据库
        hintManager.addDatabaseShardingValue("t_user", 3);
        // 根据数据表hint分片规则选取数据表
        hintManager.addTableShardingValue("t_user", 1);
        User user = new User()
                .setName(FAKER.name().fullName())
                .setAge(18)
                .setSalary(10000.0)
                .setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
        userService.save(user);
        // HintManager存放在线程变量中, 所以需要清除
        HintManager.clear();
    }
}

7. 测试

在对应的策略上打断点,以debug方式启动测试方法,查看是否走断点。

2.4. 自定义主键生成策略

2.4.1. 源码参考

1. Ctrl + 左键点击 type,点击定位文件位置,如下图所示,这三个文件即为主键生成策略的配置文件。org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件指定了两种生成策略,分别为UUID和雪花算法。

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

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

1. 编写 ShardingKeyGenerator 实现类

package com.zjp.shadingjdbcdemo.strategy.keygen;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;

import java.util.Properties;
import java.util.Random;

@Slf4j
@Getter
@Setter
public class MyShardingKeyGenerator implements ShardingKeyGenerator {
    private Properties properties = new Properties();

    /**
     * 重写generateKey方法以生成唯一的键值
     * 此方法用于生成一个随机的长整型数字作为键值,
     * 键值范围为1到1000,旨在用于需要唯一标识符的场景
     *
     * @return 生成的键值作为一个Comparable对象返回,由于键值为Long类型,
     *         而Long实现了Comparable接口,这使得返回值可以与其它对象进行比较
     */
    @Override
    public Comparable<?> generateKey() {
        Random random = new Random();
        Long id = (long) (random.nextInt(1000) + 1);
        log.info("自定义主键生成策略,主键为:{}", id);
        return id;
    }

    /**
     * 重写getType方法
     * 返回一个预定义的键值,用于标识特定的类型或功能这个方法总是返回"MyKey",
     * 表示当前对象或方法的特定类型或功能
     *
     * @return String 表示当前对象或方法类型的预定义键值
     */
    @Override
    public String getType() {
        return "MyKey";
    }
}

2. 在resource目录下创建META-INF/services/org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件,文件内容为自定义主键生成策略类的全限定名。

com.zjp.shadingjdbcdemo.strategy.keygen.MyShardingKeyGenerator

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

spring:
    shardingsphere:
        sharding:
            tables:
                key-generator:
                    column: id # 主键ID
                    type: MyKey  # 自定义主键生成策略

4. 如果是MyBatis Plus,则需要将主键生成策略换成IdType.AUTO

package com.zjp.shadingjdbcdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户编号
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String name;

    /**
     * 用户年龄
     */
    private Integer age;

    /**
     * 用户薪资
     */
    private Double salary;

    /**
     * 用户生日
     */

    private LocalDateTime birthday;
}

 5. 编写测试类

package com.zjp.shadingjdbcdemo;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.javafaker.Faker;
import com.zjp.shadingjdbcdemo.entity.User;
import com.zjp.shadingjdbcdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.hint.HintManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.time.Year;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Random;

@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
   
    @Autowired
    private UserService userService;

    private static final Faker FAKER = new Faker();

    @Test
    public void testSaveUser() {
        Date birthday = FAKER.date().birthday(18, 70);
        User user = new User()
                .setName(FAKER.name().fullName())
                .setAge(Year.now().getValue() - birthday.getYear() - 1900)
                .setSalary(10000.0)
                .setBirthday(birthday.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
        userService.save(user);
    }

    /**
     * 测试范围分片
     */
    @Test
    public void testGetUser() {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.ge(User::getId,1L);
        List<User> list = userService.list(wrapper);
        list.forEach(System.out::println);
    }
}

6. 启动测试

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

 2.5. 广播表

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

2.5.1. 配置步骤

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

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: root
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: root
    sharding:
      broadcast-tables: # 广播表规则列表
        - t_dict
      tables:
        t_dict:
          key-generator:
            column: dict_id
            type: SNOWFLAKE
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

 2.5.2. 测试

1. 创建表结构

CREATE DATABASE IF NOT EXISTS database1;

USE database1;

DROP TABLE IF EXISTS `t_dict`;

CREATE TABLE `t_dict` (
  `dict_id` bigint(20) NOT NULL COMMENT '字典id',
  `type` varchar(50) NOT NULL COMMENT '字典类型',
  `code` varchar(50) NOT NULL COMMENT '字典编码',
  `value` varchar(50) NOT NULL COMMENT '字典值',
  PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

CREATE DATABASE IF NOT EXISTS database2;

USE database2;

DROP TABLE IF EXISTS `t_dict`;

CREATE TABLE `t_dict` (
  `dict_id` bigint(20) NOT NULL COMMENT '字典id',
  `type` varchar(50) NOT NULL COMMENT '字典类型',
  `code` varchar(50) NOT NULL COMMENT '字典编码',
  `value` varchar(50) NOT NULL COMMENT '字典值',
  PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

2. 创建实体类

package com.zjp.shadingjdbcdemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_dict")
public class TDict implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 字典id
     */
    private Long dictId;

    /**
     * 字典类型
     */
    private String type;

    /**
     * 字典编码
     */
    private String code;

    /**
     * 字典值
     */
    private String value;
}

 3. 编写sevice层和mapper层

package com.zjp.shadingjdbcdemo.service;

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

public interface ITDictService extends IService<TDict> {
}
package com.zjp.shadingjdbcdemo.service.impl;

import com.zjp.shadingjdbcdemo.entity.TDict;
import com.zjp.shadingjdbcdemo.mapper.TDictMapper;
import com.zjp.shadingjdbcdemo.service.ITDictService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class TDictServiceImpl extends ServiceImpl<TDictMapper, TDict> implements ITDictService {
}
package com.zjp.shadingjdbcdemo.mapper;

import com.zjp.shadingjdbcdemo.entity.TDict;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TDictMapper extends BaseMapper<TDict> {
}

 4. 编写测试类

package com.zjp.shadingjdbcdemo;

import com.zjp.shadingjdbcdemo.entity.TDict;
import com.zjp.shadingjdbcdemo.service.ITDictService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class ShadingJdbcDemoTests {
    @Autowired
    private ITDictService itDictService;

    @Test
    public void testSaveDict() {
        TDict dict = new TDict()
                .setCode("001")
                .setValue("男")
                .setType("sex");
        itDictService.save(dict);
    }
}

7. 测试

运行发现两个库的 t_dict 表均插入成功。

2.6. 绑定表

指分片规则一致的主表和子表。例如:t_user表和t_user_item表,均按照user_id分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果SQL为:

SELECT i.*, o.* FROM t_user o JOIN t_user_item i ON o.id = i.user_id WHERE o.id IN (10, 11);

在不配置绑定表关系时,假设分片键user_id将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:

SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);

在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.*, o.* FROM t_user_1 o JOIN t_user_item_1 i ON o.id = i.user_id WHERE o.id IN (10, 11);
SELECT i.*, o.* FROM t_user_2 o JOIN t_user_item_2 i ON o.id = i.user_id WHERE o.id IN (10, 11);

其中t_user在FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么t_user_item表的分片计算将会使用t_user的条件。故绑定表之间的分区键要完全相同。

2.6.1. 配置步骤

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

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/database1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: root
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/database2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: root
    sharding:
      default-data-source-name: ds0
      default-database-strategy:
        inline:
          sharding-column: id
          algorithm-expression: ds$->{id % 2}
      tables:
        t_user:
          actual-data-nodes: ds$->{0..1}.t_user_$->{1..2}
          key-generator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_user_$->{id % 2 + 1}
        t_user_item:
          actual-data-nodes: ds$->{0..1}.t_user_item_$->{1..2}
          key-generator:
            column: item_id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: user_id
              algorithm-expression: t_user_item_$->{user_id % 2 + 1}
      binding-tables: # 绑定表规则列表
        - t_user,t_user_item
    props: # 属性配置
      sql:
        show: true #是否开启SQL显示,默认值: false
      # executor.size: #工作线程数量,默认值: CPU核数
      # max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
      # check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

 2.5.2. 测试

1. 创建表结构

CREATE DATABASE IF NOT EXISTS database1;

USE database1;

DROP TABLE IF EXISTS `t_user_item_1`;
DROP TABLE IF EXISTS `t_user_item_2`;

CREATE TABLE `t_user_item_1` (
  `item_id` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_user_item_2` (
  `item_id` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE DATABASE IF NOT EXISTS database2;

USE database2;

DROP TABLE IF EXISTS `t_user_item_1`;
DROP TABLE IF EXISTS `t_user_item_2`;

CREATE TABLE `t_user_item_1` (
  `item_id` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_user_item_2` (
  `item_id` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 创建实体类

package com.zjp.shadingjdbcdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

import java.time.LocalDateTime;

public class UserItemVo {
    /**
     * 用户编号
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String name;

    /**
     * 用户年龄
     */
    private Integer age;

    /**
     * 用户薪资
     */
    private Double salary;

    /**
     * 用户生日
     */

    private LocalDateTime birthday;

    private Long itemId;
}

 3. 编写mapper层

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zjp.shadingjdbcdemo.mapper.UserItemMapper">
    <select id="getList" resultType="com.zjp.shadingjdbcdemo.entity.UserItemVo">
        SELECT i.*, o.*
        FROM t_user o
                 JOIN t_user_item i ON o.id = i.user_id
        WHERE o.id IN (10, 11);
    </select>
</mapper>

 4. 编写测试类

package com.zjp.shadingjdbcdemo;

import com.zjp.shadingjdbcdemo.entity.UserItemVo;
import com.zjp.shadingjdbcdemo.service.UserItemService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class ShadingJdbcDemoTests {
    @Autowired
    private UserItemService userItemService;

    @Test
    public void test() {
        UserItemVo result = userItemService.selectList();
        log.info("查询结果为:{}", result);
    }
}

7. 测试

在注释掉spring.shardingsphere.sharding.binding-tables配置项时,日志为:

在配置 spring.shardingsphere.sharding.binding-tables配置项时,日志为:


本篇文章示例代码:GitHub - kerrsixy/sharding-jdbc-demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2244415.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

芋道启用undertown

芋道使用的是tomcat, 可以替换为性能更好的undertown 1.maven配置 1.1在yudao-dependencies模块下yudao-spring-boot-starter-web 和yudao-spring-boot-starter-websocket排除spring-boot-starter-tomcat 添加 <exclusions><exclusion><groupId>org.spring…

(Linux 入门) 基本指令、基本权限

目录 一、什么是操作系统 二、基础指令 01. ls 指令 02. pwd命令 03.mkdir 04. touch指令 05.rmdir指令 && rm 指令 06.man指令&#xff08;重要&#xff09; 07 cat 08.cp指令 09 mv指令 10 alias 指令 11.more指令 12.head指令 13.less指令 14.时间相…

贴代码框架PasteForm特性介绍之select,selects,lselect和reload

简介 PasteForm是贴代码推出的 “新一代CRUD” &#xff0c;基于ABPvNext&#xff0c;目的是通过对Dto的特性的标注&#xff0c;从而实现管理端的统一UI&#xff0c;借助于配套的PasteBuilder代码生成器&#xff0c;你可以快速的为自己的项目构建后台管理端&#xff01;目前管…

【AI+教育】一些记录@2024.11.16

《万字长文&#xff0c;探讨关于ChatGPT的五个最核心问题》 万字长文&#xff0c;探讨关于ChatGPT的五个最核心问题关于 ChatGPT 铺天盖地的信息让人无所适从。本文则试图提炼出五个关键问题&#xff1a;如何理解这次范式突破&#xff0c;未来能达到的技术天花板&#xff0c;行…

基于vue框架的的图书在线销售系统设计2503xPC端(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,图书分类,图书信息,入库信息 开题报告内容 基于Vue框架的图书在线销售系统设计开题报告 一、研究背景与意义 随着互联网技术的不断发展和普及&#xff0c;电子商务已成为现代商业的重要组成部分。图书作为知识和文化的载体&#…

android:taskAffinity 对Activity退出时跳转的影响

android:taskAffinity 对Activity跳转的影响 概述taskAffinity 的工作机制taskAffinity对 Activity 跳转的影响一个实际的开发问题总结参考 概述 在 Android 开发中&#xff0c;任务栈&#xff08;Task&#xff09;是一个核心概念。它决定了应用程序的 Activity 如何相互交互以…

Android15之解决:Dex checksum does not match for dex:framework.jar问题(二百三十九)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

SpringBoot(7)-Swagger

目录 一、是什么 二、SpringBoot集成Swagger 三、配置Swagger 3.1 配置文档信息 3.2 配置扫描接口 3.3 配置Swagger开关 3.4 配置API分组 3.5 实体配置 四、常用注解 五、总结 一、是什么 是一款API框架&#xff0c;API文档和API定义同步更新&#xff0c;可以在线测…

小程序19-微信小程序的样式和组件介绍

在小程序中不能使用 HTML 标签&#xff0c;也就没有 DOM 和 BOM&#xff0c;CSS 也仅支持部分选择器 小程序提供了 WXML 进行页面结构的编写&#xff0c;WXSS 进行页面的样式编写 WXML 提供了 view、text、image、navigator等标签构建页面结构&#xff0c;小程序中标签称为组件…

[Admin] Dashboard Filter for Mix Report Types

Background RevOps team has built a dashboard for sales team to track team members’ performance, but they’re blocked by how to provide a manager view based on sales’ hierarchy. Therefore, they seek for dev team’s help to clear their blocker. From foll…

基于AIRTEST和Jmeter、Postman的自动化测试框架

基于目前项目和团队技术升级&#xff0c;采用了UI自动化和接口自动化联动数据&#xff0c;进行相关测试活动&#xff0c;获得更好的测试质量和测试结果。

使用 Azure OpenAI 服务对数据进行联合 SharePoint 搜索

作者&#xff1a;来自 Elastic Gustavo Llermaly 使用 Azure OpenAI 服务处理你的数据&#xff0c;并使用 Elastic 作为向量数据库。 在本文中&#xff0c;我们将探索 Azure OpenAI 服务 “On Your Data”&#xff0c;使用 Elasticsearch 作为数据源。我们将使用 Elastic Shar…

Linux 入门——基本指令1

目录 一背景知识的简介 二 入门相关指令的使用 一.背景知识的简介 1.认识 Linux &#xff0c;了解Linux 的相关背景 其实Linux 是从 Unix 发展而来的。 Linux&#xff0c;一般指GNU/Linux&#xff08;单独的Linux内核并不可直接使用&#xff0c;一般搭配GNU套件&#xff0…

RabbitMQ消息可靠性保证机制4--消费端限流

7.7 消费端限流 在类似如秒杀活动中&#xff0c;一开始会有大量并发写请求到达服务端&#xff0c;城机对消息进行削峰处理&#xff0c;如何做&#xff1f; 当消息投递的速度远快于消费的速度时&#xff0c;随着时间积累就会出现“消息积压”。消息中间件本身是具备一定的缓冲…

抽象java入门1.5.3.2——类的进阶(中)

前期回顾&#xff1a;抽象java入门1.5.3.1——类的进阶https://blog.csdn.net/c_yanxin_ru/article/details/140858898?spm1001.2014.3001.5501 总结&#xff1a; 在代码溯源中&#xff0c;我发现了一个奇怪的东西&#xff0c;就是OUT不是类中类&#xff08;不是常规类的写法…

题解 洛谷 Luogu P1873 [COCI 2011/2012 #5] EKO / 砍树 二分答案 C/C++

题目传送门&#xff1a; P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷 | 计算机科学教育新生态https://www.luogu.com.cn/problem/P1873思路&#xff1a; 很简单的二分答案 每次找区间中点 m&#xff0c;判断以 m 为高度砍下的木头是否够 h 即可 代码&#xff1a; #defin…

蓝桥杯第22场小白入门赛2~5题

这场比赛开打第二题就理解错意思了&#xff0c;还以为只能用3个消除和5个消除其中一种呢&#xff0c;结果就是死活a不过去&#xff0c;第三题根本读不懂题意&#xff0c;这蓝桥杯的题面我只能说出的是一言难尽啊。。第四题写出来一点但是后来知道是错了&#xff0c;不会正解&am…

PyQt天天酷跑游戏(附下载地址)

欢迎下载体验&#xff01; 文件大小&#xff1a;25.7 M 下载地址&#xff1a;链接&#xff1a;https://wwrr.lanzoul.com/ifOvc2fe163c 观看演示视频~ Pyqt-跑酷游戏 一&#xff0e;前言 天天酷跑大家都玩过吧&#xff0c;这是我们学生时代的回忆&#xff0c;目前这款游戏还…

跨平台WPF框架Avalonia教程 十五

ListBox 列表框 列表框从元素源集合中显示多行元素&#xff0c;并允许选择单个或多个。 列表中的元素可以组合、绑定和模板化。 列表的高度会扩展以适应所有元素&#xff0c;除非特别设置&#xff08;使用高度属性&#xff09;&#xff0c;或由容器控件设置&#xff0c;例如…

python蓝桥杯刷题2

1.最短路 题解&#xff1a;这个采用暴力枚举&#xff0c;自己数一下就好了 2.门牌制作 题解&#xff1a;门牌号从1到2020&#xff0c;使用for循环遍历一遍&#xff0c;因为range函数无法调用最后一个数字&#xff0c;所以设置成1到2021即可&#xff0c;然后每一次for循环&…