欢迎你来读这篇博客,这篇博客主要是关于 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 很强,但它不是万能胶。分库分表会让系统复杂度明显上升,一旦用了,就要认真面对数据建模、分片键选择、跨库查询、全路由、分布式事务、扩容迁移、监控排查这些问题。它不是“加个依赖解决数据库性能问题”,更像是你给数据库系统加了一层交通枢纽。交通枢纽能提效,也会带来新的规则。
正文 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 方案,不要只看配置能启动。
能通过业务设计规避跨库事务,就尽量规避。
订单、结算、库存这类场景可以考虑最终一致性、消息表、补偿任务、对账任务。
分库分表之后还想继续像单库事务一样随便写,这是很多事故的开始。数据库不会因为我们加了中间件就突然变温柔。
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 与治理中心配置冲突。
变更规则时只更新了一侧。
不同入口对同一张逻辑表的路由结果不同。
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_0、demo_ds.t_order_1
只分库:demo_ds_0.t_order、demo_ds_1.t_order
分库又分表:demo_ds_${0..1}.t_order_${0..15}
3.2 读写分离 读写分离用于主从架构:
写 SQL 路由到主库。
读 SQL 路由到从库。
事务内读请求默认可以路由到主库,保证读己之写。
从库可以通过随机、轮询、权重等算法负载均衡。
3.3 分布式事务 ShardingSphere 提供:
但是 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 则用于捕获数据变更,适合数据同步、数据订阅、异构系统集成。
4. 核心概念 想把 ShardingSphere 用稳,先把这些概念吃透。
4.1 逻辑库 应用看到的是逻辑库,例如:
它不一定对应真实数据库,而是 ShardingSphere 对外暴露的数据库名称。
4.2 真实库 真实库是实际数据库实例里的 schema,例如:
4.3 逻辑表 应用 SQL 里写的表名:
1 2 3 select * from t_orderwhere 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 绑定表 绑定表用于避免关联查询时出现笛卡尔路由。
例如:
如果二者都按 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 分布式主键 分库分表后,数据库自增主键容易重复,所以常用:
ShardingSphere 内置 SNOWFLAKE 和 UUID,也可以自定义。
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"
如果开启了:
日志里会看到逻辑 SQL 和真实 SQL。
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_orderwhere user_id = 10 and order_id = 10086 ;
根据规则:
1 2 database = ds_${user_id % 2} table = t_order_${order_id % 2}
会路由到:
如果 SQL 缺少分片键:
1 2 3 select * from t_orderwhere 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 分库键和分表键可以不同吗 可以。
例如:
优点:
用户维度数据落到固定库。
订单维度在库内分散到不同表。
缺点:
只带 order_id 不带 user_id 的查询,可能无法精准定位库。
如果业务经常只用 order_id 查订单,建议:
让 order_id 本身携带分片信息。
建立 order_id -> user_id 或 order_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 复合分片 当分片需要多个字段:
可以使用 complex 或 COMPLEX_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()) { } } } } }
Hint 使用 ThreadLocal,务必使用 try-with-resources 自动关闭。不关闭会污染当前线程后续请求,这种问题很阴,排查起来让人想喝三杯冰水冷静一下。
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_ds 或 read_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(); }
例如:
刚写完马上查。
支付状态确认。
库存扣减确认。
后台人工审核刚提交的数据。
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_0、ds_1 是读写分离逻辑数据源名称,不是物理库名称。
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 业务建议 对于订单、结算、交易类系统,我更推荐:
如果外部暴露 ID,可以另加业务单号:
1 2 order_id: 内部主键,BIGINT order_no: 外部单号,VARCHAR
不要把数据库主键、业务单号、幂等号、支付流水号全部混成一个字段。字段少了不代表架构干净,有时候只是以后的人要替你擦桌子。
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 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, emailfrom t_userwhere phone = '13800001111' ;
ShardingSphere 会改写到真实密文字段。
10.5 加密实践建议
密钥不要明文写在 Git 仓库。
密钥建议接入 KMS、环境变量、配置中心加密能力。
加密字段长度要预留足够。
查询字段需要辅助查询列,否则等值查询会很难做。
模糊查询加密成本高,能不用就不用。
旧数据加密迁移要设计双写、回填、校验、切换步骤。
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 使用建议
后台管理系统可以用脱敏降低误泄露风险。
不同角色需要不同脱敏策略时,建议在应用层结合权限控制。
脱敏不是权限系统,不要用脱敏替代鉴权。
审计日志里也要注意敏感字段,不然数据库脱敏了,日志又把数据泄露出去。
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 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。
不要让影子规则误伤正常生产流量。
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 工程建议 真正稳定的分库分表系统,通常不是靠到处上分布式事务,而是靠:
分片键让同一业务聚合落同库。
明确聚合边界。
使用本地事务完成单库写入。
跨库动作通过消息、任务、补偿、对账完成。
关键状态机可重入、可恢复、可幂等。
14. ShardingSphere-Proxy 14.1 为什么需要 Proxy JDBC 适合 Java 应用内嵌,Proxy 适合平台化。
Proxy 价值:
多语言统一接入。
规则集中管理。
支持 DistSQL。
运维视角更清晰。
应用无需引入 ShardingSphere 依赖。
14.2 启动方式 常见方式:
二进制启动:
1 sh /opt/shardingsphere-proxy/bin/start.sh
默认端口通常是:
连接:
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 应用 / 微服务
平台化数据库网关
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 PREVIEWSQL select * from t_orderwhere user_id = 10 and order_id = 10086 ;
这个命令非常适合排查路由问题。路由问题不要靠猜,猜数据库路由跟猜对象心思差不多,都容易误伤自己。
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 数据校验 至少校验:
行数。
主键范围。
核心金额字段合计。
状态分布。
更新时间最大值。
抽样比对。
订单、财务、结算系统只校验行数是不够的,金额合计一定要校验。
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 主从延迟 连接池等待 跨库事务异常
分库分表系统里,全路由是性能事故的温柔前奏。它一开始只是慢一点,后来就会很有存在感。
18. SQL 支持与限制 分库分表不是完整分布式数据库,SQL 能力一定有边界。
18.1 友好的 SQL 1 2 3 4 5 6 7 8 9 10 11 12 select * from t_orderwhere user_id = ? and order_id = ?;select * from t_orderwhere 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_orderwhere status = 'CREATED' ;select count (* )from t_order;select * from t_orderorder by create_time desc limit 100000 , 20 ;select * from t_orderwhere amount between 100 and 200 ;
问题:
缺少分片键。
可能全库全表扫描。
跨分片排序分页成本高。
count 需要多分片归并。
18.3 关联查询 好的关联:
1 2 3 4 5 select o.order_id, i.sku_codefrom t_order o join t_order_item i on o.order_id = i.order_idwhere 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_idwhere u.level = 'VIP' ;
如果 t_user 没有相同分片策略,可能出现跨库关联或全路由。
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 张表。表多不是架构先进,有时候只是把复杂度提前透支。
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
最大连接数理论上是:
这还没算其他应用。很多数据库连接数爆掉,不是业务突然变猛,而是连接池配置像开自助餐,大家都拿满了。
20.3 推荐连接池计算方式 先从小值开始:
1 单应用单数据源 maximumPoolSize = 10 ~ 20
观察:
Hikari active connection。
pending threads。
DB CPU。
SQL 平均耗时。
慢查询。
再逐步调整。
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 指定路由。
对管理端查询单独建搜索索引或报表库。
调整分片键。
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_orderwhere user_id = #{userId} and order_id = #{orderId}
不要在 Mapper 里写真实表名:
这会绕开逻辑表模型,后期维护会很难受。
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 脚本管理真实表。
23. 工程落地清单 23.1 接入前必须回答的问题
数据量为什么需要分库分表?
当前瓶颈是存储、查询、写入、连接数,还是索引设计?
主要查询路径是什么?
分片键是什么?
是否存在大量不带分片键的查询?
是否需要跨库事务?
如何扩容?
如何迁移旧数据?
如何回滚?
如何监控全路由和慢 SQL?
23.2 开发阶段清单
本地准备两个真实库。
建完整真实表。
打开 sql-show。
写插入测试。
写按分片键查询测试。
写缺少分片键查询测试,观察是否全路由。
写绑定表 join 测试。
写广播表测试。
写事务回滚测试。
23.3 上线前清单
关闭生产 sql-show。
检查连接池总连接数。
检查真实库索引。
压测核心 SQL。
压测全路由边界。
验证主从延迟。
验证备份恢复。
准备回滚脚本。
准备路由排查手册。
接入监控指标。
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 高可用
25. 一个更现实的架构建议 分库分表不是第一步,而是系统演进到一定阶段后的手术。
在决定使用 ShardingSphere 之前,先确认是否已经做过:
SQL 优化。
索引优化。
冷热数据拆分。
缓存优化。
读写分离。
垂直拆库。
历史数据归档。
报表查询隔离。
如果这些都没做,直接上分库分表,可能只是把一个慢系统升级成一个复杂的慢系统。
参考资料
启示录 分库分表不是为了证明架构复杂,而是为了让数据在增长之后仍然可控。
好的架构不是把所有功能都堆上去,而是在每一个功能背后都知道自己为什么需要它、什么时候不用它、出了问题如何收回来。
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。