Spring Boot 3.5 整合 Apache ShardingSphere 5.5.3 深度实践

欢迎你来读这篇博客,这篇博客主要是关于 Spring Boot 3.5 整合 Apache ShardingSphere 5.5.3 的工程化实践。

这篇文章不会只停留在“配置两个库、插一条数据”的入门层面,而是尽量把 ShardingSphere 的定位、架构、功能边界、Spring Boot 3.5
接入方式、分库分表、读写分离、分布式事务、加密、脱敏、影子库、Proxy、DistSQL、数据迁移、可观察性和生产避坑都串起来。

如果你只想复制一份可跑配置,可以直接看 正文 -> 5. Spring Boot 3.5 整合 ShardingSphere-JDBC

序言

提到分库分表,很多人的第一反应还是“拆表、取模、路由、改 SQL”。这当然是核心,但 ShardingSphere 早就不只是一个分库分表工具。

Apache ShardingSphere 官方把它定位为 Database Plus,也就是构建在异构数据库之上的增强层。它不重新发明一个数据库,而是尽量复用
MySQL、PostgreSQL、openGauss、SQL Server 等已有数据库的存储能力,然后在其上提供统一访问、分布式
SQL、分片、读写分离、数据加密、数据脱敏、影子库、数据迁移、可观察性等能力。

在 Spring Boot 项目里,ShardingSphere 最常用的是 ShardingSphere-JDBC。它本质上是一个增强版 JDBC Driver,应用依旧拿到的是一个
DataSource,MyBatis、JPA、JdbcTemplate 都可以接在这个数据源上。你写的是逻辑 SQL,ShardingSphere
根据规则解析、路由、改写、执行、归并,最后把结果返回给应用。

有一点先说清楚:ShardingSphere
很强,但它不是万能胶。分库分表会让系统复杂度明显上升,一旦用了,就要认真面对数据建模、分片键选择、跨库查询、全路由、分布式事务、扩容迁移、监控排查这些问题。它不是“加个依赖解决数据库性能问题”,更像是你给数据库系统加了一层交通枢纽。交通枢纽能提效,也会带来新的规则。

本文解决什么问题

很多 ShardingSphere 教程的问题并不是配置不能启动,而是只展示了“最顺利的一条路径”:建两个库、按 ID 取模、插入两条记录,然后宣布分库分表完成。真正进入生产后,问题才会集中出现:

  • 为什么同一条逻辑 SQL 会展开成几十条真实 SQL?
  • 为什么加了分片后,COUNTORDER BYGROUP BY 和深分页突然变慢?
  • 为什么明明用了 @Transactional,跨库写入仍然可能出现部分成功?
  • 为什么应用只有 20 个连接,最终却把数据库打出了上千个连接?
  • 为什么扩容不是把 ${0..1} 改成 ${0..3} 就结束?
  • 为什么同样的 YAML 在 5.3、5.4、5.5 中可能出现属性名或行为差异?
  • 如何验证一条 SQL 的真实路由、归并方式和连接消耗,而不是靠猜?

因此,本文以“设计—接入—验证—上线—扩容—排障”为主线。你不仅会得到一份 Spring Boot 3.5 + ShardingSphere 5.5.3 的完整配置,还会理解配置背后的代价。

阅读路线

flowchart TD
    A[先判断是否真的需要分库分表] --> B[理解逻辑表与真实节点]
    B --> C[选择分片键与分片算法]
    C --> D[完成 Spring Boot 3.5 接入]
    D --> E[验证精确路由与全路由]
    E --> F[处理事务与读一致性]
    F --> G[建立监控和压测基线]
    G --> H[设计迁移、扩容和回滚]

    A -.不需要.-> I[优先索引、归档、缓存、读写分离]

建议第一次阅读按顺序看;已经接入过 ShardingSphere 的读者,可以重点阅读第 6、13、16、17、20、26~32 节。

版本与资料使用说明

本文的配置语法以 Apache ShardingSphere 5.5.3 为准,不混用 4.x 的 spring.shardingsphere.* Starter 配置,也不把 current 文档中未来版本的变化直接套回 5.5.3。尤其是数据源 URL 属性、单表规则、事务支持、Proxy 配置与 DistSQL 语法,必须以项目实际锁定版本为准。

本文在原有博客内容上做增量扩写,保留已有章节与示例,并新增:

  1. SQL 解析、绑定、路由、改写、执行、归并的完整内核链路;
  2. 可直接启动的 Docker Compose、初始化 SQL 与工程目录;
  3. MyBatis、JdbcTemplate、集成测试和路由断言;
  4. 分片容量规划、虚拟槽位与扩容策略;
  5. 读写一致性、分布式事务边界和 Outbox 最终一致性;
  6. Proxy 高可用、DistSQL 变更流程和数据迁移 Runbook;
  7. 监控指标、日志规范、压测方法与生产验收清单。

正文

1. 版本选择与 Spring Boot 3.5 适配结论

截至本文撰写时,Apache ShardingSphere 官方下载页显示当前版本为 5.5.3,发布日期为 2026-03-01。本文以:

1
2
3
4
5
6
Spring Boot: 3.5.x
JDK: 17 / 21
Apache ShardingSphere: 5.5.3
Database: MySQL 8.x
Connection Pool: HikariCP
Access Mode: ShardingSphere-JDBC Driver

作为示例基线。

Spring Boot 3.x 已经切换到 Jakarta EE 9+,最低 Java 基线也发生了变化。ShardingSphere 5.5.x 对 Spring Boot 3
的推荐接入方式,不再是老版本常见的 shardingsphere-jdbc-core-spring-boot-starter,而是:

1
2
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
spring.datasource.url=jdbc:shardingsphere:classpath:shardingsphere.yaml

也就是说,应用只需要把 ShardingSphere 当成 JDBC Driver 使用。

1.1 旧 starter 不建议再用

历史文章里经常能看到这些依赖:

1
2
3
4

<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>

新项目不要再优先照抄这些。Spring Boot 3.5 项目建议直接使用:

1
2
3
4
5
6

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>5.5.3</version>
</dependency>

然后通过 ShardingSphereDriver 加载 YAML 配置。

1.2 Spring Boot 3.5 下的事务提醒

官方文档明确提醒:ShardingSphere 的 XA 分布式事务在 Spring Boot OSS 3 / Jakarta EE 9+ 场景下尚未完全就绪。

所以 Spring Boot 3.5 项目里建议这样处理:

  • 普通业务优先使用 LOCAL
  • 强一致跨库事务不要轻易承诺。
  • 真正需要跨库强一致时,要单独压测和验证 XA / BASE 方案,不要只看配置能启动。
  • 能通过业务设计规避跨库事务,就尽量规避。
  • 订单、结算、库存这类场景可以考虑最终一致性、消息表、补偿任务、对账任务。

分库分表之后还想继续像单库事务一样随便写,这是很多事故的开始。数据库不会因为我们加了中间件就突然变温柔。

1.3 5.5.3 为什么值得单独说明

5.5.3 不是简单的修订号升级。它包含大量安全修复、SQL 兼容性改进、JDBC/Proxy/DistSQL 稳定性修复,并继续推进可插拔打包。对生产项目而言,升级价值主要体现在三个方面:

  • 安全基线更完整:发布说明列出了多项 CVE 修复。生产环境不应长期停留在已知漏洞版本。
  • 连接与资源管理修复:包括特定分布式事务和 JDBC 连接释放问题,直接关系到长时间运行后的连接泄漏风险。
  • 兼容性和并发稳定性改进:涉及 PreparedStatement、DistSQL 并发执行、不同数据库协议等问题。

但升级不能只改 Maven 版本。至少需要回归:

1
2
3
4
5
6
7
8
9
10
11
启动与元数据加载
核心 CRUD
批量 INSERT / UPDATE
分页、排序、聚合
绑定表 JOIN
Hint 路由
事务回滚
主从路由
DistSQL(如使用 Proxy)
迁移任务(如使用 Pipeline)
Agent 指标与链路追踪

1.4 版本锁定与依赖收敛

建议在父 POM 中锁定 ShardingSphere 版本,并检查依赖树中是否混入旧模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
<java.version>21</java.version>
<shardingsphere.version>5.5.3</shardingsphere.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-bom</artifactId>
<version>${shardingsphere.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

如果项目只使用统一聚合依赖,也可以直接指定:

1
2
3
4
5
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere.version}</version>
</dependency>

检查依赖:

1
2
./mvnw dependency:tree \
-Dincludes=org.apache.shardingsphere,org.yaml:snakeyaml,com.zaxxer:HikariCP

重点排查:

  • 同时存在 4.x 与 5.x ShardingSphere 包;
  • 业务组件偷偷引入旧 Starter;
  • SnakeYAML 被强制降级;
  • HikariCP 版本被第三方 BOM 覆盖;
  • MySQL Connector/J 同时出现 mysql:mysql-connector-javacom.mysql:mysql-connector-j

1.5 不同配置方式如何选择

配置方式 优点 缺点 适用场景
JDBC Driver + YAML 配置直观,Spring Boot 3 接入简单 静态配置变更通常需要重启 大多数 Java 服务
Java API 可编程、可动态组装 代码量大,配置治理成本高 框架封装、动态租户
Proxy + YAML 多语言透明接入 独立部署,增加网络跳转 中小规模 Proxy 集群
Proxy + DistSQL 在线管理、集中治理 变更权限与审计要求高 平台化数据库网关
JDBC/Proxy + Cluster Mode 元数据共享、支持治理 引入注册中心和一致性问题 多实例统一规则

对普通 Spring Boot 业务服务,优先使用 JDBC Driver + YAML。不要因为“动态”听起来高级,就把规则修改权限直接暴露给应用。

2. ShardingSphere 是什么

Apache ShardingSphere 是一个分布式 SQL 事务和查询引擎生态,它主要包含两个常用接入端:

  • ShardingSphere-JDBC
  • ShardingSphere-Proxy

也可以把二者混合使用。

2.1 ShardingSphere-JDBC

ShardingSphere-JDBC 是轻量级 Java 框架,在 Java JDBC 层提供增强能力。

特点:

  • 以 Jar 包方式集成到应用内部。
  • 应用直接连接真实数据库。
  • 不需要单独部署 Proxy 服务。
  • 性能损耗相对小。
  • 适合 Java 项目,尤其是 Spring Boot、MyBatis、JPA、JdbcTemplate 项目。
  • 配置跟随应用发布,适合应用自治。

适合场景:

  • 只有 Java 技术栈。
  • 希望接入成本低。
  • 希望少部署一个中间件服务。
  • 应用团队能管理分片规则。
  • 对性能敏感。

2.2 ShardingSphere-Proxy

ShardingSphere-Proxy 是透明数据库代理。应用把它当成一个 MySQL / PostgreSQL / openGauss 服务连接。

特点:

  • 独立部署。
  • 对多语言友好,Java、Go、Python、PHP、Node.js 都能连。
  • 配置和规则可以集中治理。
  • 支持 DistSQL 动态管理规则。
  • 更适合运维管控、异构语言、统一数据库网关场景。

适合场景:

  • 多语言系统。
  • 多业务系统共享一套分片规则。
  • 希望 DBA 或平台团队集中管理规则。
  • 需要 DistSQL、数据迁移、CDC、治理能力。
  • 不希望每个应用都内嵌 ShardingSphere-JDBC。

2.3 Hybrid 混合架构

混合架构是指部分 Java 应用使用 ShardingSphere-JDBC,其他语言或外部系统通过 ShardingSphere-Proxy 访问同一套逻辑数据库。

这种模式适合大平台,但规则治理要非常谨慎,尤其要避免:

  • JDBC 和 Proxy 的规则不一致。
  • 应用本地 YAML 与治理中心配置冲突。
  • 变更规则时只更新了一侧。
  • 不同入口对同一张逻辑表的路由结果不同。

2.4 JDBC 与 Proxy 的请求链路差异

flowchart LR
    subgraph JDBC模式
        A1[Spring Boot 应用] --> A2[ShardingSphere-JDBC]
        A2 --> A3[(MySQL ds_0)]
        A2 --> A4[(MySQL ds_1)]
    end

    subgraph Proxy模式
        B1[Java / Go / Python / BI] --> B2[ShardingSphere-Proxy 集群]
        B2 --> B3[(MySQL ds_0)]
        B2 --> B4[(MySQL ds_1)]
        B2 <--> B5[(ZooKeeper / Etcd)]
    end

JDBC 模式的“网络跳数”更少,但规则和依赖进入每个应用实例;Proxy 模式把计算和规则集中到了独立服务,却需要承担 Proxy 的容量、高可用、连接复用和协议兼容问题。

2.5 选型决策树

flowchart TD
    A{是否多语言统一访问} -->|是| P[优先 Proxy]
    A -->|否| B{Java 服务是否允许内嵌依赖}
    B -->|否| P
    B -->|是| C{是否要求规则集中在线变更}
    C -->|强要求| P
    C -->|弱要求| J[优先 JDBC]
    P --> D{是否有独立 DBA/平台团队}
    D -->|没有| E[谨慎:Proxy 运维成本可能更高]
    D -->|有| F[建设 Proxy 高可用与 DistSQL 审计]

2.6 ShardingSphere 不负责什么

为了避免错误预期,需要明确它不等于:

  • 自动把任意慢 SQL 变快;
  • 自动选择正确分片键;
  • 自动消除跨库事务;
  • 自动把 OLTP 查询变成 OLAP 引擎;
  • 自动解决数据库主从复制延迟;
  • 自动完成无风险扩容;
  • 自动保障业务幂等和数据对账;
  • 自动替代数据库索引、备份、审计与权限系统。

ShardingSphere 提供的是数据库增强和统一 SQL 入口。业务模型错了,中间件通常只会让错误跑得更分布式。

3. 核心功能总览

ShardingSphere 5.5.3 常见能力可以按下面几类理解。

mindmap
  root((Apache ShardingSphere))
    接入端
      ShardingSphere-JDBC
      ShardingSphere-Proxy
      Hybrid
    数据分布
      数据分片
      读写分离
      单表
      广播表
      绑定表
    数据安全
      数据加密
      数据脱敏
      SQL 审计
    流量与测试
      影子库
      强制路由 Hint
      流量治理
    事务
      LOCAL
      XA
      BASE
    管控
      DistSQL
      元数据持久化
      集群模式
      规则治理
    迁移与生态
      数据迁移
      CDC
      SQL 联邦查询
      可观察性

3.1 数据分片

数据分片就是把一张逻辑表拆到多个真实数据库、多个真实表里。

常见拆法:

  • 只分表:demo_ds.t_order_0demo_ds.t_order_1
  • 只分库:demo_ds_0.t_orderdemo_ds_1.t_order
  • 分库又分表:demo_ds_${0..1}.t_order_${0..15}

3.2 读写分离

读写分离用于主从架构:

  • 写 SQL 路由到主库。
  • 读 SQL 路由到从库。
  • 事务内读请求默认可以路由到主库,保证读己之写。
  • 从库可以通过随机、轮询、权重等算法负载均衡。

3.3 分布式事务

ShardingSphere 提供:

  • LOCAL
  • XA
  • BASE

但是 Spring Boot 3.5 场景要特别注意 XA 的兼容性限制。不要拿分布式事务当默认方案,优先通过分片键设计让单次业务写入落到同一个物理库。

3.4 数据加密

应用读写逻辑列,例如 phone,真实数据库保存密文列,例如 phone_cipher

ShardingSphere 自动完成:

  • INSERT 时明文转密文。
  • SELECT 时密文转明文。
  • WHERE 查询时改写查询条件。
  • 可配置辅助查询列、模糊查询列。

3.5 数据脱敏

数据脱敏更偏向查询结果保护,比如:

  • 手机号:138****1234
  • 邮箱:ma***@example.com
  • 密码:MD5 或固定掩码

加密是存储层保护,脱敏是展示层保护,二者不要混为一谈。

3.6 影子库

影子库用于生产压测隔离。带压测标记的 SQL 会路由到影子库,不污染生产库。

常见标记:

  • SQL Hint:/* SHARDINGSPHERE_HINT: SHADOW=true */
  • 某个字段命中规则,比如 user_id = 1
  • 某个 SQL 操作类型命中规则,比如 INSERT

3.7 DistSQL

DistSQL 是 ShardingSphere 的分布式 SQL 管理语言,主要用于 Proxy 场景。

它可以通过 SQL 方式完成:

  • 注册存储单元。
  • 创建分片规则。
  • 查看规则。
  • 修改规则。
  • 导入导出配置。
  • 管理迁移任务。
  • 查看运行状态。

3.8 数据迁移与 CDC

ShardingSphere-Proxy 提供数据迁移能力,适合:

  • 单库迁移到分库分表。
  • 老库迁移到新库。
  • 调整分片规则后的数据搬迁。

CDC 则用于捕获数据变更,适合数据同步、数据订阅、异构系统集成。

3.9 一条 SQL 在 ShardingSphere 内部经历什么

从应用视角看,只是一次 PreparedStatement.executeQuery();从内核视角看,至少包含以下阶段:

flowchart LR
    A[逻辑 SQL] --> B[解析 Parse]
    B --> C[绑定 Bind]
    C --> D[路由 Route]
    D --> E[改写 Rewrite]
    E --> F[执行 Execute]
    F --> G[归并 Merge]
    G --> H[逻辑 ResultSet]

    C -.依赖.-> M[(逻辑表/列/规则元数据)]
    D -.输出.-> R[路由上下文]
    E -.输出.-> U[真实执行单元]

各阶段职责:

阶段 输入 核心工作 常见风险
解析 SQL 文本、参数 生成语法树和 SQLStatement 方言、不支持语法、解析成本
绑定 语法树、元数据 解析表、列、别名和上下文 元数据不一致、列歧义
路由 SQL 上下文、规则 计算目标库表 缺分片键、笛卡尔路由
改写 逻辑 SQL、路由结果 替换表名、补列、修正分页 SQL 膨胀、参数重排
执行 真实 SQL 集合 获取连接、并发/串行执行 连接风暴、线程和内存压力
归并 多个 ResultSet 排序、聚合、分页、去装饰 深分页、内存归并、慢消费

3.10 规则叠加顺序

一个逻辑数据库里可能同时启用分片、读写分离、加密、脱敏、影子库等规则。不要把它们理解成互相独立的“开关”,因为同一条 SQL 会经历规则链。

例如“分片 + 读写分离 + 影子库”的请求:

sequenceDiagram
    participant App as 应用
    participant SS as ShardingSphere
    participant Shard as 分片规则
    participant RW as 读写分离规则
    participant Shadow as 影子规则
    participant DB as 真实数据库

    App->>SS: 逻辑 SQL + 参数 + 压测标记
    SS->>Shard: 根据 user_id/order_id 计算分片
    Shard-->>SS: ds_1.t_order_0
    SS->>RW: 根据 SQL 类型和事务状态选主/从
    RW-->>SS: ds_1_primary
    SS->>Shadow: 判断是否命中影子算法
    Shadow-->>SS: shadow_ds_1_primary
    SS->>DB: 改写后的真实 SQL
    DB-->>SS: ResultSet / update count
    SS-->>App: 逻辑结果

规则越多,测试矩阵呈乘法增长。生产接入时应逐个启用并分别验收,不要一次把所有能力全开。

4. 核心概念

想把 ShardingSphere 用稳,先把这些概念吃透。

4.1 逻辑库

应用看到的是逻辑库,例如:

1
logic_db

它不一定对应真实数据库,而是 ShardingSphere 对外暴露的数据库名称。

4.2 真实库

真实库是实际数据库实例里的 schema,例如:

1
2
demo_ds_0
demo_ds_1

4.3 逻辑表

应用 SQL 里写的表名:

1
2
3
select *
from t_order
where user_id = 10;

这里 t_order 是逻辑表。

4.4 真实表

数据库里真实存在的表:

1
2
3
4
demo_ds_0.t_order_0
demo_ds_0.t_order_1
demo_ds_1.t_order_0
demo_ds_1.t_order_1

4.5 actualDataNodes

actualDataNodes 描述逻辑表对应哪些真实节点:

1
actualDataNodes: ds_${0..1}.t_order_${0..1}

展开后是:

1
2
3
4
ds_0.t_order_0
ds_0.t_order_1
ds_1.t_order_0
ds_1.t_order_1

4.6 分片键

分片键是决定数据落点的字段,例如:

1
2
3
4
5
user_id
order_id
tenant_id
shop_id
create_time

分片键选择是分库分表最重要的设计之一。选错分片键,后面所有查询都会开始“环游世界”。

4.7 分片策略

常见策略:

  • standard:单分片键。
  • complex:多分片键。
  • hint:通过 HintManager 指定路由。
  • none:不分片。

4.8 分片算法

常用算法:

  • INLINE:行表达式,例如 ds_${user_id % 2}
  • MOD:取模。
  • HASH_MOD:哈希取模。
  • INTERVAL:按时间间隔。
  • AUTO_INTERVAL:自动时间段。
  • COMPLEX_INLINE:复合行表达式。
  • HINT_INLINE:Hint 行表达式。
  • CLASS_BASED:自定义 Java 类算法。

4.9 绑定表

绑定表用于避免关联查询时出现笛卡尔路由。

例如:

1
2
t_order
t_order_item

如果二者都按 order_id 分片,可以配置为绑定表:

1
2
bindingTables:
- t_order,t_order_item

这样关联查询时可以保持同路由。

4.10 广播表

广播表会在所有数据源中保存一份,适合小型字典表、配置表,例如:

1
2
3
t_dict
t_region
t_address

配置:

1
2
3
- !BROADCAST
tables:
- t_address

4.11 单表

单表是被 ShardingSphere 管理,但不参与分片的表。

例如:

1
2
3
4
5
- !SINGLE
tables:
- ds_0.t_config
- ds_1.*
defaultDataSource: ds_0

ShardingSphere 5.4.0 之后单表加载方式有调整,建议显式配置单表,别指望它总是自动猜。

4.12 分布式主键

分库分表后,数据库自增主键容易重复,所以常用:

  • SNOWFLAKE
  • UUID
  • 业务自定义 ID

ShardingSphere 内置 SNOWFLAKEUUID,也可以自定义。

4.13 数据节点、分片策略和算法的关系

这三个概念最容易混淆:

1
2
3
actualDataNodes:允许路由到哪里
strategy:从 SQL 中取什么值、按什么策略调用算法
algorithm:拿到分片值后如何计算具体目标
flowchart LR
    A[SQL 条件 user_id=10] --> B[databaseStrategy.standard]
    B --> C[提取 shardingColumn=user_id]
    C --> D[调用 database_inline]
    D --> E[计算 ds_0]
    E --> F{是否属于 actualDataNodes}
    F -->|是| G[生成真实执行单元]
    F -->|否| H[路由异常]

actualDataNodes 是边界,算法不能返回边界之外的名字。自定义算法必须对返回值做存在性校验,避免悄悄路由到错误节点。

4.14 分片值条件对路由数量的影响

假设有 2 个库、每库 4 张表:

SQL 条件 可能路由数 说明
user_id = ? AND order_id = ? 1 精确库 + 精确表
user_id IN (?, ?) + 精确 order_id 1~2 命中一个或两个库
精确 user_id,无 order_id 4 单库全表
user_id,精确 order_id 2 所有库中的同尾号表
两个分片键都没有 8 全库全表
分片键范围查询 取决于算法 INLINE 通常无法精确裁剪

所以“SQL 能执行”不等于“路由合理”。代码评审时应把路由数量当作 SQL 的一部分。

4.15 绑定表为什么能避免笛卡尔路由

如果 t_ordert_order_item 都按照相同的 order_id 规则分表,配置绑定表后:

flowchart TD
    A[t_order_0] --- B[t_order_item_0]
    C[t_order_1] --- D[t_order_item_1]
    E[逻辑 JOIN] --> A
    E --> C

ShardingSphere 只生成同后缀表之间的 JOIN。未配置绑定关系时,中间件无法证明两张表的分片拓扑一致,可能尝试更多组合。

绑定表的必要条件不是“字段名相同”,而是:

  • 分片键语义一致;
  • 分片算法一致;
  • 真实节点拓扑一致;
  • 业务数据确实满足同键同片;
  • JOIN 条件包含绑定关系所需字段。

4.16 广播表不是“大表复制术”

广播表适合低频写、小数据量、各库都需要本地 JOIN 的表。常见候选:地区字典、业务枚举、少量配置。

不适合广播:

  • 高频更新库存;
  • 百万级商品主数据;
  • 大量历史地址;
  • 需要严格单点自增的表;
  • 变更依赖复杂触发器的表。

每次广播表写操作会扩散到多个数据源,规模越大,写放大和失败处理越复杂。

4.17 单表与默认数据源的工程边界

未分片表不代表可以随意散落。建议按以下规则管理:

1
2
3
4
5
业务核心表:明确配置 SHARDING / SINGLE / BROADCAST
框架表:单独数据源或明确 SINGLE
Flyway 历史表:固定到管理数据源
Quartz / XXL-JOB 表:不要随机落库
临时表:确认 ShardingSphere 与数据库方言支持

如果单表未明确配置,某些 DDL 或元数据查询可能采用单播路由,导致“开发库能跑、生产库偶发找错表”的问题。

5. Spring Boot 3.5 整合 ShardingSphere-JDBC

这一节给出完整示例:两个库,每个库两张订单表和两张明细表。

1
2
3
4
5
6
7
8
9
10
11
12
13
demo_ds_0
├── t_order_0
├── t_order_1
├── t_order_item_0
├── t_order_item_1
└── t_address

demo_ds_1
├── t_order_0
├── t_order_1
├── t_order_item_0
├── t_order_item_1
└── t_address

分片规则:

1
2
3
4
5
分库:user_id % 2
分表:order_id % 2
主键:SNOWFLAKE
绑定表:t_order,t_order_item
广播表:t_address

5.1 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

<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>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>shardingsphere-demo</artifactId>
<version>1.0.0</version>

<properties>
<java.version>21</java.version>
<shardingsphere.version>5.5.3</shardingsphere.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere.version}</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

如果你用 MyBatis,可以额外加:

1
2
3
4
5
6

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>

5.2 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8080

spring:
application:
name: shardingsphere-demo
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml

logging:
level:
org.apache.shardingsphere: info

这里没有直接写 MySQL 连接信息,因为真实数据源都放在 shardingsphere.yaml 中。

5.3 shardingsphere.yaml

放在:

1
src/main/resources/shardingsphere.yaml

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
databaseName: logic_db

mode:
type: Standalone

dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_ds_0?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
idleTimeout: 600000
maxLifetime: 1800000
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_ds_1?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
idleTimeout: 600000
maxLifetime: 1800000

rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_table_inline
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
auditStrategy:
auditorNames:
- sharding_key_required_auditor
allowHintDisable: true
t_order_item:
actualDataNodes: ds_${0..1}.t_order_item_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_item_table_inline
keyGenerateStrategy:
column: order_item_id
keyGeneratorName: snowflake
bindingTables:
- t_order,t_order_item
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
order_table_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}
allow-range-query-with-inline-sharding: false
order_item_table_inline:
type: INLINE
props:
algorithm-expression: t_order_item_${order_id % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
max-vibration-offset: 1
auditors:
sharding_key_required_auditor:
type: DML_SHARDING_CONDITIONS

- !BROADCAST
tables:
- t_address

props:
sql-show: true
sql-simple: false
kernel-executor-size: 16
max-connections-size-per-query: 1
check-table-metadata-enabled: false

注意:ShardingSphere 5.5.3 官方数据源配置示例中使用 standardJdbcUrl。如果你看旧文章,可能看到的是 jdbcUrl。新项目优先按当前官方
5.5.3 文档写。

5.4 建库建表 SQL

1
2
3
4
CREATE
DATABASE IF NOT EXISTS demo_ds_0 DEFAULT CHARACTER SET utf8mb4;
CREATE
DATABASE IF NOT EXISTS demo_ds_1 DEFAULT CHARACTER SET utf8mb4;

在两个库里分别执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
CREATE TABLE IF NOT EXISTS t_order_0
(
order_id
BIGINT
NOT
NULL,
user_id
BIGINT
NOT
NULL,
order_no
VARCHAR
(
64
) NOT NULL,
amount DECIMAL
(
18,
2
) NOT NULL,
status VARCHAR
(
32
) NOT NULL,
create_time DATETIME
(
6
) NOT NULL,
PRIMARY KEY
(
order_id
),
KEY idx_user_id
(
user_id
),
UNIQUE KEY uk_order_no
(
order_no
)
);

CREATE TABLE IF NOT EXISTS t_order_1 LIKE t_order_0;

CREATE TABLE IF NOT EXISTS t_order_item_0
(
order_item_id
BIGINT
NOT
NULL,
order_id
BIGINT
NOT
NULL,
user_id
BIGINT
NOT
NULL,
sku_code
VARCHAR
(
64
) NOT NULL,
quantity INT NOT NULL,
price DECIMAL
(
18,
2
) NOT NULL,
PRIMARY KEY
(
order_item_id
),
KEY idx_order_id
(
order_id
),
KEY idx_user_id
(
user_id
)
);

CREATE TABLE IF NOT EXISTS t_order_item_1 LIKE t_order_item_0;

CREATE TABLE IF NOT EXISTS t_address
(
id
BIGINT
NOT
NULL,
region_code
VARCHAR
(
32
) NOT NULL,
region_name VARCHAR
(
128
) NOT NULL,
PRIMARY KEY
(
id
)
);

5.5 Spring Boot 启动类

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.sharding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShardingSphereDemoApplication {

public static void main(String[] args) {
SpringApplication.run(ShardingSphereDemoApplication.class, args);
}
}

5.6 Repository 示例

这里用 JdbcTemplate,便于观察 SQL 路由,不引入额外 ORM 复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.example.sharding.order;

import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class OrderRepository {

private final JdbcTemplate jdbcTemplate;

public OrderRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public long createOrder(Long userId, String orderNo, BigDecimal amount) {
String sql = """
insert into t_order(user_id, order_no, amount, status, create_time)
values (?, ?, ?, ?, ?)
""";
jdbcTemplate.update(sql, userId, orderNo, amount, "CREATED", LocalDateTime.now());

Long orderId = jdbcTemplate.queryForObject(
"select order_id from t_order where user_id = ? and order_no = ?",
Long.class,
userId,
orderNo
);

if (orderId == null) {
throw new IllegalStateException("Order id not generated.");
}
return orderId;
}

public void createOrderItem(Long userId, Long orderId, String skuCode, int quantity, BigDecimal price) {
String sql = """
insert into t_order_item(order_id, user_id, sku_code, quantity, price)
values (?, ?, ?, ?, ?)
""";
jdbcTemplate.update(sql, orderId, userId, skuCode, quantity, price);
}

public List<OrderView> findByUserId(Long userId) {
String sql = """
select order_id, user_id, order_no, amount, status, create_time
from t_order
where user_id = ?
order by create_time desc
""";
return jdbcTemplate.query(sql, this::mapOrder, userId);
}

public OrderView findByUserIdAndOrderId(Long userId, Long orderId) {
String sql = """
select order_id, user_id, order_no, amount, status, create_time
from t_order
where user_id = ? and order_id = ?
""";
return jdbcTemplate.queryForObject(sql, this::mapOrder, userId, orderId);
}

private OrderView mapOrder(ResultSet rs, int rowNum) throws SQLException {
return new OrderView(
rs.getLong("order_id"),
rs.getLong("user_id"),
rs.getString("order_no"),
rs.getBigDecimal("amount"),
rs.getString("status"),
rs.getTimestamp("create_time").toLocalDateTime()
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.sharding.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public record OrderView(
Long orderId,
Long userId,
String orderNo,
BigDecimal amount,
String status,
LocalDateTime createTime
) {
}

5.7 Service 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.sharding.order;

import java.math.BigDecimal;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

private final OrderRepository orderRepository;

public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

@Transactional
public Long create(Long userId, String orderNo, BigDecimal amount) {
Long orderId = orderRepository.createOrder(userId, orderNo, amount);
orderRepository.createOrderItem(userId, orderId, "SKU-001", 1, amount);
return orderId;
}

public List<OrderView> listByUserId(Long userId) {
return orderRepository.findByUserId(userId);
}
}

5.8 Controller 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.sharding.order;

import java.math.BigDecimal;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/orders")
public class OrderController {

private final OrderService orderService;

public OrderController(OrderService orderService) {
this.orderService = orderService;
}

@PostMapping
public Long create(
@RequestParam Long userId,
@RequestParam String orderNo,
@RequestParam BigDecimal amount
) {
return orderService.create(userId, orderNo, amount);
}

@GetMapping("/users/{userId}")
public List<OrderView> listByUserId(@PathVariable Long userId) {
return orderService.listByUserId(userId);
}
}

5.9 测试请求

1
2
3
4
5
curl -X POST "http://localhost:8080/orders?userId=10&orderNo=NO1001&amount=99.90"
curl -X POST "http://localhost:8080/orders?userId=11&orderNo=NO1002&amount=199.90"

curl "http://localhost:8080/orders/users/10"
curl "http://localhost:8080/orders/users/11"

如果开启了:

1
2
props:
sql-show: true

日志里会看到逻辑 SQL 和真实 SQL。

5.10 推荐工程目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shardingsphere-demo/
├── compose.yaml
├── docker/
│ ├── ds0/init.sql
│ └── ds1/init.sql
├── pom.xml
└── src/
├── main/
│ ├── java/com/example/sharding/
│ │ ├── ShardingSphereDemoApplication.java
│ │ ├── order/OrderController.java
│ │ ├── order/OrderService.java
│ │ ├── order/OrderRepository.java
│ │ └── algorithm/OrderStandardShardingAlgorithm.java
│ └── resources/
│ ├── application.yml
│ └── shardingsphere.yaml
└── test/
└── java/com/example/sharding/
├── OrderRoutingIT.java
└── TransactionBoundaryIT.java

5.11 使用 Docker Compose 准备两个 MySQL 实例

为了让示例可复现,建议使用两个独立 MySQL 容器,而不是在同一个实例里只建两个 Schema。独立实例更容易观察连接数、故障隔离与跨库事务行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
services:
mysql-ds0:
image: mysql:8.4
container_name: shardingsphere-mysql-ds0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: demo_ds_0
TZ: Asia/Shanghai
ports:
- "33061:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
- --default-time-zone=+08:00
volumes:
- ds0-data:/var/lib/mysql
- ./docker/ds0/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
interval: 5s
timeout: 3s
retries: 30

mysql-ds1:
image: mysql:8.4
container_name: shardingsphere-mysql-ds1
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: demo_ds_1
TZ: Asia/Shanghai
ports:
- "33062:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
- --default-time-zone=+08:00
volumes:
- ds1-data:/var/lib/mysql
- ./docker/ds1/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
interval: 5s
timeout: 3s
retries: 30

volumes:
ds0-data:
ds1-data:

启动:

1
2
docker compose up -d
docker compose ps

对应的 shardingsphere.yaml 需要使用:

1
standardJdbcUrl: jdbc:mysql://127.0.0.1:33061/demo_ds_0?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

和:

1
standardJdbcUrl: jdbc:mysql://127.0.0.1:33062/demo_ds_1?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

5.12 一份格式完整的初始化 SQL

将下面 SQL 分别放入 docker/ds0/init.sqldocker/ds1/init.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
order_no VARCHAR(64) NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(6) NOT NULL,
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (order_id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_create_time (user_id, create_time DESC),
KEY idx_status_create_time (status, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS t_order_1 LIKE t_order_0;

CREATE TABLE IF NOT EXISTS t_order_item_0 (
order_item_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
sku_code VARCHAR(64) NOT NULL,
quantity INT NOT NULL,
price DECIMAL(18, 2) NOT NULL,
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (order_item_id),
KEY idx_order_id (order_id),
KEY idx_user_order (user_id, order_id),
KEY idx_sku_code (sku_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS t_order_item_1 LIKE t_order_item_0;

CREATE TABLE IF NOT EXISTS t_address (
id BIGINT NOT NULL,
region_code VARCHAR(32) NOT NULL,
region_name VARCHAR(128) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_region_code (region_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

所有真实分片表必须保持:

  • 列名和列顺序一致;
  • 数据类型一致;
  • 主键和唯一约束一致;
  • 索引定义一致;
  • 字符集、排序规则一致;
  • 默认值和精度一致。

否则会出现某些分片正常、某些分片报错,或者归并时类型不一致。

5.13 使用 KeyHolder 获取 ShardingSphere 生成的主键

原示例插入后通过 order_no 回查主键,能工作,但多一次数据库往返,并依赖 order_no 唯一。更推荐使用 JDBC generated keys:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Repository
public class OrderRepository {

private final JdbcTemplate jdbcTemplate;

public OrderRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public long createOrder(Long userId, String orderNo, BigDecimal amount) {
String sql = """
INSERT INTO t_order(user_id, order_no, amount, status, create_time)
VALUES (?, ?, ?, ?, ?)
""";

GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
int affected = jdbcTemplate.update(connection -> {
PreparedStatement statement = connection.prepareStatement(
sql,
Statement.RETURN_GENERATED_KEYS
);
statement.setLong(1, userId);
statement.setString(2, orderNo);
statement.setBigDecimal(3, amount);
statement.setString(4, "CREATED");
statement.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now()));
return statement;
}, keyHolder);

if (affected != 1) {
throw new IllegalStateException("Unexpected affected rows: " + affected);
}

Number key = keyHolder.getKey();
if (key == null) {
throw new IllegalStateException("Sharding key generator returned no key.");
}
return key.longValue();
}
}

需要导入:

1
2
3
4
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import org.springframework.jdbc.support.GeneratedKeyHolder;

5.14 使用 MyBatis 时的 Mapper 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Mapper
public interface OrderMapper {

@Insert("""
INSERT INTO t_order(user_id, order_no, amount, status, create_time)
VALUES (#{userId}, #{orderNo}, #{amount}, #{status}, #{createTime})
""")
@Options(useGeneratedKeys = true, keyProperty = "orderId", keyColumn = "order_id")
int insert(OrderDO order);

@Select("""
SELECT order_id, user_id, order_no, amount, status, create_time
FROM t_order
WHERE user_id = #{userId}
AND order_id = #{orderId}
""")
OrderDO findById(@Param("userId") Long userId,
@Param("orderId") Long orderId);
}

实体仍使用逻辑表语义,不要在 Mapper 中拼接 t_order_0t_order_1

5.15 路由验证不能只看查询结果

至少验证三个层面:

  1. 逻辑正确性:接口返回结果正确;
  2. 物理落点正确性:数据进入预期库表;
  3. 路由数量正确性:没有多余全路由。

人工验证:

1
2
3
4
5
mysql -h127.0.0.1 -P33061 -uroot -proot demo_ds_0 \
-e "SELECT order_id,user_id,order_no FROM t_order_0 UNION ALL SELECT order_id,user_id,order_no FROM t_order_1;"

mysql -h127.0.0.1 -P33062 -uroot -proot demo_ds_1 \
-e "SELECT order_id,user_id,order_no FROM t_order_0 UNION ALL SELECT order_id,user_id,order_no FROM t_order_1;"

自动化测试可以抓取 sql-show 日志,或者在 Proxy 中使用 PREVIEW SQL 验证路由。

5.16 启动阶段的健康检查

应用成功启动不代表所有真实数据源都可用。建议增加启动自检:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class ShardingDataSourceVerifier implements ApplicationRunner {

private final JdbcTemplate jdbcTemplate;

public ShardingDataSourceVerifier(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@Override
public void run(ApplicationArguments args) {
Integer value = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
if (!Integer.valueOf(1).equals(value)) {
throw new IllegalStateException("ShardingSphere datasource verification failed");
}
}
}

更严格的生产自检应覆盖每个真实数据源。可以在部署流水线中直接连接真实库执行 SELECT 1、检查表结构摘要和索引摘要,不建议让在线应用启动时做高成本全量扫描。

6. 数据分片深度讲解

6.1 路由过程

一次查询大致经历:

flowchart LR
    A[逻辑 SQL] --> B[SQL 解析]
    B --> C[SQL 路由]
    C --> D[SQL 改写]
    D --> E[并发执行]
    E --> F[结果归并]
    F --> G[返回应用]

例如:

1
2
3
4
select *
from t_order
where user_id = 10
and order_id = 10086;

根据规则:

1
2
database = ds_${user_id % 2}
table = t_order_${order_id % 2}

会路由到:

1
ds_0.t_order_0

如果 SQL 缺少分片键:

1
2
3
select *
from t_order
where status = 'CREATED';

ShardingSphere 无法判断数据在哪个库哪张表,就可能路由到所有真实表。这就是全路由。全路由不是不能用,但高频接口里出现全路由,数据库基本就开始冒烟了。

6.2 分片键选择原则

分片键建议满足:

  • 高频查询条件里一定带它。
  • 数据分布足够均匀。
  • 业务上不容易变。
  • 能让关联表同路由。
  • 能减少跨库事务。

订单系统常见选择:

1
2
3
4
5
user_id
buyer_id
tenant_id
shop_id
order_id

如果大多数查询都是按用户查订单,user_id 是不错的分库键。如果订单详情按 order_id 查很多,则要考虑 order_id
是否也参与分片,或者做订单号到分片位置的映射。

6.3 分库键和分表键可以不同吗

可以。

例如:

1
2
分库:user_id
分表:order_id

优点:

  • 用户维度数据落到固定库。
  • 订单维度在库内分散到不同表。

缺点:

  • 只带 order_id 不带 user_id 的查询,可能无法精准定位库。

如果业务经常只用 order_id 查订单,建议:

  • order_id 本身携带分片信息。
  • 建立 order_id -> user_idorder_id -> ds 的路由索引。
  • 查询接口强制要求传入 user_id
  • 使用 Hint 强制路由。

6.4 INLINE 算法

最常见配置:

1
2
3
4
5
6
7
8
9
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
order_table_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}

优点:

  • 简单。
  • 可读性好。
  • 适合取模场景。

缺点:

  • 默认不适合范围查询。
  • 复杂逻辑写起来不优雅。

如果设置:

1
allow-range-query-with-inline-sharding: true

范围查询会被允许,但通常会全路由,不要误以为它能自动精准范围裁剪。

6.5 按时间分片

按月表:

1
2
3
t_order_202601
t_order_202602
t_order_202603

示例思路:

1
2
3
4
5
6
7
8
9
10
shardingAlgorithms:
order_interval:
type: INTERVAL
props:
datetime-pattern: yyyy-MM-dd HH:mm:ss
datetime-lower: 2026-01-01 00:00:00
datetime-upper: 2027-01-01 00:00:00
sharding-suffix-pattern: yyyyMM
datetime-interval-amount: 1
datetime-interval-unit: MONTHS

时间分片适合日志、流水、账单,但要注意:

  • 跨月查询会命中多张表。
  • 归档策略要提前设计。
  • 新月份表要提前创建。
  • 历史分片和实时分片的访问频率不同。

6.6 复合分片

当分片需要多个字段:

1
tenant_id + order_id

可以使用 complexCOMPLEX_INLINE

1
2
3
4
5
6
7
8
9
10
11
tableStrategy:
complex:
shardingColumns: tenant_id,order_id
shardingAlgorithmName: complex_order_inline

shardingAlgorithms:
complex_order_inline:
type: COMPLEX_INLINE
props:
sharding-columns: tenant_id,order_id
algorithm-expression: t_order_${(tenant_id + order_id) % 4}

复合分片不要为了“看起来强大”滥用。多数情况下,一个好的主分片键比多个字段混算更容易维护。

6.7 CLASS_BASED 自定义分片

复杂规则可以用 Java 类。

配置:

1
2
3
4
5
6
shardingAlgorithms:
order_custom:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.example.sharding.algorithm.OrderStandardShardingAlgorithm

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.sharding.algorithm;

import java.util.Collection;
import java.util.Properties;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;

public final class OrderStandardShardingAlgorithm implements StandardShardingAlgorithm<Long> {

private Properties props;

@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
long value = shardingValue.getValue();
String suffix = String.valueOf(value % availableTargetNames.size());
return availableTargetNames.stream()
.filter(name -> name.endsWith(suffix))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No target matched for value: " + value));
}

@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
return availableTargetNames;
}

@Override
public void init(Properties props) {
this.props = props;
}
}

范围查询这里直接返回全部目标,表示全路由。生产里如果要精准范围路由,需要自己根据上下界计算目标表集合。

6.8 Hint 强制路由

当分片键不在 SQL 里,而是在业务上下文中,可以使用 Hint。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.sql.DataSource;
import org.apache.shardingsphere.infra.hint.HintManager;

public class HintQueryExample {

private final DataSource dataSource;

public HintQueryExample(DataSource dataSource) {
this.dataSource = dataSource;
}

public void queryWithHint() throws Exception {
String sql = "select * from t_order";

try (HintManager hintManager = HintManager.getInstance();
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {

hintManager.addDatabaseShardingValue("t_order", 1L);
hintManager.addTableShardingValue("t_order", 2L);

try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
// handle row
}
}
}
}
}

Hint 使用 ThreadLocal,务必使用 try-with-resources 自动关闭。不关闭会污染当前线程后续请求,这种问题很阴,排查起来让人想喝三杯冰水冷静一下。

6.9 SQL 解析与绑定:不只是把字符串切开

SQL 解析需要识别数据库方言、表、列、条件、占位符、子查询、聚合、排序、分页等信息。随后绑定阶段结合元数据确定:

  • user_id 属于哪张表;
  • 别名 o 指向哪个逻辑表;
  • * 具体包含哪些列;
  • JOIN 条件中的列是否歧义;
  • ORDER BY 引用的是列名、别名还是序号;
  • 参数索引与分片条件如何对应。

这也是为什么分片环境中元数据一致性非常重要。一个真实表少一列,可能不是立即在启动时报错,而是在特定 SQL 路由到该表时才暴露。

6.10 路由引擎的几种典型路径

ShardingSphere 路由可以从工程角度理解为:

路由类型 典型触发条件 代价
Direct Route Hint 明确指定,场景满足直接路由条件 最低,但依赖业务正确提供 Hint
Standard Route 单表或绑定表,带可计算分片条件 推荐路径
Cartesian Route 非绑定表 JOIN、分片拓扑不一致 路由组合可能快速膨胀
Full DB/Table Route 缺少分片键的 DQL/DML 扫描所有相关真实表
Unicast Route DESCRIBE 等只需任一真实表 单节点执行
Block Route 不应下发到真实库的逻辑操作 被阻断或在逻辑层处理

路由数量近似可以表达为:

1
route_count = matched_databases × matched_tables_per_database × join_combinations

当 JOIN 涉及多张非绑定分片表时,join_combinations 可能成为最危险的一项。

6.11 SQL 改写具体改了什么

改写不只是把 t_order 替换成 t_order_0。常见改写包括:

  1. 标识符改写:逻辑表、Schema、索引名替换为真实名称;
  2. 主键补全:INSERT 未传分布式主键时生成并补入;
  3. 派生列补全:ORDER BY/GROUP BY 所需列未在 SELECT 中时补列;
  4. AVG 拆分:跨分片 AVG 改写为 SUM + COUNT 后归并;
  5. 分页修正:深分页扩大每个分片的拉取范围;
  6. 批量语句拆分:一条批量 INSERT 按路由目标拆成多条真实 SQL;
  7. 单节点优化:只命中一个节点时尽量减少不必要归并。

AVG 不能简单平均每个分片的平均值。例如:

1
2
3
4
分片 A:1 条,平均值 100
分片 B:99 条,平均值 1
错误:(100 + 1) / 2 = 50.5
正确:(100 + 99) / (1 + 99) = 1.99

所以 SQL 会在各分片计算 SUMCOUNT,最后统一计算。

6.12 执行引擎:连接和内存之间的取舍

假设一条 SQL 路由到同一数据库实例中的 200 张表:

  • 每张表一个连接,并行快,但可能瞬间占用 200 个连接;
  • 一个连接串行跑 200 条 SQL,连接省,但延迟高,而且结果可能需要先加载到内存。

ShardingSphere 执行引擎要在连接资源、并发度和归并内存之间平衡。max-connections-size-per-query 是重要的保护参数,不应盲目调大。

flowchart TD
    A[路由得到 N 个执行单元] --> B{每查询允许的最大连接数}
    B -->|连接充足| C[更多并行执行]
    B -->|严格受限| D[同数据源内串行复用]
    C --> E[优先流式归并]
    D --> F[可能增加内存归并需求]
    E --> G[低延迟但连接消耗高]
    F --> H[连接稳定但延迟/内存上升]

6.13 归并引擎详解

归并能力可分为:

  • 遍历归并;
  • ORDER BY 归并;
  • GROUP BY 归并;
  • 聚合归并;
  • 分页归并。

结构上又可分为:

  • 流式归并:ResultSet 逐行读取,通常更省内存;
  • 内存归并:先加载结果再统一计算;
  • 装饰器归并:在基础归并上叠加分页、聚合等能力。

跨分片 ORDER BY 可以把每个分片看成一个已排序列表,ShardingSphere 用类似优先队列的方式取出全局最小/最大值:

flowchart LR
    A[分片0有序结果] --> Q[优先队列]
    B[分片1有序结果] --> Q
    C[分片2有序结果] --> Q
    Q --> R[每次弹出全局下一行]
    R --> M[移动对应 ResultSet 游标]
    M --> Q

6.14 深分页为什么昂贵

逻辑 SQL:

1
2
3
4
SELECT *
FROM t_order
ORDER BY create_time DESC
LIMIT 100000, 20;

为了保证全局正确性,每个分片通常都要提供足够多的候选记录,近似改写为:

1
LIMIT 0, 100020

如果有 16 个分片,潜在读取量接近:

1
16 × 100020 = 1,600,320 行候选数据

更推荐游标分页:

1
2
3
4
5
6
7
SELECT order_id, user_id, order_no, create_time
FROM t_order
WHERE user_id = :userId
AND (create_time < :lastCreateTime
OR (create_time = :lastCreateTime AND order_id < :lastOrderId))
ORDER BY create_time DESC, order_id DESC
LIMIT 20;

游标分页必须使用稳定、唯一的排序组合,避免同一时间戳导致重复或漏数据。

6.15 全路由审计

当前示例配置了:

1
2
3
4
auditStrategy:
auditorNames:
- sharding_key_required_auditor
allowHintDisable: true

审计器的价值是把“可能很慢”提前变成“明确拒绝”。但使用前需要划分接口:

  • 在线核心接口:建议强制带分片键;
  • 后台低频管理查询:可以走独立查询服务;
  • 离线任务:可通过批次扫描各分片;
  • 数据修复:使用受控 Hint 或直连真实库工具;
  • 报表:进入 ClickHouse、Doris、Elasticsearch 或数仓。

不要为了让后台一个搜索框方便,就把整个在线库的分片审计关掉。

6.16 分片算法的边界测试

自定义算法至少测试:

1
2
3
4
5
6
7
8
9
10
11
12
0
1
-1
Long.MAX_VALUE
Long.MIN_VALUE
null
字符串数字
非法字符串
IN 空集合
IN 大集合
范围上下界相等
跨全部分片的范围

取模算法若可能接收负数,应使用 Math.floorMod(value, shardCount),而不是直接 value % shardCount

6.17 批量 INSERT 的路由与拆分

1
2
3
4
5
INSERT INTO t_order(user_id, order_no, amount, status, create_time)
VALUES
(10, 'A', 10, 'CREATED', NOW()),
(11, 'B', 20, 'CREATED', NOW()),
(12, 'C', 30, 'CREATED', NOW());

三行可能路由到不同库表,ShardingSphere 需要按目标拆分。批量越大,单次解析、改写和参数复制越重。

生产建议:

  • 批次控制在可压测的范围内,例如 100~1000 条,而不是无限堆积;
  • 同一批尽量按分片键预分组;
  • 关注单包大小、数据库 max_allowed_packet
  • 失败时明确整批重试还是分组重试;
  • 幂等键必须能跨重试生效。

7. 读写分离

7.1 YAML 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
databaseName: readwrite_db

mode:
type: Standalone

dataSources:
write_ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_master?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
read_ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_slave_0?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
read_ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_slave_1?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root

rules:
- !READWRITE_SPLITTING
dataSourceGroups:
readwrite_ds:
writeDataSourceName: write_ds
readDataSourceNames:
- read_ds_0
- read_ds_1
transactionalReadQueryStrategy: PRIMARY
loadBalancerName: random
loadBalancers:
random:
type: RANDOM

props:
sql-show: true

应用连接的是逻辑数据源 readwrite_ds,不是直接连 write_dsread_ds_0

7.2 负载均衡算法

内置算法:

  • RANDOM:随机。
  • ROUND_ROBIN:轮询。
  • WEIGHT:权重。

权重示例:

1
2
3
4
5
6
loadBalancers:
weight:
type: WEIGHT
props:
read_ds_0: 2
read_ds_1: 1

7.3 事务内读策略

1
transactionalReadQueryStrategy: PRIMARY

常见取值:

  • PRIMARY:事务内读请求路由到主库,默认推荐。
  • FIXED:事务内固定路由到某个数据源。
  • DYNAMIC:事务内动态路由。

普通 MySQL 主从复制存在延迟,所以强烈建议事务内读走主库。

7.4 强制主库路由

某些查询虽然是 SELECT,但业务上必须读主库:

1
2
3
4
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly();
// execute select sql
}

例如:

  • 刚写完马上查。
  • 支付状态确认。
  • 库存扣减确认。
  • 后台人工审核刚提交的数据。

7.5 读写分离解决的是吞吐,不是数据一致性

主从复制通常是异步或半同步。写入主库后,数据需要经过 binlog、网络传输、relay log 和从库回放,延迟可能从毫秒波动到秒级甚至更高。

sequenceDiagram
    participant App as 应用
    participant Master as 主库
    participant Binlog as Binlog复制
    participant Slave as 从库

    App->>Master: INSERT order
    Master-->>App: COMMIT 成功
    Master->>Binlog: 生成并发送日志
    App->>Slave: 立即 SELECT
    Slave-->>App: 可能查不到
    Binlog->>Slave: 回放完成
    App->>Slave: 再次 SELECT
    Slave-->>App: 查到数据

7.6 一致性等级按业务分类

场景 推荐读路径 原因
创建订单后返回详情 主库 必须读己之写
支付结果确认 主库 状态错误代价高
库存扣减后确认 主库 防止超卖判断错误
用户浏览历史订单 从库可接受 通常允许轻微延迟
运营报表 从库/OLAP 吞吐优先
配置发布后校验 主库或版本确认 需要确认最新版本

不要全局强制所有 SELECT 走主库,否则读写分离形同虚设;也不要全局所有 SELECT 走从库,否则一致性事故只是时间问题。

7.7 会话级“读己之写”设计

除了事务内读主库,可以使用短期主库粘滞:

1
2
3
写入成功 -> 在请求上下文/Redis 记录 user_id 最近写时间
读取请求 -> 若距写入小于 3 秒,则强制主库
超过窗口 -> 恢复从库

伪代码:

1
2
3
4
5
6
7
8
9
10
11
public <T> T readOrder(Long userId, Supplier<T> query) {
boolean recentlyWritten = writeMarkerService.isRecentlyWritten(userId);
if (!recentlyWritten) {
return query.get();
}

try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly();
return query.get();
}
}

窗口值不能拍脑袋,应根据 P99 主从延迟确定,并设置最大保护值。

7.8 从库故障与负载均衡

读库并不是越多越好。每增加一个从库,都增加:

  • 复制延迟差异;
  • 数据不一致窗口;
  • 连接池数量;
  • 监控和故障摘除复杂度;
  • 权重配置错误风险。

需要监控每个副本:

1
2
3
4
5
6
replication_lag_seconds
read_only / super_read_only
last_io_error
last_sql_error
connection_usage
query_latency_p95/p99

ShardingSphere 负责选择配置中的读数据源,但数据库复制拓扑、故障转移和复制修复仍需数据库层或云数据库承担。

7.9 @Transactional(readOnly = true) 不等于一定走从库

Spring 的 readOnly 主要是事务提示;ShardingSphere 的读写路由还会结合 SQL 类型、事务状态和 transactionalReadQueryStrategy。在配置 PRIMARY 时,即使事务只执行 SELECT,也可能走主库。

因此不要用下面这种直觉做架构保证:

1
2
readOnly=true -> 一定从库
readOnly=false -> 一定主库

真正路由结果必须通过日志或 PREVIEW 验证。

8. 分片 + 读写分离混合规则

典型架构:

1
2
3
4
5
6
7
8
9
ds_0:
master_0
slave_0_0
slave_0_1

ds_1:
master_1
slave_1_0
slave_1_1

逻辑上先分库,再在每个分库组里做读写分离。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
dataSources:
master_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_master_0
username: root
password: root
slave_0_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_slave_0_0
username: root
password: root
master_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_master_1
username: root
password: root
slave_1_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_slave_1_0
username: root
password: root

rules:
- !READWRITE_SPLITTING
dataSourceGroups:
ds_0:
writeDataSourceName: master_0
readDataSourceNames:
- slave_0_0
transactionalReadQueryStrategy: PRIMARY
loadBalancerName: random
ds_1:
writeDataSourceName: master_1
readDataSourceNames:
- slave_1_0
transactionalReadQueryStrategy: PRIMARY
loadBalancerName: random
loadBalancers:
random:
type: RANDOM

- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_table_inline
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
order_table_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}

这里 actualDataNodes 里的 ds_0ds_1 是读写分离逻辑数据源名称,不是物理库名称。

8.1 混合规则的完整拓扑

flowchart TD
    App[Spring Boot 应用] --> SS[ShardingSphere-JDBC]
    SS --> Shard{按 user_id 分库}
    Shard --> G0[逻辑组 ds_0]
    Shard --> G1[逻辑组 ds_1]

    G0 --> RW0{读写路由}
    RW0 --> M0[(master_0)]
    RW0 --> S00[(slave_0_0)]
    RW0 --> S01[(slave_0_1)]

    G1 --> RW1{读写路由}
    RW1 --> M1[(master_1)]
    RW1 --> S10[(slave_1_0)]
    RW1 --> S11[(slave_1_1)]

    M0 --> T00[t_order_0]
    M0 --> T01[t_order_1]
    M1 --> T10[t_order_0]
    M1 --> T11[t_order_1]

逻辑路由顺序可以理解为:先确定分库逻辑组,再在组内选择写库/读库,最后执行表路由。配置中 actualDataNodes 使用的是读写分离逻辑数据源名称。

8.2 配置组合时最容易犯的错误

  • actualDataNodes 写成物理主库名,绕过读写分离逻辑组;
  • 两个读写组复用了同名底层数据源;
  • 主从库表结构不一致;
  • 从库没有对应分片表;
  • 事务读策略设置为从库,但业务要求读己之写;
  • 连接池按物理数据源数倍增后超过数据库上限;
  • 故障切换后,ShardingSphere 配置仍指向旧主库。

8.3 连接池总量计算

假设:

1
2
3
4
应用副本数:12
分库数:4
每个分库:1 主 2 从
每个物理数据源 maximumPoolSize:15

理论连接上限:

1
12 × 4 × 3 × 15 = 2160

还没有计算迁移任务、管理工具、监控、其他服务和数据库保留连接。组合规则上线前必须做全局连接预算。

9. 分布式主键设计

9.1 SNOWFLAKE

1
2
3
4
5
6
7
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
max-vibration-offset: 1
max-tolerate-time-difference-milliseconds: 10

字段配置:

1
2
3
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake

注意点:

  • 如果用生成出来的 ID 再取模分片,建议关注 max-vibration-offset
  • 集群模式下 worker-id 可以由系统自动生成。
  • 单机模式下多实例部署要确保 worker-id 不重复。
  • 时钟回拨会影响雪花 ID,服务器时间同步必须做好。

9.2 UUID

1
2
3
keyGenerators:
uuid:
type: UUID

优点:

  • 不依赖机器号。
  • 多节点天然不重复。

缺点:

  • 字符串主键较长。
  • 索引局部性较差。
  • 排查问题不如 Long 顺手。

9.3 业务建议

对于订单、结算、交易类系统,我更推荐:

1
BIGINT + SNOWFLAKE

如果外部暴露 ID,可以另加业务单号:

1
2
order_id: 内部主键,BIGINT
order_no: 外部单号,VARCHAR

不要把数据库主键、业务单号、幂等号、支付流水号全部混成一个字段。字段少了不代表架构干净,有时候只是以后的人要替你擦桌子。

9.4 Snowflake ID 的结构与分片关系

Snowflake ID 通常由时间、机器标识和序列组成。不同实现位数可能有差异,但核心目标相同:在分布式节点上生成大致有序、全局唯一的长整型 ID。

flowchart LR
    A[1 bit<br/>符号位] --> B[41 bits<br/>时间戳差值]
    B --> C[10 bits<br/>Worker 标识]
    C --> D[12 bits<br/>序列号]

需要注意:如果直接用 Snowflake ID 末位取模分表,短时间内低位分布可能受序列生成方式影响。max-vibration-offset 用于改善分片值的离散程度,但不能替代真实压测。

9.5 Worker ID 管理

多实例部署时,worker ID 冲突会导致主键重复。可选方案:

  1. Cluster 模式由治理能力分配;
  2. Kubernetes StatefulSet 序号映射;
  3. 配置中心分配并加租约;
  4. 数据库表抢占;
  5. 直接使用独立 ID 服务。

不推荐:

1
2
3
所有实例都写 worker-id: 1
随机生成 worker-id 且不检查冲突
使用 Pod IP 最后一段但网络可能重复

9.6 时钟回拨

必须部署 NTP/chrony 并监控时钟偏移。发生回拨时,根据实现可能等待、拒绝生成或在容忍窗口内处理。

建议监控:

1
2
3
node_clock_offset_milliseconds
id_generation_errors_total
duplicate_key_errors_total

9.7 ID 是否应该承载路由信息

三种常见模式:

模式 查询便利性 扩容灵活性 复杂度
user_id 分库,order_id 分表 用户查询好
order_id 同时分库分表 订单详情好
ID 中编码逻辑槽位 详情可直达

在 ID 中编码“物理库编号”会让扩容困难;更合理的是编码稳定的逻辑槽位,再通过槽位映射到物理节点。

10. 数据加密

10.1 加密解决什么问题

数据加密解决的是“数据库中不直接保存明文敏感数据”。

适合字段:

  • 手机号。
  • 身份证号。
  • 银行卡号。
  • 邮箱。
  • 地址。
  • 真实姓名。

10.2 表结构示例

1
2
3
4
5
6
7
8
9
10
CREATE TABLE t_user
(
user_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
phone_cipher VARCHAR(256) NULL,
phone_hash VARCHAR(64) NULL,
email_cipher VARCHAR(256) NULL,
PRIMARY KEY (user_id),
KEY idx_phone_hash (phone_hash)
);

逻辑字段是:

1
2
phone
email

真实字段是:

1
2
3
phone_cipher
phone_hash
email_cipher

10.3 YAML 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
rules:
- !ENCRYPT
tables:
t_user:
columns:
phone:
cipher:
name: phone_cipher
encryptorName: aes_encryptor
assistedQuery:
name: phone_hash
encryptorName: md5_encryptor
email:
cipher:
name: email_cipher
encryptorName: aes_encryptor
encryptors:
aes_encryptor:
type: AES
props:
aes-key-value: 123456abc
digest-algorithm-name: SHA-1
md5_encryptor:
type: MD5
props:
salt: user_phone_salt

10.4 业务 SQL

业务仍然写逻辑字段:

1
2
insert into t_user(user_id, username, phone, email)
values (1, 'Mario', '13800001111', 'mario@example.com');

查询也写逻辑字段:

1
2
3
select user_id, username, phone, email
from t_user
where phone = '13800001111';

ShardingSphere 会改写到真实密文字段。

10.5 加密实践建议

  • 密钥不要明文写在 Git 仓库。
  • 密钥建议接入 KMS、环境变量、配置中心加密能力。
  • 加密字段长度要预留足够。
  • 查询字段需要辅助查询列,否则等值查询会很难做。
  • 模糊查询加密成本高,能不用就不用。
  • 旧数据加密迁移要设计双写、回填、校验、切换步骤。

10.6 加密查询的真实代价

加密后的字段通常失去数据库原生索引能力。等值查询可通过确定性加密或辅助查询列实现,但要理解安全权衡:

  • 确定性密文会暴露“相同明文产生相同密文”的频率特征;
  • Hash 辅助列适合等值查询,不能解密;
  • 范围查询很难保持原语义;
  • LIKE 查询通常需要专用模糊索引或应用层重构;
  • 排序、聚合在密文上通常没有业务意义。

10.7 密钥管理架构

flowchart LR
    App[Spring Boot] --> SS[ShardingSphere Encrypt Rule]
    SS --> KMS[KMS / Vault]
    KMS --> SS
    SS --> DB[(密文列 + 辅助查询列)]
    CI[CI/CD] -.不保存明文密钥.-> App

建议:

  • Git 仓库只保存密钥引用,不保存真实密钥;
  • 不同环境使用不同密钥;
  • 密钥读取权限与数据库权限分离;
  • 建立密钥轮换版本;
  • 审计谁在何时读取过密钥;
  • 备份恢复时同步验证密钥可用性。

10.8 老数据加密迁移步骤

flowchart TD
    A[新增 cipher/hash 列] --> B[上线兼容读写逻辑]
    B --> C[新数据写入密文]
    C --> D[批量回填历史数据]
    D --> E[按主键/摘要校验]
    E --> F[灰度切换逻辑列]
    F --> G[观察错误和查询命中率]
    G --> H[清理明文列或收紧权限]

回填任务必须:

  • 按主键分批;
  • 可暂停、可续跑;
  • 幂等;
  • 限速;
  • 记录失败主键;
  • 校验密文可解密、Hash 可命中;
  • 在切换前完成备份。

10.9 密钥轮换

密钥轮换不能直接把配置中的 key 改掉,否则旧密文无法解密。常见做法是:

1
2
3
phone_cipher_v1
phone_cipher_v2
phone_key_version

先支持双版本解密,新写入使用 v2,再后台重加密旧数据,校验完成后停止 v1 写入,最后下线旧密钥。

11. 数据脱敏

11.1 脱敏和加密的区别

1
2
加密:保护数据库存储,不让库里出现明文。
脱敏:保护查询展示,不让返回结果暴露完整敏感信息。

11.2 YAML 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
rules:
- !MASK
tables:
t_user:
columns:
password:
maskAlgorithm: md5_mask
email:
maskAlgorithm: mask_before_special_chars_mask
telephone:
maskAlgorithm: keep_first_n_last_m_mask
maskAlgorithms:
md5_mask:
type: MD5
mask_before_special_chars_mask:
type: MASK_BEFORE_SPECIAL_CHARS
props:
special-chars: '@'
replace-char: '*'
keep_first_n_last_m_mask:
type: KEEP_FIRST_N_LAST_M
props:
first-n: 3
last-m: 4
replace-char: '*'

11.3 使用建议

  • 后台管理系统可以用脱敏降低误泄露风险。
  • 不同角色需要不同脱敏策略时,建议在应用层结合权限控制。
  • 脱敏不是权限系统,不要用脱敏替代鉴权。
  • 审计日志里也要注意敏感字段,不然数据库脱敏了,日志又把数据泄露出去。

11.4 脱敏应放在哪一层

层级 优点 缺点
ShardingSphere MASK 对 SQL 透明、规则统一 难表达复杂角色权限
应用 DTO 可按角色、场景精细控制 容易漏接口
API Gateway 可统一出口 不理解所有业务语义
前端 展示灵活 不能作为安全边界

推荐组合:数据库存储加密 + ShardingSphere/应用层脱敏 + 权限控制 + 日志脱敏。前端脱敏只能改善展示,不能阻止接口泄漏。

11.5 日志和异常中的敏感数据

重点检查:

  • sql-show 是否打印手机号、身份证;
  • MyBatis 参数日志;
  • 异常堆栈中的 SQL 参数;
  • Controller 请求体日志;
  • MQ 消息体;
  • 链路追踪 Span Attribute;
  • 数据修复脚本输出;
  • APM 慢 SQL 样本。

生产环境建议关闭完整 SQL 参数打印,改为 SQL 指纹 + 参数类型 + 路由数。

12. 影子库

12.1 适用场景

影子库适合全链路压测:

  • 生产流量和压测流量共用应用链路。
  • 压测数据不能污染生产库。
  • 需要模拟真实环境下的数据库访问。

12.2 YAML 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
dataSources:
ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_prod?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
shadow_ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
standardJdbcUrl: jdbc:mysql://127.0.0.1:3306/order_shadow?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root

rules:
- !SHADOW
dataSources:
shadowDataSource:
productionDataSourceName: ds
shadowDataSourceName: shadow_ds
tables:
t_order:
dataSourceNames:
- shadowDataSource
shadowAlgorithmNames:
- user_id_regex_match_algorithm
- sql_hint_algorithm
shadowAlgorithms:
user_id_regex_match_algorithm:
type: REGEX_MATCH
props:
operation: insert
column: user_id
regex: "[1]"
sql_hint_algorithm:
type: SQL_HINT

12.3 SQL Hint 示例

1
2
3
/* SHARDINGSPHERE_HINT: SHADOW=true */
insert into t_order(order_id, user_id, order_no, amount, status, create_time)
values (1001, 1, 'PT1001', 10.00, 'CREATED', now());

12.4 注意事项

  • 影子库表结构要和生产库保持一致。
  • 压测数据要有清理策略。
  • 压测标记要贯穿 HTTP、MQ、RPC、SQL。
  • 不要让影子规则误伤正常生产流量。

12.5 影子标记必须贯穿全链路

只有数据库层识别压测标记是不够的。一个完整压测请求可能经过:

flowchart LR
    A[HTTP Header] --> B[Gateway]
    B --> C[RPC Metadata]
    C --> D[业务服务]
    D --> E[MQ Header]
    E --> F[消费者]
    F --> G[SQL Hint / 影子字段]
    G --> H[(影子数据库)]

任意一环丢标,压测流量都可能进入生产数据。

12.6 影子库上线前验证

  • 正常流量 100% 进入生产库;
  • 带标流量 100% 进入影子库;
  • UPDATE/DELETE 不误伤生产数据;
  • DDL 变更同步到影子库;
  • 影子库容量和索引接近生产;
  • 影子数据有独立账号、清理和备份策略;
  • 压测结束后标记不会残留在线程池、消息或缓存中。

Hint 基于线程上下文时,必须使用 try-with-resources,虚拟线程、异步任务和 Reactor 场景还要验证上下文传播,不能默认 ThreadLocal 会自动跨边界。

13. 分布式事务

13.1 三种模式

1
2
transaction:
defaultType: LOCAL

ShardingSphere 支持:

模式 说明 建议
LOCAL 本地事务 Spring Boot 3.5 默认优先使用
XA 强一致分布式事务 Spring Boot 3.x 场景谨慎,需验证兼容性
BASE 柔性事务,常见为 Seata 适合最终一致性场景

13.2 LOCAL 模式

1
2
transaction:
defaultType: LOCAL

LOCAL 模式下,如果一次事务只命中一个真实库,效果接近普通本地事务。

如果一次事务写多个真实库,就不要把它理解成强一致分布式事务。业务上要能接受异常场景下的补偿、对账和恢复。

13.3 XA 模式

1
2
3
transaction:
defaultType: XA
providerType: Narayana

Spring Boot 3.5 下,XA 要非常谨慎。官方文档对 Spring Boot OSS 3 已给出限制提醒。生产使用前至少要验证:

  • 启动兼容性。
  • 多库 commit。
  • 某个库 commit 失败。
  • 网络闪断。
  • 应用重启恢复。
  • 连接池回收。
  • 事务超时。
  • 压测下延迟。

13.4 BASE / Seata

1
2
3
transaction:
defaultType: BASE
providerType: Seata

BASE 更偏最终一致性。需要额外部署 Seata Server,并设计全局事务、分支事务、undo 日志、异常恢复。

13.5 工程建议

真正稳定的分库分表系统,通常不是靠到处上分布式事务,而是靠:

  • 分片键让同一业务聚合落同库。
  • 明确聚合边界。
  • 使用本地事务完成单库写入。
  • 跨库动作通过消息、任务、补偿、对账完成。
  • 关键状态机可重入、可恢复、可幂等。

13.6 为什么 LOCAL 跨库不是强一致

一次 Spring 事务命中两个物理库:

sequenceDiagram
    participant App as 应用事务
    participant DB0 as ds_0
    participant DB1 as ds_1

    App->>DB0: UPDATE A
    App->>DB1: UPDATE B
    App->>DB0: COMMIT
    DB0-->>App: 成功
    App->>DB1: COMMIT
    DB1--xApp: 网络故障

此时 DB0 已提交,DB1 未提交。普通本地事务无法让已经提交的 DB0 自动回滚。

13.7 事务方案决策表

业务要求 推荐方案 说明
单聚合、同分片写入 LOCAL 最简单、性能最好
跨库但允许秒级一致 Outbox + MQ + 补偿 常见推荐
跨库且要求强一致 XA,严格验证 Boot 3 场景有明确限制
长事务、外部服务参与 Saga/TCC/状态机 业务复杂度高
批量离线同步 可重入任务 + 对账 不要包超大事务

13.8 Outbox 最终一致性示例

同一个本地事务写业务表和消息表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE local_event (
event_id BIGINT NOT NULL,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(16) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_time DATETIME(6) NULL,
create_time DATETIME(6) NOT NULL,
update_time DATETIME(6) NOT NULL,
PRIMARY KEY (event_id),
KEY idx_status_retry (status, next_retry_time)
);
flowchart LR
    A[本地事务] --> B[(业务表)]
    A --> C[(local_event)]
    C --> D[发布任务]
    D --> E[MQ]
    E --> F[下游消费者]
    F --> G[(下游分片库)]
    D -.失败重试.-> C
    G --> H[对账/补偿]

关键要求:

  • 事件 ID 全局唯一;
  • 发布端至少一次投递;
  • 消费端幂等;
  • 失败指数退避;
  • 死信和人工处理;
  • 业务状态与事件状态可对账。

13.9 @Transactional 的常见误区

  • 同类方法内部调用导致事务代理失效;
  • 异步线程不继承原事务;
  • 捕获异常后不重新抛出,事务不会回滚;
  • rollbackFor 未覆盖检查异常;
  • 一个事务里先查从库再写主库,读到旧值;
  • 事务跨多个物理库却误以为 LOCAL 强一致;
  • 远程 RPC 不属于本地数据库事务。

13.10 事务故障演练

至少模拟:

1
2
3
4
5
6
7
8
9
第一个库写成功、第二个库写失败
提交前应用进程被 kill -9
提交过程中网络断开
数据库主库切换
连接池获取超时
事务超时
死锁回滚
消息发布失败
补偿重复执行

没有故障演练的分布式事务设计,通常只验证了“世界和平时能工作”。

14. ShardingSphere-Proxy

14.1 为什么需要 Proxy

JDBC 适合 Java 应用内嵌,Proxy 适合平台化。

Proxy 价值:

  • 多语言统一接入。
  • 规则集中管理。
  • 支持 DistSQL。
  • 运维视角更清晰。
  • 应用无需引入 ShardingSphere 依赖。

14.2 启动方式

常见方式:

  • 二进制包。
  • Docker。
  • Helm。

二进制启动:

1
sh /opt/shardingsphere-proxy/bin/start.sh

默认端口通常是:

1
3307

连接:

1
mysql -h 127.0.0.1 -P 3307 -uroot -proot

如果后端是 MySQL,二进制包方式通常需要把 MySQL 驱动放到:

1
%SHARDINGSPHERE_PROXY_HOME%/ext-lib

14.3 Proxy 配置文件

核心文件:

1
2
conf/global.yaml
conf/database-xxx.yaml

global.yaml 管全局配置,例如权限、事务、模式、属性。

database-xxx.yaml 管逻辑库数据源和规则。

14.4 Proxy 与 JDBC 的选择

对比项 ShardingSphere-JDBC ShardingSphere-Proxy
部署方式 应用内 Jar 独立服务
性能 更少网络跳转 多一层代理
多语言 Java 友好 多语言友好
规则管理 跟随应用 集中治理
运维复杂度 中等
DistSQL 不作为主入口 核心能力
适合场景 单 Java 应用 / 微服务 平台化数据库网关

14.5 Proxy 的完整部署拓扑

flowchart TD
    Client1[Java 服务] --> LB[四层负载均衡]
    Client2[Go 服务] --> LB
    Client3[BI / MySQL Client] --> LB

    LB --> P1[Proxy-1]
    LB --> P2[Proxy-2]
    LB --> P3[Proxy-3]

    P1 <--> Registry[(ZooKeeper / Etcd)]
    P2 <--> Registry
    P3 <--> Registry

    P1 --> DB0[(ds_0)]
    P1 --> DB1[(ds_1)]
    P2 --> DB0
    P2 --> DB1
    P3 --> DB0
    P3 --> DB1

生产至少考虑:

  • Proxy 无状态计算节点横向扩容;
  • 注册中心高可用;
  • 负载均衡健康检查;
  • 长连接摘除和优雅下线;
  • Proxy 到每个真实库的连接池预算;
  • 配置变更审计;
  • 管理端口与业务端口隔离;
  • TLS、账号权限和网络 ACL。

14.6 Docker 启动 Proxy 的示例思路

镜像标签和目录结构应以 5.5.3 发布包为准。通常流程是:

1
2
3
4
5
# 1. 下载并校验 5.5.3 Proxy 发布包
# 2. 放置数据库驱动到 ext-lib
# 3. 准备 global.yaml 和 database-*.yaml
# 4. 启动 Proxy
./bin/start.sh

生产不要省略发布包校验:

1
2
sha512sum -c apache-shardingsphere-5.5.3-shardingsphere-proxy-bin.tar.gz.sha512
# 或使用 GPG 校验 .asc 签名

14.7 Proxy 的连接模型

客户端连接数和 Proxy 到后端库的连接数不是一一固定相等,但两侧都需要预算:

1
2
客户端连接 -> Proxy 会话与协议状态
Proxy SQL 执行 -> 后端真实数据源连接池

重点指标:

1
2
3
4
5
6
7
proxy_frontend_connections
backend_pool_active
backend_pool_pending
sql_parse_latency
route_units_per_sql
execute_latency
merge_latency

14.8 Proxy 高可用不是只起两个实例

还需要验证:

  • 某实例停止后客户端是否自动重连;
  • 事务中的连接断开如何呈现;
  • PreparedStatement 是否能在重连后正确重建;
  • DistSQL 变更是否被所有实例感知;
  • 注册中心短暂不可用时,现有流量是否继续;
  • 节点恢复后元数据是否一致;
  • LB 是否能优雅摘除正在执行长 SQL 的节点。

14.9 JDBC 与 Proxy 混用的治理原则

如果同一逻辑库既被 JDBC 又被 Proxy 访问:

  • 规则必须有唯一事实源;
  • 禁止一边静态 YAML、一边 DistSQL 随意修改;
  • 发布前生成规则指纹并比对;
  • 使用相同的逻辑表、算法和数据节点命名;
  • 建立统一迁移窗口;
  • 任何扩容先验证两种入口路由一致。

15. DistSQL

DistSQL 是 Proxy 的灵魂之一。它让你用 SQL 管理分布式数据库规则。

15.1 注册存储单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
REGISTER
STORAGE UNIT ds_0 (
HOST="127.0.0.1",
PORT=3306,
DB="demo_ds_0",
USER="root",
PASSWORD="root",
PROPERTIES("maximumPoolSize"=10)
);

REGISTER
STORAGE UNIT ds_1 (
URL="jdbc:mysql://127.0.0.1:3306/demo_ds_1?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true",
USER="root",
PASSWORD="root",
PROPERTIES("maximumPoolSize"=10,"idleTimeout"=30000)
);

15.2 创建分片规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CREATE
SHARDING TABLE RULE t_order (
DATANODES("ds_${0..1}.t_order_${0..1}"),
DATABASE_STRATEGY(
TYPE="standard",
SHARDING_COLUMN=user_id,
SHARDING_ALGORITHM(
TYPE(
NAME="inline",
PROPERTIES("algorithm-expression"="ds_${user_id % 2}")
)
)
),
TABLE_STRATEGY(
TYPE="standard",
SHARDING_COLUMN=order_id,
SHARDING_ALGORITHM(
TYPE(
NAME="inline",
PROPERTIES("algorithm-expression"="t_order_${order_id % 2}")
)
)
),
KEY_GENERATE_STRATEGY(COLUMN=order_id, TYPE(NAME="snowflake")),
AUDIT_STRATEGY(TYPE(NAME="DML_SHARDING_CONDITIONS"), ALLOW_HINT_DISABLE=true)
);

15.3 常用查看命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SHOW
STORAGE UNITS;
SHOW
SHARDING TABLE RULE;
SHOW
SHARDING ALGORITHMS;
SHOW
SHARDING TABLE NODES;
SHOW
SHARDING KEY GENERATORS;
SHOW
READWRITE_SPLITTING RULE;
SHOW
ENCRYPT RULES;
SHOW
MASK RULES;
SHOW
SHADOW RULE;
SHOW
COMPUTE NODES;
SHOW
DIST VARIABLE;

15.4 预览 SQL 路由

1
2
3
4
5
6
PREVIEW
SQL
select *
from t_order
where user_id = 10
and order_id = 10086;

这个命令非常适合排查路由问题。路由问题不要靠猜,猜数据库路由跟猜对象心思差不多,都容易误伤自己。

15.5 DistSQL 变更必须像数据库 DDL 一样管理

推荐流程:

flowchart TD
    A[提交 DistSQL 变更脚本] --> B[代码评审]
    B --> C[测试环境 PREVIEW/回归]
    C --> D[备份当前规则]
    D --> E[生产变更审批]
    E --> F[执行 DistSQL]
    F --> G[SHOW RULE 验证]
    G --> H[路由冒烟测试]
    H --> I{是否正常}
    I -->|是| J[记录审计与规则指纹]
    I -->|否| K[执行回滚脚本]

15.6 规则导出与漂移检测

每天或每次变更后导出:

1
2
3
4
5
6
7
SHOW STORAGE UNITS;
SHOW SHARDING TABLE RULE;
SHOW SHARDING ALGORITHMS;
SHOW READWRITE_SPLITTING RULE;
SHOW ENCRYPT RULES;
SHOW MASK RULES;
SHOW SHADOW RULE;

对输出做规范化后计算 SHA-256:

1
logic_db + rule_type + normalized_rule -> rule_fingerprint

多环境、多 Proxy 节点出现不同指纹时立即告警。

15.7 DistSQL 权限隔离

建议划分账号:

1
2
3
4
app_readwrite:业务 DML/DQL
migration_operator:迁移任务
rule_admin:DistSQL 规则变更
observer:SHOW/PREVIEW 只读

业务应用账号不应拥有规则修改权限。否则一次 SQL 注入或误操作可能改变整个逻辑库路由。

15.8 PREVIEW SQL 的使用场景

上线前至少对以下 SQL 建路由快照:

  • 主键点查;
  • 分片键 IN 查询;
  • 范围查询;
  • 绑定表 JOIN;
  • 非绑定 JOIN;
  • 分页;
  • 聚合;
  • 广播表写入;
  • 单表查询;
  • Hint 查询。

每次规则变更后对比快照,可以快速发现路由数量变化。

16. 数据迁移

16.1 什么时候需要迁移

典型场景:

  • 单库单表迁移到分库分表。
  • 分片数量从 2 扩到 4。
  • 旧数据源迁移到新数据源。
  • 调整分片键。
  • 迁移到 Proxy 统一治理。

16.2 迁移思路

flowchart TD
    A[准备新库新表] --> B[配置目标分片规则]
    B --> C[全量迁移]
    C --> D[增量同步]
    D --> E[数据一致性校验]
    E --> F[灰度切流]
    F --> G[观察与回滚窗口]
    G --> H[正式切换]

16.3 迁移前检查

  • 源表必须有稳定主键。
  • 分片键不能为空。
  • 目标表结构和索引提前创建。
  • 目标库容量、连接数、磁盘 IO 足够。
  • 双写或增量同步链路可观测。
  • 有回滚方案。
  • 有数据校验方案。

16.4 数据校验

至少校验:

  • 行数。
  • 主键范围。
  • 核心金额字段合计。
  • 状态分布。
  • 更新时间最大值。
  • 抽样比对。

订单、财务、结算系统只校验行数是不够的,金额合计一定要校验。

16.5 扩容不是修改取模数

原规则:

1
user_id % 2 -> ds_0 / ds_1

直接改成:

1
user_id % 4 -> ds_0 / ds_1 / ds_2 / ds_3

会让大量旧数据的计算落点改变。如果数据未同步迁移,新规则会去新位置查旧数据,结果就是“数据消失”。

16.6 扩容的四个阶段

flowchart LR
    A[双节点旧拓扑] --> B[准备四节点新拓扑]
    B --> C[全量复制旧数据]
    C --> D[增量同步]
    D --> E[一致性校验]
    E --> F[灰度切读]
    F --> G[灰度切写]
    G --> H[停止旧链路]

16.7 虚拟槽位降低扩容重映射

可以先把分片键映射到较稳定的逻辑槽位,再把槽位映射到物理库:

1
2
slot = hash(user_id) % 1024
physical_db = slot_mapping[slot]
flowchart LR
    U[user_id] --> S[1024 个逻辑槽位]
    S --> D0[ds_0: slot 0-511]
    S --> D1[ds_1: slot 512-1023]
    D0 -.扩容迁移部分槽位.-> D2[ds_2]
    D1 -.扩容迁移部分槽位.-> D3[ds_3]

扩容时只迁移部分槽位,不改变所有数据的逻辑槽位。ShardingSphere 可通过自定义 CLASS_BASED 算法实现槽位映射,但映射表本身要高可用、版本化和可审计。

16.8 迁移 Runbook

迁移前:

1
2
3
4
5
冻结表结构大变更
确认源/目标版本和字符集
确认主键、分片键与增量日志条件
评估磁盘、网络、连接数和复制延迟
准备只读窗口与回滚策略

全量阶段:

1
2
3
4
按主键范围切片
限制并发和带宽
记录每批起止主键、行数、校验值
失败批次可重跑

增量阶段:

1
2
3
4
记录位点
监控 backlog
处理 DDL 兼容性
保证事件顺序或按业务键串行

校验阶段:

1
2
3
4
5
6
7
总行数
分片行数
主键缺失/重复
金额 SUM
状态 COUNT GROUP BY
时间最大值
随机抽样字段摘要

切流阶段:

1
2
1% -> 5% -> 20% -> 50% -> 100%
每阶段观察错误率、延迟、数据差异

16.9 财务数据校验示例

1
2
3
4
5
6
7
8
9
10
SELECT
COUNT(*) AS row_count,
COUNT(DISTINCT order_id) AS distinct_order_count,
SUM(amount) AS total_amount,
MIN(order_id) AS min_id,
MAX(order_id) AS max_id,
MAX(update_time) AS max_update_time
FROM t_order
WHERE create_time >= :startTime
AND create_time < :endTime;

按状态校验:

1
2
3
4
5
6
SELECT status, COUNT(*) AS cnt, SUM(amount) AS amount_sum
FROM t_order
WHERE create_time >= :startTime
AND create_time < :endTime
GROUP BY status
ORDER BY status;

金额校验要使用相同精度和舍入规则,不能一边 DECIMAL、一边转 double。

16.10 回滚不是“把开关切回去”

切写后新库已经产生新数据,回滚到旧库需要反向同步或短暂双写。回滚方案必须提前回答:

  • 新拓扑产生的数据如何回旧拓扑;
  • 新旧主键是否冲突;
  • 消息是否重复消费;
  • 规则回退后读写落点是否一致;
  • 回滚窗口多久;
  • 超过窗口后是否改为向前修复。

16.11 迁移限流

迁移任务与在线流量争抢:

1
2
3
4
5
6
数据库连接
磁盘 IOPS
Buffer Pool
网络带宽
CPU
redo/binlog 写入

限流应根据在线 P99 延迟和数据库资源动态调整,而不是固定线程数跑到底。

17. 可观察性

17.1 SQL 日志

开发环境开启:

1
2
3
props:
sql-show: true
sql-simple: false

生产环境谨慎开启。SQL 日志可能包含敏感数据,也可能造成额外 IO 压力。

17.2 Metrics

ShardingSphere-Agent 支持指标采集,常见指标包括:

  • parsed_sql_total
  • routed_sql_total
  • routed_result_total
  • jdbc_state
  • jdbc_statement_execute_total
  • jdbc_statement_execute_errors_total

这些指标适合接入 Prometheus / Grafana。

17.3 建议监控项

应用层:

  • 接口耗时。
  • 慢 SQL 数量。
  • 连接池活跃连接数。
  • 连接池等待数。
  • 错误 SQL 数量。

ShardingSphere 层:

  • 解析 SQL 总数。
  • 路由 SQL 总数。
  • 全路由比例。
  • 路由结果数量。
  • 执行错误数量。

数据库层:

  • QPS / TPS。
  • 慢查询。
  • CPU / IO。
  • 连接数。
  • 主从延迟。
  • 锁等待。

17.4 最值得盯的指标

我最建议重点盯:

1
2
3
4
5
全路由比例
慢 SQL
主从延迟
连接池等待
跨库事务异常

分库分表系统里,全路由是性能事故的温柔前奏。它一开始只是慢一点,后来就会很有存在感。

17.5 推荐日志字段

不要只打印一条“Actual SQL”。建议结构化记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trace_id
request_id
logic_database
logic_sql_fingerprint
route_database_count
route_table_count
execution_unit_count
is_full_route
rewrite_cost_ms
execute_cost_ms
merge_cost_ms
total_cost_ms
transaction_type
readwrite_target
error_code

示例 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
{
"trace_id": "9a51...",
"logic_database": "logic_db",
"sql_fingerprint": "select_t_order_by_user_order",
"route_database_count": 1,
"route_table_count": 1,
"execution_unit_count": 1,
"is_full_route": false,
"execute_cost_ms": 8,
"merge_cost_ms": 1,
"total_cost_ms": 11
}

注意不要记录完整敏感参数。

17.6 监控面板分层

flowchart TD
    A[业务 SLO] --> B[应用层]
    B --> C[ShardingSphere 层]
    C --> D[连接池层]
    D --> E[数据库层]
    E --> F[复制与存储层]

**业务层:**成功率、P95/P99、超时率、核心交易量。

**应用层:**线程、虚拟线程 pin、GC、堆、CPU、请求队列。

**ShardingSphere 层:**解析、路由、改写、执行、归并耗时,路由单元数,全路由比例。

**连接池层:**active、idle、pending、timeout、创建连接耗时。

**数据库层:**QPS/TPS、锁等待、慢 SQL、Buffer Pool、IO、连接数。

**复制层:**主从延迟、复制错误、位点差距。

17.7 ShardingSphere-Agent 示例

Agent 支持 Metrics、Tracing 和 Logging 插件。示意配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins:
metrics:
Prometheus:
host: "0.0.0.0"
port: 9090
props:
jvm-information-collector-enabled: "true"
tracing:
OpenTelemetry:
props:
otel.service.name: "shardingsphere-proxy"
otel.traces.exporter: "otlp"
otel.exporter.otlp.endpoint: "http://otel-collector:4317"
otel.traces.sampler: "parentbased_traceidratio"
otel.traces.sampler.arg: "0.1"

具体插件名称和属性以 5.5.3 发布包中的 agent.yaml 为准。

17.8 告警建议

指标 告警示例 说明
全路由比例 5 分钟 > 1% 在线接口通常应接近 0
单 SQL 路由单元 P99 > 16 可能出现路由放大
连接池 pending 持续 > 0 连接不足或 SQL 过慢
获取连接超时 任意增长 高优先级
主从延迟 超过业务一致性窗口 读旧数据风险
merge P99 持续升高 深分页/聚合问题
duplicate key 异常增长 ID/幂等/迁移冲突
元数据加载失败 任意出现 规则或真实表不一致

17.9 SQL 指纹与 TopN

将常量参数归一化:

1
SELECT * FROM t_order WHERE user_id = ? AND order_id = ?

按 SQL 指纹统计:

1
2
3
4
5
6
7
调用次数
平均/P95/P99 延迟
平均路由数
最大路由数
错误率
读取行数
返回行数

同一 SQL 指纹的路由数量突然变化,往往意味着代码条件丢失、规则变更或参数类型异常。

17.10 可观测性本身的成本

  • 全量 SQL 日志会增加 IO;
  • 全量 Trace 会增加网络和存储;
  • 高基数标签会拖垮 Prometheus;
  • 参数日志可能泄露数据;
  • 慢 SQL采样过低会漏问题。

推荐:Metrics 全量、Trace 采样、错误 Trace 全量、SQL 参数默认不记录。

18. SQL 支持与限制

分库分表不是完整分布式数据库,SQL 能力一定有边界。

18.1 友好的 SQL

1
2
3
4
5
6
7
8
9
10
11
12
select *
from t_order
where user_id = ?
and order_id = ?;

select *
from t_order
where user_id = ?
order by create_time desc limit 20;

insert into t_order(user_id, order_no, amount, status, create_time)
values (?, ?, ?, ?, ?);

特点:

  • 带分片键。
  • 路由明确。
  • 查询范围小。
  • 排序分页在单分片内完成。

18.2 危险 SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select *
from t_order
where status = 'CREATED';

select count(*)
from t_order;

select *
from t_order
order by create_time desc limit 100000, 20;

select *
from t_order
where amount between 100 and 200;

问题:

  • 缺少分片键。
  • 可能全库全表扫描。
  • 跨分片排序分页成本高。
  • count 需要多分片归并。

18.3 关联查询

好的关联:

1
2
3
4
5
select o.order_id, i.sku_code
from t_order o
join t_order_item i on o.order_id = i.order_id
where o.user_id = ?
and o.order_id = ?;

前提:

  • 两张表是绑定表。
  • 分片策略一致。
  • 查询条件带分片键。

危险关联:

1
2
3
4
select *
from t_order o
join t_user u on o.user_id = u.user_id
where u.level = 'VIP';

如果 t_user 没有相同分片策略,可能出现跨库关联或全路由。

18.4 SQL 兼容性要用业务 SQL 集验证

不要只看官方“支持 SELECT/INSERT/UPDATE/DELETE”。真实 SQL 复杂度来自:

1
2
3
4
5
6
7
8
9
10
数据库方言
函数
子查询
CTE
窗口函数
JSON
锁语句
批量语句
驱动 PreparedStatement 行为
ORM 自动生成 SQL

建立 SQL 兼容性测试集,将生产 Top SQL、核心 Mapper 和报表 SQL全部纳入升级回归。

18.5 聚合查询的风险矩阵

SQL 是否可归并 性能风险
COUNT(*) 通常可以 全路由 + 多分片求和
SUM(amount) 通常可以 扫描量大
AVG(amount) 改写为 SUM/COUNT 多字段归并
MIN/MAX 通常可以 仍需查询各分片
GROUP BY key 可以但可能内存归并 高基数风险
COUNT(DISTINCT x) 需重点验证 去重成本高
多维 GROUP BY + ORDER BY 需重点压测 内存与排序风险

18.6 管理端搜索如何处理

管理端常见条件:

1
2
3
4
5
6
7
手机号
订单号
状态
时间范围
商品名
支付流水号
模糊关键字

它们不一定包含主分片键。可选架构:

flowchart LR
    A[业务写入] --> B[(分片 OLTP)]
    A --> C[CDC / MQ]
    C --> D[(Elasticsearch)]
    C --> E[(ClickHouse / Doris)]
    F[管理端点查] --> B
    G[复杂搜索] --> D
    H[报表聚合] --> E

不要强迫 ShardingSphere 承担所有查询类型。

18.7 分布式分页的正确接口设计

不推荐:

1
GET /orders?page=5000&size=20

推荐:

1
GET /orders?userId=10&cursor=eyJ0aW1lIjoi...&size=20

Cursor 中包含:

1
2
3
4
{
"lastCreateTime": "2026-07-26T12:00:00.123456",
"lastOrderId": 19876543210001
}

Cursor 应签名或加密,防止客户端随意篡改。

18.8 UPDATE/DELETE 必须带分片键

危险:

1
UPDATE t_order SET status = 'CLOSED' WHERE order_no = ?;

如果 order_no 不是分片键,会向多个分片发送 UPDATE。即使最终只命中一行,也产生写放大和锁竞争。

推荐:

1
2
3
4
5
UPDATE t_order
SET status = 'CLOSED'
WHERE user_id = ?
AND order_id = ?
AND status = 'CREATED';

同时利用状态条件保证幂等状态转换。

18.9 DDL 管理

分片表 DDL 必须同步所有真实表:

flowchart TD
    A[Flyway 逻辑迁移定义] --> B[生成真实库表脚本]
    B --> C[预检查所有分片]
    C --> D[分批执行]
    D --> E[Schema 摘要校验]
    E --> F[应用发布]

高风险变更:

  • 大表加非空默认值列;
  • 重建大索引;
  • 修改主键;
  • 修改分片键;
  • 改字符集;
  • 同时变更所有分片导致 IO 峰值。

推荐使用 Online DDL 工具或云数据库在线变更能力,并控制并发分片数。

19. 分片建模最佳实践

19.1 按业务聚合设计

分片不要只看表大小,要看业务聚合。

例如订单系统:

1
用户 -> 订单 -> 订单明细 -> 支付记录 -> 售后单

如果用户维度查询最多,可以考虑 user_id

如果商家维度结算最多,可以考虑 shop_id

如果租户隔离最重要,可以考虑 tenant_id

没有绝对正确的分片键,只有最符合业务主路径的分片键。

19.2 避免跨库事务

设计时尽量让一次事务内修改的数据落在同一个库:

1
2
3
同一个 user_id 的订单和订单明细落同库
同一个 tenant_id 的配置和业务数据落同库
同一个 shop_id 的结算数据落同库

如果做不到,就要补充:

  • 幂等键。
  • 事务消息。
  • 补偿任务。
  • 对账任务。
  • 异常状态机。

19.3 冷热数据分离

大表经常不是平均热,而是近期数据热、历史数据冷。

可以考虑:

  • 当前表 + 历史表。
  • 按月分表。
  • 热数据 MySQL,冷数据归档到 OLAP。
  • 搜索查询走 Elasticsearch / ClickHouse / Doris。

ShardingSphere 解决 OLTP 分片问题,不要让它独自扛所有分析型查询。

19.4 分片数量不要乱定

分片数量要考虑:

  • 当前数据量。
  • 三年增长量。
  • 单表可接受大小。
  • 数据库实例容量。
  • 扩容复杂度。
  • 运维成本。

不要一上来就 1024 张表。表多不是架构先进,有时候只是把复杂度提前透支。

19.5 什么时候需要分库分表

不要只用“单表超过 500 万/2000 万”作为标准。真正判断维度:

维度 问题
容量 三年后数据和索引能否放下
写入 单实例 TPS 是否达到瓶颈
查询 核心 SQL 是否因数据规模退化
连接 单实例能否承载应用连接总量
运维 DDL、备份、恢复窗口是否不可接受
隔离 大租户是否影响其他租户
成本 分片复杂度是否低于扩容硬件成本

19.6 分片数量估算

可以使用三个约束分别计算,取最大值:

1
2
3
按数据量:ceil(未来总行数 × 安全系数 / 单表目标行数)
按写吞吐:ceil(峰值写 TPS / 单库安全写 TPS)
按容量:ceil(未来总数据容量 / 单库安全容量)

例如:

1
2
3
4
三年订单:24 亿行
安全系数:1.5
单表目标:5000 万行
需要表数:ceil(24亿 × 1.5 / 5000万) = 72

可以选择 96 或 128 个逻辑槽位,但不意味着立即创建同等数量物理库。表数、槽位数和库数可以分层设计。

19.7 容量规划表

上线前至少记录:

1
2
3
4
5
6
7
8
9
10
日新增行数
平均/峰值行大小
索引放大系数
三年数据量
冷热比例
峰值读写 QPS/TPS
单 SQL 平均读取行数
备份窗口
恢复时间目标 RTO
恢复点目标 RPO

19.8 租户分片策略

SaaS 常见三类:

  1. tenant_id % N:简单,但大租户可能形成热点;
  2. 大租户独占库,小租户共享库:隔离好,但路由表复杂;
  3. 逻辑槽位 + 租户映射:扩容灵活,治理成本高。

大租户不能只按租户 ID 取模。一个超级租户可能独占整个分片的绝大多数流量。

19.9 热点与数据倾斜

平均分布不代表请求均匀。需要分别评估:

1
2
3
4
5
6
行数分布
数据字节分布
读 QPS 分布
写 TPS 分布
热点 Key 分布
锁等待分布

一个分片只有 10% 数据,也可能承载 80% 热点请求。

19.10 分片键变更为什么困难

分片键决定物理位置。修改它相当于全量重新分布数据,还会影响:

  • 查询接口参数;
  • 唯一约束;
  • 绑定表;
  • 事务边界;
  • 下游事件;
  • 数据归档;
  • 缓存 Key;
  • 路由索引。

因此分片前应收集真实 SQL 和访问模式,而不是只开架构会拍脑袋。

20. 生产配置建议

20.1 props 建议

开发环境:

1
2
3
4
5
6
props:
sql-show: true
sql-simple: false
kernel-executor-size: 16
max-connections-size-per-query: 1
check-table-metadata-enabled: true

生产环境:

1
2
3
4
5
6
7
props:
sql-show: false
sql-simple: true
kernel-executor-size: 32
max-connections-size-per-query: 1
check-table-metadata-enabled: true
load-table-metadata-batch-size: 1000

kernel-executor-size 不是越大越好,要结合 CPU、连接池、真实数据库能力调。

20.2 连接池建议

每个真实数据源都有自己的连接池。假设:

1
2
3
应用实例数 = 10
真实数据源数 = 4
每个数据源 maximumPoolSize = 30

最大连接数理论上是:

1
10 * 4 * 30 = 1200

这还没算其他应用。很多数据库连接数爆掉,不是业务突然变猛,而是连接池配置像开自助餐,大家都拿满了。

20.3 推荐连接池计算方式

先从小值开始:

1
单应用单数据源 maximumPoolSize = 10 ~ 20

观察:

  • Hikari active connection。
  • pending threads。
  • DB CPU。
  • SQL 平均耗时。
  • 慢查询。

再逐步调整。

20.4 HikariCP 与 ShardingSphere 双层理解

Spring Boot 创建的是 ShardingSphere 逻辑 DataSource,ShardingSphere 内部又为每个真实数据源创建连接池。配置中的:

1
2
3
dataSources:
ds_0:
maximumPoolSize: 20

ds_0 这个真实数据源池的上限,不是整个应用的总上限。

20.5 连接池预算公式

1
总潜在连接 = 应用副本数 × 真实数据源数 × 单池上限

数据库允许连接还需扣除:

1
2
3
4
5
管理连接
监控连接
迁移连接
其他应用
故障切换保留

建议数据库目标使用率不要长期超过连接上限的 70%~80%。

20.6 连接池参数建议

参数 建议
maximumPoolSize 从 10~20 起步,结合压测调
minimumIdle 不要等于过大的 maximum,防止启动连接风暴
connectionTimeout 通常 1~5 秒比 30 秒更利于快速失败,需结合业务
idleTimeout 结合数据库 wait_timeout
maxLifetime 略小于数据库/网络设备连接生命周期
validationTimeout 保持较短
keepaliveTime 仅在网络空闲连接易被切断时启用

20.7 JDK 21 虚拟线程不会增加数据库连接

虚拟线程可以降低阻塞线程成本,但数据库连接仍是稀缺资源:

flowchart LR
    A[10000 个虚拟线程请求] --> B[Hikari 连接池 20]
    B --> C[(MySQL)]
    A -.其余请求等待连接.-> B

因此,不能因为启用虚拟线程就把连接池调到几百。正确做法是控制并发、优化 SQL、缩短持有连接时间。

20.8 kernel-executor-size 调优

该参数影响 ShardingSphere 内核执行并发。过小会限制多路由执行速度,过大会:

  • 增加线程竞争;
  • 放大后端连接请求;
  • 增加数据库瞬时 QPS;
  • 让慢 SQL 并行压垮所有分片。

调优步骤:

1
2
3
4
5
1. 固定连接池和数据库资源
2. 使用真实路由分布压测
3. 从 CPU 核数附近的值开始
4. 观察吞吐、P99、连接 pending、DB CPU
5. 每次只调整一个参数

20.9 生产 YAML 安全化

不要在 Git 中写:

1
2
username: root
password: root

可选方式:

  • 容器环境变量渲染配置;
  • Kubernetes Secret + CSI;
  • Vault Agent 模板;
  • 配置中心密文;
  • 云数据库 IAM 临时凭证(若支持)。

配置文件权限至少限制为应用用户可读。

20.10 启动连接风暴

Kubernetes 一次滚动启动 50 个实例,每实例 8 个数据源、minimumIdle=10

1
50 × 8 × 10 = 4000 个初始连接

解决:

  • 控制 maxSurge
  • 降低 minimumIdle
  • 启动随机抖动;
  • 数据库代理/连接复用;
  • 分批发布;
  • 监控数据库登录和握手耗时。

20.11 推荐的生产属性基线

下面只是起点,不是通用答案:

1
2
3
4
5
6
7
props:
sql-show: false
sql-simple: true
kernel-executor-size: 16
max-connections-size-per-query: 1
check-table-metadata-enabled: true
load-table-metadata-batch-size: 1000

上线前应通过压测决定 kernel-executor-sizemax-connections-size-per-query

21. 常见问题与排查

21.1 启动报错找不到 ShardingSphereDriver

检查依赖:

1
2
3
4
5
6

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>5.5.3</version>
</dependency>

检查配置:

1
2
3
4
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml

21.2 YAML 文件没加载

检查:

  • 文件是否在 src/main/resources
  • 文件名是否和 classpath:shardingsphere.yaml 一致。
  • YAML 缩进是否正确。
  • rules 下的 !SHARDING!READWRITE_SPLITTING 是否大写。

21.3 表不存在

检查:

  • 真实库是否存在。
  • 真实表是否都创建。
  • actualDataNodes 是否写错。
  • 分片算法是否路由到了不存在的表。

21.4 查询很慢

优先看:

  • 是否缺少分片键。
  • 是否全路由。
  • 是否跨分片排序。
  • 是否跨分片分页。
  • 是否广播表太大。
  • 是否连接池不够。
  • 是否真实库索引缺失。

开启 sql-show 看真实 SQL 是第一步。

21.5 插入后 ID 为空

检查:

1
2
3
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake

以及:

1
2
3
keyGenerators:
snowflake:
type: SNOWFLAKE

同时确认 INSERT SQL 中没有主动传入空 ID 导致行为异常。

21.6 主从延迟导致读不到刚写数据

解决:

  • 事务内使用 transactionalReadQueryStrategy: PRIMARY
  • 关键读请求使用 HintManager.setWriteRouteOnly()
  • 业务允许延迟时再读从库。
  • 监控主从延迟。

21.7 全路由太多

解决:

  • 改接口入参,强制带分片键。
  • 建路由索引。
  • 用 Hint 指定路由。
  • 对管理端查询单独建搜索索引或报表库。
  • 调整分片键。

21.8 系统化排障流程

flowchart TD
    A[发现慢/错/数据缺失] --> B[确认逻辑 SQL 与参数]
    B --> C[检查规则版本和指纹]
    C --> D[查看路由目标数量]
    D --> E[查看真实 SQL]
    E --> F[直连真实库 EXPLAIN]
    F --> G[检查连接池与数据库资源]
    G --> H[检查归并、主从延迟、事务]
    H --> I[复现并补充自动化测试]

21.9 案例:同一查询偶尔查不到数据

排查顺序:

  1. 是否刚写完读从库;
  2. 是否查询条件缺分库键;
  3. order_iduser_id 是否属于同一条数据;
  4. 是否迁移期间新旧规则不一致;
  5. 是否某个分片表结构或数据缺失;
  6. Hint 是否在线程中残留;
  7. 数据是否写到了错误 worker/算法计算的节点。

21.10 案例:CPU 不高但接口大量超时

可能是:

  • Hikari pending;
  • 数据库连接上限;
  • 网络连接建立慢;
  • 锁等待;
  • 全路由串行执行;
  • 主从切换;
  • 结果集消费慢;
  • 应用下游阻塞但事务一直持有连接。

先看连接池等待和事务持有时间,不要只盯 CPU。

21.11 案例:增加索引后仍然很慢

单个真实 SQL 使用索引,不代表整体快:

1
2
3
4
5
6
单分片 5ms × 64 分片
+ 连接获取
+ 网络
+ 线程调度
+ 归并排序
= 可能数百毫秒或更高

需要减少路由数,而不是只优化每个分片。

21.12 案例:线上走全路由,测试环境精确路由

检查:

  • 两环境 YAML 是否一致;
  • 分片键参数类型是否一致;
  • 线上 SQL 是否被动态条件删除;
  • MyBatis 是否传 null;
  • 数据库字段类型和 Java 类型;
  • ShardingSphere 版本;
  • 自定义算法配置;
  • 规则中心是否存在旧配置;
  • Hint 是否仅在测试代码中设置。

21.13 案例:应用启动很慢

可能原因:

  • 数据源数量多;
  • 元数据检查;
  • 单表 *.* 扫描;
  • 网络 DNS 慢;
  • 数据库握手慢;
  • 连接池预热过大;
  • 真实表数量巨大。

可以分别记录:

1
2
3
4
Spring DataSource 创建耗时
每个真实池初始化耗时
元数据加载耗时
规则构建耗时

21.14 案例:内存突然上涨

重点看:

  • 跨分片 GROUP BY;
  • 深分页;
  • 大结果集未设置合理 fetch size;
  • 连接受限导致流式归并退化;
  • SQL 日志大量字符串;
  • 一次批量写过大;
  • 客户端读取结果过慢。

21.15 路由问题最小复现模板

提交问题或内部排查时,至少准备:

1
2
3
4
5
6
7
8
9
10
ShardingSphere 精确版本
JDK / Spring Boot / JDBC Driver 版本
最小 YAML
建表 SQL
逻辑 SQL 与参数类型
预期路由
实际路由
完整异常堆栈
是否事务内
是否使用 Hint

没有参数类型的 SQL 日志经常不够,因为字符串 "10" 与 Long 10L 可能走不同类型处理路径。

22. 和 MyBatis / JPA 的整合建议

22.1 MyBatis

MyBatis 不需要特别感知 ShardingSphere,只要使用 Spring Boot 的 DataSource 即可。

1
2
3
4
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml

Mapper 里写逻辑 SQL:

1
2
3
4
select *
from t_order
where user_id = #{userId}
and order_id = #{orderId}

不要在 Mapper 里写真实表名:

1
2
select *
from t_order_0

这会绕开逻辑表模型,后期维护会很难受。

22.2 JPA

JPA 也可以接 ShardingSphere DataSource,但要注意:

  • Entity 表名写逻辑表名。
  • 不要依赖数据库自增主键。
  • 关闭复杂级联。
  • 谨慎使用跨表关联。
  • 分库分表场景下复杂查询更适合 MyBatis / SQL。

示例:

1
2
3
4
5
6
7
@Entity
@Table(name = "t_order")
public class OrderEntity {
@Id
private Long orderId;
private Long userId;
}

JPA 的自动 DDL 不建议用于分片表创建,生产环境应该由 Flyway、Liquibase 或 DBA 脚本管理真实表。

22.3 MyBatis 动态 SQL 的分片键丢失

1
2
3
4
5
6
7
8
9
10
11
<select id="queryOrders" resultType="OrderDO">
SELECT * FROM t_order
<where>
<if test="userId != null">
user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>

userId=null 时,SQL 仍合法,却可能全路由。建议:

  • 在线接口对分片键使用 Bean Validation;
  • Mapper 方法区分“按分片查询”和“管理查询”;
  • 分片键不能为空时直接抛错;
  • 使用 DML_SHARDING_CONDITIONS 审计器;
  • 单元测试覆盖动态条件缺失。

22.4 MyBatis-Plus 注意事项

  • selectById(orderId) 只有主键,若分库键是 user_id,可能跨库;
  • Wrapper 条件需要显式加入分片键;
  • 分页插件与 ShardingSphere 分页会叠加,必须验证生成 SQL;
  • 批量方法可能被拆分到多个分片;
  • 逻辑删除字段不会自动成为分片键;
  • 自动填充主键与 ShardingSphere key generator 不要重复生成。

推荐方法签名:

1
OrderDO selectByUserIdAndOrderId(Long userId, Long orderId);

而不是在分库规则需要 userId 时只提供 selectById

22.5 JPA 的 N+1 在分片环境更危险

普通单库的 N+1 已经很慢;分片环境中每次子查询还可能全路由:

1
2
3
4
1 次订单列表查询
+ 100 次订单明细查询
× 每次 8 个路由
= 801 个真实执行单元

建议:

  • 禁用无意识懒加载;
  • 使用显式 DTO 查询;
  • 绑定表 JOIN;
  • 批量按分片键分组查询;
  • 在测试中统计 SQL 数和路由单元数。

22.6 Flyway 与分片表

Flyway 默认针对一个 DataSource 维护一张历史表。分片环境有三种做法:

  1. 独立管理数据源维护迁移历史,脚本显式更新所有真实库;
  2. 每个真实库独立 Flyway 实例;
  3. 在 CI/CD 中生成并执行物理 DDL,不由业务应用启动时迁移。

生产更推荐第三种或受控的第二种。不要让几十个应用副本同时执行同一批分片 DDL。

22.7 Repository 层接口应体现路由约束

不推荐:

1
OrderDO findByOrderId(Long orderId);

当分库键是 userId 时,推荐:

1
OrderDO findByUserIdAndOrderId(Long userId, Long orderId);

如果业务只有 orderId,需要设计:

1
orderId -> shard/tenant/user 路由索引

而不是默认广播查询。

23. 工程落地清单

23.1 接入前必须回答的问题

  • 数据量为什么需要分库分表?
  • 当前瓶颈是存储、查询、写入、连接数,还是索引设计?
  • 主要查询路径是什么?
  • 分片键是什么?
  • 是否存在大量不带分片键的查询?
  • 是否需要跨库事务?
  • 如何扩容?
  • 如何迁移旧数据?
  • 如何回滚?
  • 如何监控全路由和慢 SQL?

23.2 开发阶段清单

  • 本地准备两个真实库。
  • 建完整真实表。
  • 打开 sql-show
  • 写插入测试。
  • 写按分片键查询测试。
  • 写缺少分片键查询测试,观察是否全路由。
  • 写绑定表 join 测试。
  • 写广播表测试。
  • 写事务回滚测试。

23.3 上线前清单

  • 关闭生产 sql-show
  • 检查连接池总连接数。
  • 检查真实库索引。
  • 压测核心 SQL。
  • 压测全路由边界。
  • 验证主从延迟。
  • 验证备份恢复。
  • 准备回滚脚本。
  • 准备路由排查手册。
  • 接入监控指标。

23.4 自动化测试矩阵

测试类型 目标
算法单元测试 边界值、负数、IN、范围、非法值
Mapper 测试 逻辑 SQL 正确、分片键不丢
路由测试 真实执行单元数量符合预期
集成测试 两库多表真实 CRUD
事务测试 单库回滚、跨库失败边界
一致性测试 主从延迟、强制主库
升级回归 SQL 兼容性和规则行为
故障测试 宕库、断网、连接池耗尽
性能测试 精确路由、IN、多分片、全路由
迁移测试 全量、增量、校验、回滚

23.5 路由断言示例

可以在测试环境采集 ShardingSphere SQL 日志,对一次请求断言:

1
2
3
4
expected database count = 1
expected table count = 1
expected actual SQL count = 1
must not contain t_order_1(当预期 t_order_0)

如果使用 Proxy,则使用 PREVIEW SQL 构建黄金路由快照。

23.6 性能基线必须分场景

不要只跑一个 QPS。至少包含:

1
2
3
4
5
6
7
8
S1:单库单表点查
S2:单库多表 IN
S3:跨库点查
S4:全路由 COUNT
S5:跨分片 ORDER BY LIMIT
S6:绑定表 JOIN
S7:批量 INSERT
S8:事务写订单 + 明细

记录:

1
2
3
4
5
6
7
TPS/QPS
P50/P95/P99
route units
数据库 CPU/IO
连接池 active/pending
应用 CPU/Heap/GC
错误率

23.7 上线验收门槛示例

1
2
3
4
5
6
7
8
核心点查路由单元 = 1
在线接口全路由比例 = 0
P99 不高于单库基线的约定阈值
连接池 pending = 0(稳定负载)
数据库连接峰值低于预算
跨库失败场景有补偿和告警
迁移校验无金额差异
规则变更有回滚脚本

24. 一份推荐的生产组合

如果是 Spring Boot 3.5 的 Java 业务系统,我推荐优先:

1
2
3
4
5
6
7
8
9
10
11
ShardingSphere-JDBC 5.5.3
+ YAML 配置
+ Standalone 本地模式起步
+ MyBatis / JdbcTemplate
+ SNOWFLAKE 主键
+ INLINE / HASH_MOD 分片
+ bindingTables
+ BROADCAST 小字典表
+ LOCAL 事务
+ sql-show 开发开、生产关
+ Prometheus/Grafana 监控

如果是多语言、多系统统一接入:

1
2
3
4
5
6
7
ShardingSphere-Proxy 5.5.3
+ Cluster 模式
+ ZooKeeper / Etcd 元数据持久化
+ DistSQL 管理规则
+ 数据迁移能力
+ Agent 可观察性
+ Proxy 高可用

24.1 不同规模下的推荐演进

flowchart LR
    A[单库单表] --> B[索引与 SQL 优化]
    B --> C[缓存/归档/读写分离]
    C --> D[垂直拆库]
    D --> E[ShardingSphere-JDBC 分片]
    E --> F[Proxy/Cluster 集中治理]
    F --> G[迁移、弹性与统一数据平台]

**早期:**优先单库、索引、缓存、归档。

**增长期:**读写分离、按业务垂直拆库,降低单库负担。

**大规模:**核心大表水平分片,建立分片键约束、迁移和监控。

**平台期:**多语言统一接入、Proxy 高可用、DistSQL 审计、规则中心。

24.2 一份更保守的默认选择

对于多数 Spring Boot 3.5 服务:

1
2
3
4
5
6
7
8
9
10
11
12
JDK 21
Spring Boot 3.5.x
ShardingSphere-JDBC 5.5.3
MyBatis / JdbcTemplate
MySQL 8.x
HikariCP 小连接池起步
单分片键 Standard Strategy
SNOWFLAKE 或业务 ID 服务
LOCAL 事务
Outbox 最终一致性
Prometheus + OpenTelemetry
Flyway 由部署流水线管理真实表

先把主路径做窄、做准,再增加复杂能力。

25. 一个更现实的架构建议

分库分表不是第一步,而是系统演进到一定阶段后的手术。

在决定使用 ShardingSphere 之前,先确认是否已经做过:

  • SQL 优化。
  • 索引优化。
  • 冷热数据拆分。
  • 缓存优化。
  • 读写分离。
  • 垂直拆库。
  • 历史数据归档。
  • 报表查询隔离。

如果这些都没做,直接上分库分表,可能只是把一个慢系统升级成一个复杂的慢系统。

26. 完整生产架构示例

下面是一套相对完整但仍可渐进落地的架构:

flowchart TB
    U[客户端] --> GW[API Gateway]
    GW --> APP[Spring Boot 3.5 集群]

    APP --> SS[ShardingSphere-JDBC 5.5.3]
    SS --> G0[读写组 ds_0]
    SS --> G1[读写组 ds_1]

    G0 --> M0[(MySQL Master 0)]
    G0 --> R00[(Replica 0-0)]
    G0 --> R01[(Replica 0-1)]
    G1 --> M1[(MySQL Master 1)]
    G1 --> R10[(Replica 1-0)]
    G1 --> R11[(Replica 1-1)]

    APP --> REDIS[(Redis)]
    APP --> OUTBOX[(Local Event)]
    OUTBOX --> MQ[MQ]
    MQ --> SEARCH[(Elasticsearch)]
    MQ --> OLAP[(Doris / ClickHouse)]

    SS --> OTEL[OpenTelemetry]
    APP --> OTEL
    OTEL --> OBS[Prometheus / Grafana / Trace]

    CICD[CI/CD] --> DDL[分片 DDL 管理]
    DDL --> M0
    DDL --> M1

这套架构中:

  • ShardingSphere 负责 OLTP 透明路由;
  • Redis 只承担适合缓存的数据;
  • MQ + Outbox 解决跨聚合最终一致性;
  • Elasticsearch 解决多条件检索;
  • Doris/ClickHouse 解决报表聚合;
  • CI/CD 统一管理真实 DDL;
  • 可观察平台关联业务请求与真实 SQL。

27. 从单库迁移到分库分表的实施计划

阶段 0:证明问题存在

产出:

1
2
3
4
5
Top SQL
数据增长模型
单库资源瓶颈
三年容量预测
核心查询路径

阶段 1:分片建模

产出:

1
2
3
4
5
分片键决策记录 ADR
数据节点拓扑
绑定表/广播表/单表清单
事务边界
不支持查询清单

阶段 2:双环境验证

  • 本地 Docker 双库;
  • 测试环境接近生产规模;
  • 兼容性回归;
  • 路由快照;
  • 故障演练。

阶段 3:迁移与灰度

  • 全量;
  • 增量;
  • 校验;
  • 灰度读;
  • 灰度写;
  • 全量切换。

阶段 4:稳定性建设

  • 全路由审计;
  • 慢 SQL TopN;
  • 规则漂移检测;
  • 定期故障演练;
  • 容量复盘;
  • 扩容预案。

28. 安全与合规清单

数据安全

  • 敏感字段存储加密;
  • 查询结果脱敏;
  • 日志不打印敏感参数;
  • 密钥进入 KMS/Vault;
  • 备份同样加密;
  • 测试环境不使用生产明文数据。

访问安全

  • 应用账号最小权限;
  • DistSQL 管理账号隔离;
  • Proxy 业务与管理网络隔离;
  • 数据库只允许指定网段;
  • 定期轮换密码和证书;
  • 审计规则和账号变更。

供应链安全

  • 固定依赖版本;
  • 校验 Apache 发布包签名;
  • 扫描 Maven 依赖漏洞;
  • 关注 ShardingSphere Release Notes;
  • 镜像使用摘要锁定;
  • SBOM 归档。

29. 性能压测方案

29.1 数据准备

压测数据必须模拟:

1
2
3
4
5
6
分片均匀数据
热点用户
大租户
近期热数据
历史冷数据
不同状态分布

只生成完全均匀数据会掩盖热点。

29.2 压测阶梯

1
2
3
4
5
10% 目标流量:验证功能
30%:观察连接和数据库资源
60%:确认延迟拐点
100%:目标容量
120%~150%:验证保护和降级

29.3 对照组

至少对比:

1
2
3
4
直连单真实库
ShardingSphere 单路由
ShardingSphere 多路由
全路由

这样才能区分数据库耗时与中间件解析、执行、归并开销。

29.4 性能报告模板

场景 QPS P50 P95 P99 路由数 DB CPU 连接峰值 错误率
点查 1
IN 查询
深分页
聚合
批量写

30. 故障演练清单

1
2
3
4
5
6
7
8
9
10
11
12
关闭一个从库
关闭一个主库
主从延迟 10 秒
注册中心短暂不可用
Proxy 实例 kill -9
应用实例滚动重启
数据库连接池耗尽
执行 30 秒慢 SQL
迁移增量积压
错误分片规则发布
Snowflake 时钟回拨
MQ 重复投递

每个演练都记录:

1
2
3
4
5
6
7
影响范围
用户表现
告警触发时间
自动恢复时间
人工操作
数据一致性结果
改进项

31. 代码评审检查表

看到访问分片表的代码时,逐项问:

  • SQL 是否包含分库键和分表键?
  • 参数可能为 null 吗?
  • IN 条件最大多少?
  • 是否可能范围跨所有分片?
  • 是否深分页?
  • 是否跨分片排序/聚合?
  • JOIN 是否绑定表?
  • UPDATE/DELETE 是否精准路由?
  • 事务是否跨库?
  • 重试是否幂等?
  • 读请求是否允许从库延迟?
  • 日志是否泄露参数?
  • 是否有路由单元断言?

32. 常见问答

Q1:分库分表后还能使用数据库唯一索引吗?

单个真实表内可以,但它只能保证该物理表范围内唯一。全局唯一需要:

  • 全局 ID;
  • 路由前置唯一性服务;
  • 唯一索引表;
  • 业务上让唯一值包含分片维度;
  • 最终冲突检测与补偿。

Q2:能不能按时间分表,同时按用户查询?

可以,但用户跨时间范围查询会命中多张时间表。需要在“写入归档便利”和“用户查询便利”之间取舍。也可以分库按用户、分表按时间,但查询条件最好同时包含两者。

Q3:分片后能不能随便 JOIN?

不能。绑定表、广播表和同分片键 JOIN 最友好;非绑定跨分片 JOIN 需要重点验证路由组合和 SQL 支持。

Q4:为什么不推荐所有查询都用 Hint?

Hint 把路由正确性推给业务代码,容易因上下文丢失或残留导致错路由。只有分片键确实不在 SQL、且业务上下文可靠时使用。

Q5:ShardingSphere 能代替数据库代理吗?

Proxy 本身可以作为数据库网关,但 JDBC 不是通用网络代理。是否需要 Proxy 取决于语言、集中治理、运维和协议要求。

Q6:单表多大必须分片?

没有统一阈值。要结合行宽、索引、查询方式、写入量、DDL 窗口和硬件能力。一个 1 亿行只做主键点查的表,可能比一个 1000 万行复杂聚合表更轻松。

Q7:是否应该先分 1024 张表?

通常不应该。可以预留 1024 个逻辑槽位,但物理表数量要根据容量和运维能力决定。

Q8:读写分离会不会自动保证刚写数据可见?

不会。使用事务内主库、Hint 强制主库、会话粘滞或版本等待策略。

Q9:Spring Boot 3.5 能否直接使用 XA?

ShardingSphere 5.5.3 官方文档明确提示 Jakarta EE 9+/Spring Boot 3 场景下 XA 尚未就绪。不要把“能配置”当成“生产可用”。

Q10:如何判断分片是否成功?

不是只看数据插入成功,而要验证:逻辑结果、物理落点、路由数量、事务边界、故障行为和性能基线。

33. 术语表

术语 含义
Logic Database 应用看到的逻辑数据库
Storage Unit ShardingSphere 管理的真实数据源
Logic Table SQL 中使用的逻辑表名
Actual Table 数据库中真实存在的物理表
Actual Data Node 数据源与真实表的组合
Sharding Column 用于计算路由的字段
Sharding Strategy 提取分片值并调用算法的策略
Sharding Algorithm 将分片值映射到目标节点的算法
Binding Table 分片规则一致、可同路由 JOIN 的表组
Broadcast Table 在多个数据源保存副本的表
Single Table 被管理但不参与分片的表
Route Unit 路由生成的逻辑目标单元
Execution Unit 改写后可执行的真实 SQL 单元
DistSQL 用 SQL 管理 ShardingSphere 规则的语言
Pipeline 数据迁移和弹性相关能力
Hint 由业务上下文显式指定路由
Merge 把多分片结果合成为逻辑结果

34. 最终上线检查清单

架构

  • 已证明单库优化不足;
  • 分片键有真实 SQL 数据支撑;
  • 绑定表、广播表、单表清单明确;
  • 管理端复杂查询有独立方案;
  • 扩容和回滚方案已评审。

配置

  • 锁定 ShardingSphere 5.5.3;
  • 未混入旧 Starter;
  • 每个真实数据源连接池已预算;
  • 生产密码不在 Git;
  • 元数据检查已验证;
  • SQL 日志生产默认关闭。

SQL

  • 核心查询精准路由;
  • UPDATE/DELETE 带分片键;
  • 无不可控深分页;
  • 聚合 SQL 有压测;
  • JOIN 已验证绑定关系;
  • ORM 动态 SQL 不会丢分片键。

事务与一致性

  • 跨库事务边界清楚;
  • LOCAL 不被误当强一致;
  • 读己之写策略明确;
  • 消息和补偿幂等;
  • 对账任务已实现;
  • 故障演练通过。

运维

  • 所有真实表结构一致;
  • DDL 发布流程明确;
  • 迁移全量/增量/校验可观测;
  • 规则指纹和漂移告警已建立;
  • 全路由、连接池、主从延迟有告警;
  • 备份恢复已演练。

35. 结论

ShardingSphere 的价值不是让应用“感觉不到分库分表”,而是把大量重复的 SQL 解析、路由、改写、执行和归并能力标准化。透明只存在于 API 表面,架构复杂度不会凭空消失。

真正高质量的落地有几个共同点:

  1. 分片键来自访问模式,而不是来自表字段里“看起来像 ID”的那一列;
  2. 核心 SQL 默认精准路由,全路由是被识别、限制和监控的例外;
  3. 事务边界通过业务聚合设计收敛,跨库一致性有消息、补偿和对账;
  4. 扩容从第一天就考虑逻辑槽位、迁移、灰度和回滚;
  5. 监控不仅看数据库 CPU,还看路由数、执行单元、连接池等待和归并耗时;
  6. 每次规则、版本和 DDL 变化都经过自动化回归。

分库分表最怕的不是复杂,而是复杂却不可见。把规则、路由、事务和迁移全部变成可验证的工程对象,ShardingSphere 才会真正成为基础设施,而不是新的不确定性来源。

参考资料

启示录

分库分表不是为了证明架构复杂,而是为了让数据在增长之后仍然可控。

好的架构不是把所有功能都堆上去,而是在每一个功能背后都知道自己为什么需要它、什么时候不用它、出了问题如何收回来。

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Spring Boot 3.5 整合 Apache ShardingSphere 5.5.3 深度实践
https://allendericdalexander.github.io/2026/06/11/java/spring/Spring-Boot-3.5-ShardingSphere-5.5-deep-guide/
作者
AtLuoFu
发布于
2026年6月11日
许可协议