ShardingSphere-jdbc 5.5.0 分库分表实战:从分片规则到读写分离
先说结论如果你的服务还在单库单表硬扛数据量到了千万级、写入吞吐上不去、慢查询明显变多与其继续靠加索引硬撑不如认真考虑一下分库分表。而在所有分库分表方案里ShardingSphere-jdbc 5.5.0 配合 Spring Boot 做基础配置是我个人认为成本最低、见效最快的一条路。这篇文章就把我这次搭建环境、配分片规则、跑通读写分离、排坑调优的完整过程写出来给准备上车的人一份可以直接抄作业的参考。文章适合三类人看一是项目数据量正在快速增长、想提前做技术预演的后端开发二是已经决定分库分表、但被 ShardingSphere 5.x 配置结构绕晕的入门者三是想从 4.x 老版本迁移到 5.5.0 的同学。下面我按实际操作的先后顺序来写尽量把每一步背后的原因讲清楚。1. 从单库瓶颈到分库分表为什么最终选了 ShardingSphere-jdbc1.1 单库瓶颈的三个信号我做分库分表不是心血来潮是业务数据把我逼到这个位置的。订单表数据量到了差不多三千万行单表索引层级变深写入锁竞争明显高峰期数据库 CPU 经常飙到 80% 以上。当时我总结了三个必须上分库分表的信号如果你也遇到了基本就说明单库单表到极限了单表数据量超过千万级即使走了索引随机 IO 和索引维护成本也在肉眼可见地上升。写入吞吐成为瓶颈主库的 binlog 同步延迟、锁等待、磁盘 IO 都开始报警。慢查询越来越难优化不是 SQL 写得烂而是数据量本身太大索引再优化也有限。这时候摆在我面前的无非两条路要么升级硬件要么拆分数据。升级硬件治标不治本数据还会继续涨所以我决定做分库分表。1.2 各分库分表方案的横向对比当时我调研了几个主流方向列个对比表方便你直接看结论方案架构模式部署成本代码侵入适合场景ShardingSphere-jdbc客户端分片应用内直连数据库无独立部署随应用启动低对业务代码几乎透明中小团队、单体/微服务应用、不想维护中间件ShardingSphere-proxy独立代理层类似数据库中间件需单独部署、运维低可当数据库直连多语言异构系统、不想改应用连接的场景MyCat独立代理层需单独部署低早期方案靠 XML 配置管理新特性更新偏慢TiDB / 分布式数据库存储层分布式高需要专门团队最低数据量极大、预算充足、需要弹性扩展我最终选了 ShardingSphere-jdbc核心原因有三个。第一它不需要额外部署中间件不增加运维负担一个 Spring Boot 服务直接集成第二它是在 JDBC 层做分片应用拿到的还是一个 DataSourceMyBatis、MyBatis-Plus 这些框架不用改任何代码逻辑第三ShardingSphere 已经是 Apache 顶级项目社区活跃度在 Java 分库分表方案里排第一。1.3 5.5.0 版本值得关注的几点变化ShardingSphere 5.x 和 4.x 的配置结构差别非常大网上大量资料还停留在 4.x 的spring.shardingsphere.sharding.tables...的写法直接搬到 5.5.0 上会启动报错。5.5.0 里规则配置统一收敛到spring.shardingsphere.rules下面数据源和规则分得很清楚这点后面我会展开写。另外 5.5.0 对 Spring Boot 3.x 和 JDK 17 的支持已经非常成熟如果你是新项目建议直接用 Spring Boot 3.2 和 JDK 17没必要再从 Spring Boot 2.x 起步。旧项目升级的话也还好配置迁移主要就是改规则结构SQL 层基本不受影响。2. 环境准备与依赖引入先把版本坑填平2.1 Maven 依赖的准确坐标这一步是很多新手翻车的地方。ShardingSphere-jdbc 在 5.x 之后的 starter 坐标已经改成了shardingsphere-jdbc-spring-boot-starter网上很多旧的sharding-jdbc-spring-boot-starter坐标早就不能用了。我使用的是dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-spring-boot-starter/artifactId version5.5.0/version /dependency这里需要提醒两个细节。第一个是版本冲突。ShardingSphere 5.5.0 内部依赖了特定版本的 gson、snakeyaml、guava 等库如果你的项目里还有其他框架传递依赖很容易出现 jar 包冲突。我这次就遇到了 guava 的NoSuchMethodError排查了半天发现是一个老的工具包传递依赖把 guava 拉低到了 18.0。解决办法是在pom.xml里用 dependencyManagement 显式统一相关依赖的版本或者把冲突的传递依赖排除掉。第二个是数据库驱动。我用的是 MySQL 8.0.33驱动必须用com.mysql:mysql-connector-j老式的mysql-connector-java坐标也行但注意 5.5.0 里对驱动类名com.mysql.cj.jdbc.Driver是强要求的如果你的库是 MySQL 5.7 且用的老驱动建议一并升级。2.2 初始化分库分表DDL 脚本与数据节点规划配置 ShardingSphere 之前物理表要先建好。我用的是最经典的 2 库 × 2 表方案订单表t_order拆到 ds0 和 ds1 两个库里每个库各建 t_order_0 和 t_order_1 两张物理表。DDL 脚本大概是这样的CREATE DATABASE IF NOT EXISTS ds0 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE DATABASE IF NOT EXISTS ds1 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE ds0.t_order_0 ( order_id BIGINT NOT NULL COMMENT 订单ID分片键, user_id BIGINT NOT NULL COMMENT 用户ID, order_no VARCHAR(64) NOT NULL COMMENT 订单号, amount DECIMAL(10, 2) NOT NULL COMMENT 订单金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), KEY idx_user_id (user_id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE ds0.t_order_1 LIKE ds0.t_order_0; CREATE TABLE ds1.t_order_0 LIKE ds0.t_order_0; CREATE TABLE ds1.t_order_1 LIKE ds0.t_order_0;如果你用CREATE TABLE ... LIKE建表主键、索引都会复制比较省事。我用这个方式把四个物理表一次建齐了。同时建议把 user_id 的索引加上因为按用户查订单也是高频查询但没有把它作为分片键所以它是普通索引。这里要理解一个核心概念应用层写的表名是逻辑表t_orderShardingSphere 会按照你配置的分片规则把 SQL 改写成分别到 ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 上执行的路由 SQL。物理表名可以不和逻辑表名一致完全由actual-data-nodes和分片算法决定。2.3 4.x 迁移到 5.x 的配置结构变化很多老项目用的是 4.x迁移到 5.5.0 时最大的坑就是配置结构变了。我把核心差异列一下配置项4.x 写法5.x 写法starter 坐标sharding-jdbc-spring-boot-startershardingsphere-jdbc-spring-boot-starter规则前缀spring.shardingsphere.sharding.tablesspring.shardingsphere.rules.sharding.tables数据源配置spring.shardingsphere.datasourcespring.shardingsphere.datasource不变分片算法配置spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column独立放在 rules.sharding.sharding-algorithms 下表规则里引用算法名主键生成器内嵌在 table 配置里独立放在 rules.sharding.key-generators 下5.x 把分片算法、主键生成策略从表规则里拆出来了这样多个表可以复用同一个算法配置冗余更少但结构看起来也更绕。说实话第一次看 5.x 的官方示例我也懵了一下习惯之后会发现这种设计确实更合理。3. 核心配置逐项拆解每个配置参数背后的逻辑3.1 多数据源配置HikariCP 参数不能照抄默认值这是整个配置的地基。5.5.0 里多数据源统一写在spring.shardingsphere.datasource下我用的连接池是 HikariCPSpring Boot 2.x 和 3.x 默认就带它省得额外引入依赖。spring: shardingsphere: datasource: names: ds0, ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.101:3306/ds0?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8rewriteBatchedStatementstrue username: root password: your_password max-pool-size: 20 min-idle: 5 connection-timeout: 30000 idle-timeout: 600000 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.101:3306/ds1?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8rewriteBatchedStatementstrue username: root password: your_password max-pool-size: 20 min-idle: 5 connection-timeout: 30000 idle-timeout: 600000 props: sql-show: true有几个参数我特别说明一下。max-pool-size不是越大越好。分库分表之后一个请求可能同时占用多个数据源的连接如果连接池开得过大数据库端连接数会被撑爆。我这次压测时把 max-pool-size 从默认的 10 调到 20实测并发 500 时数据库连接数在 40 左右是安全的。如果你的服务是多实例部署这个数还要再往低调否则数据库连接总数 实例数 × 连接池大小 × 数据源数量很容易超限。rewriteBatchedStatementstrue一定要加。这个参数配合 JDBC 批量插入时MySQL 会把多条 insert 重写成多值 insert性能提升非常明显。后面讲批量插入时我会再详细说。还有一个容易忽略的点jdbc-url 里的serverTimezone必须写否则连接 MySQL 8 会报时区异常。字符集用 utf8mb4别用 utf8否则 emoji 和生僻字会乱码。3.2 分片规则配置分片键、分片算法、绑定表、广播表数据源配好之后重头戏是分片规则。下面是我这次用的完整配置spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_db_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_table_inline key-generate-strategy: column: order_id key-generator-name: snowflake_order t_order_item: actual-data-nodes: ds$-{0..1}.t_order_item_$-{0..1} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_db_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_item_table_inline binding-tables: - t_order, t_order_item broadcasting-tables: - t_dict sharding-algorithms: order_db_inline: type: INLINE props: algorithm-expression: ds$-{order_id % 2} order_table_inline: type: INLINE props: algorithm-expression: t_order_$-{(order_id / 2) % 2} order_item_table_inline: type: INLINE props: algorithm-expression: t_order_item_$-{(order_id / 2) % 2} key-generators: snowflake_order: type: SNOWFLAKE props: worker-id: 2 props: sql-show: true我一个个拆开说。actual-data-nodes定义了逻辑表映射到哪些物理表ds$-{0..1}.t_order_$-{0..1}是 Groovy 表达式表示 ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 四张物理表。也可以写完整枚举ds0.t_order_0,ds0.t_order_1,...但生产环境数据节点多了之后用范围表达式才是正解。分片算法这里我用的是 INLINE 类型。INLINE 支持 Groovy 表达式简单高效但要注意两个坑第一个坑表达式里的取模运算。我用order_id % 2做库分片用(order_id / 2) % 2做表分片这样 order_id 从 0 到 3 会分别落在 ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 上分布是均匀的。如果你用order_id % 2既做库分片又做表分片那么 order_id 为偶数的只会进 ds0.t_order_0奇数的只会进 ds1.t_order_1剩下两张表永远是空的相当于只拆了两张表数据倾斜会非常严重。第二个坑INLINE 表达式的 Groovy 语法在 YAML 里的转义。$-{...}里的$符号在 YAML 中不能加引号包起来否则会解析失败。我第一次写的时候给表达式整体加了双引号结果启动直接报错。如果你在 IDE 里看到表达式变红了多半是引号问题。分片键的选择上我用了 order_id 同时做库分片和表分片这样同一个订单的数据一定在同一个库的同一张表里方便后续扩展订单详情表做关联查询。如果你的业务经常按 user_id 查订单也可以在 user_id 上建一个索引但查询时不带 order_id 的话会全库路由这点后面排错章节会细讲。binding-tables配置绑定表非常关键。我把 t_order 和 t_order_item 绑定在一起因为这两个表的分片键都是 order_id。绑定表的作用是当 SQL 中两个表做 JOIN 时ShardingSphere 会保证它们路由到同一组物理表上避免出现笛卡尔积关联性能差距巨大。如果没有配置绑定表SELECT * FROM t_order o JOIN t_order_item i ON o.order_id i.order_id会被拆成 4×4 共 16 种组合去执行即使结果正确也是灾难。broadcasting-tables我配置了 t_dict 字典表。广播表的特点是每个数据节点都会存放一份全量数据写入时所有节点同步写查询时随机路由到任意节点。适合存放业务字典、配置类数据因为这类数据量小、读多写少全节点冗余是最优解。3.3 分布式ID生成雪花算法在 5.5.0 中的配置分库分表之后数据库自增主键就不能用了因为多个库各自自增会产生重复 ID。我用的主键生成策略是雪花算法 SNOWFLAKE配置如下key-generators: snowflake_order: type: SNOWFLAKE props: worker-id: 2在表规则里通过key-generate-strategy把它挂到 order_id 列上。这样插入数据时不显式传 order_idShardingSphere 会自动生成一个分布式 ID 填充进去。关于 worker-id有一点必须说清楚同一个分片集群里的多个应用实例worker-id 必须不同否则极端情况下会生成重复 ID。我这次在测试环境只部署了一个实例所以写死 2 没问题生产环境多个实例时建议把 worker-id 做成环境变量每个实例传不同的值。3.4 读写分离配置逻辑数据源与分片规则组合如果你的 MySQL 已经做了主从复制可以用 ShardingSphere 的读写分离能力把读流量分发到从库。这一块在 5.5.0 里配置位置变了是在rules.readwrite-splitting下spring: shardingsphere: datasource: names: ds_master, ds_slave0, ds_slave1, ds0, ds1 ds_master: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.201:3306/ds0 ... ds_slave0: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.202:3306/ds0 ... ds_slave1: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.203:3306/ds0 ... rules: readwrite-splitting: >Mapper public interface OrderMapper { Insert(INSERT INTO t_order(order_id, user_id, order_no, amount, status) VALUES(#{orderId}, #{userId}, #{orderNo}, #{amount}, #{status})) int insert(OrderEntity order); Select(SELECT * FROM t_order WHERE order_id #{orderId}) OrderEntity selectById(Param(orderId) Long orderId); }这里要重点理解一个代价路由是靠分片键算出来的。如果 SQL 的 WHERE 条件里不带分片键比如SELECT * FROM t_order WHERE user_id 1001ShardingSphere 不知道该去哪个节点查就只能全库全表路由把四张物理表全部查一遍再归并。这个行为在配置层面是合法的不会报错但性能差别很大。所以分片键必须成为你所有高频查询的必经之路如果做不到就要接受全路由的性能损耗。4.2 分页查询PageHelper 的兼容性需要注意如果你的项目以前用 PageHelper 做分页接入 ShardingSphere 之后建议做一次全面回归测试。PageHelper 的分页 SQL 改写和 ShardingSphere 的 SQL 归并逻辑在某些复杂 SQL 下会打架我这次就遇到过一个多表 LEFT JOIN 的分页查询PageHelper 生成的 count 语句没带分片键导致 count 走了全路由接口响应从 50ms 变成 1s。我的建议是简单单表分页查询PageHelper 基本没问题可以直接用。多表关联、子查询、GROUP BY 这种复杂 SQL最好改用 MyBatis-Plus 的IPage接口它和 ShardingSphere 的兼容性更好一些。深度分页要特别小心。LIMIT 100000, 20这种写法在分库分表下会把每个分片的前 100020 条数据都捞出来然后在内存里归并排序最后才取 20 条。数据量一大内存和耗时都扛不住。这个问题不是配置能解决的需要在业务侧做改造比如改成基于游标的分页或者限制只能翻到前多少页。4.3 绑定表 JOIN 的写法要求配置了binding-tables之后t_order 和 t_order_item 的 JOIN 查询不需要额外处理直接写SELECT o.order_no, i.item_name FROM t_order o JOIN t_order_item i ON o.order_id i.order_id WHERE o.order_id #{orderId}ShardingSphere 会把这两个表路由到同一个物理分片上比如 route 到 ds0.t_order_0 时同时也会把 t_order_item 路由到 ds0.t_order_item_0然后在这个物理库上执行 JOIN。这比笛卡尔积关联快了不止一个数量级。但如果你 JOIN 的两张表分片键不一致比如 t_order 按 order_id 分片t_user 按 user_id 分片那么 JOIN 时 ShardingSphere 只能做全路由而且跨分片 JOIN 的结果归并逻辑非常复杂性能难以保证。这类查询尽量别走数据库 JOIN改成应用层先查一张表再根据结果拼第二张表的查询反而更可控。4.4 批量插入的注意点批量插入订单是运营后台的常见操作我就用 MyBatis 的 foreach 批量插入测试了一下。rewriteBatchedStatementstrue加上之后JDBC 会自动合并 insert 语句ShardingSphere 会按分片键把同分片的记录合并到一起批量执行性能提升很明显。但要注意分布式 ID 的生成方式。批量插入时如果不传 order_idShardingSphere 会对每条记录单独生成一个雪花 ID这是没问题的。但如果你为了省事自己手动在代码里用同一个 ID 去 set 多条记录那就会导致主键冲突因为同分片下主键不能重复。还有一个坑MyBatis 的 foreach 拼接 SQL 时参数占位符数量有限制MySQL 默认的 max_allowed_packet 是 64MB但 SQL 文本本身太大也会被服务端拒绝。我一般每次批量插入控制在 500 条以内实测性能和稳定性都比较好。再大的批次拆成多个子批次循环执行用事务包起来。5. 高频问题排错实录我这次配置遇到的五个坎5.1 逻辑表没配置分片规则就执行 SQL这是我调通配置后遇到的第一个报错。报错信息大概是ShardingSphere rule does not support the table: t_dict原因是我在 SQL 里查了 t_dict但当时还没来得及把它加进broadcasting-tables。ShardingSphere 对逻辑表的管理很严格一张表要么配置了分片规则要么是广播表要么是默认数据源里的普通表否则执行 SQL 时会直接拒绝。解决办法很简单把 t_dict 加进广播表配置重启服务就好了。但这里的经验是接 ShardingSphere 之前先把业务库里的表分个类——哪些做分片表、哪些做广播表、哪些走默认数据源先规划好再写配置能省掉很多来回排查的时间。5.2 INLINE 表达式解析异常刚才提过我给 INLINE 算法配置的前缀表达式加了双引号启动时直接抛了 YAML 解析异常。这里把所有引号去掉之后就好了algorithm-expression: ds$-{order_id % 2}而不是algorithm-expression: ds$-{order_id % 2}更深一层的坑是如果你用了 Spring 的配置中心比如 Nacos、Apollo动态管理 YAML配置里的$-{...}可能会被配置中心当成占位符解析导致表达式被篡改。解决办法是把配置项改写成${...}转义形式或者把表达式调整为 Groovy 风格后再验证一次。反正记住一点表达式改完之后一定要看一眼打印出来的日志确认实际生效的表达式是你写的那样。5.3 雪花算法主键冲突某天线上突然出现主键冲突异常查了半天发现是测试环境的两个服务实例都配置了worker-id: 1再加上某个实例部署时系统时钟发生了回拨雪花算法生成的 ID 就有概率重复。雪花算法本身依赖机器时钟和 worker-id 保证唯一性worker-id 冲突和时钟回拨都会导致 ID 重复。ShardingSphere 内置的 SNOWFLAKE 算法对时钟回拨做了一定处理但跨毫秒的回拨场景下仍可能出问题。我的处理方式是把 worker-id 提取到环境变量每个实例通过部署平台注入不同的值同时把服务所在机器的时钟同步打开尽量缩小回拨窗口。如果你们的架构里已经部署了统一的分布式 ID 服务比如美团 Leaf、滴滴 Tinyid也可以让 ShardingSphere 走自定义的 key generator不一定要用内置雪花。5.4 跨库事务回滚不全这是分库分表绕不开的痛点。默认情况下ShardingSphere-jdbc 的事务是本地事务也就是说一个事务里如果涉及多个数据源比如同时往 ds0 和 ds1 写数据任何一方失败本地事务只能回滚自己这个数据源上的操作另一个数据源上已经执行成功的操作是回不掉的。我这次在测试环境专门构造了一个场景往 t_order 插入数据后故意在代码里抛了一个 RuntimeException结果 ds0 上的数据回滚了ds1 上的那条记录还留在表里。如果你要保证跨库事务的原子性需要引入 Seata 或者 XA 分布式事务方案。ShardingSphere 对 Seata 的支持是通过ShardingSphereTransactionType注解配合SeataATShardingSphereTransactionManager实现的配置上稍微复杂。我个人的建议是业务上能避免跨库事务就尽量避免比如把一次操作的数据设计成落在同一个分片上实在避不开的大额资金类操作再上分布式事务。基础配置阶段先把本地事务的边界搞清楚别默认它是万能的。5.5 启动报循环依赖我在第一次配置读写分离时服务启动直接报了一个循环依赖异常The dependencies of some of the beans in the application context form a cycle排查后发现我的项目里自己定义了一个 DataSource 配置类使用了ConfigurationProperties去读spring.datasource配置而 ShardingSphere starter 也会注册一个 DataSource两者互相引用形成了循环。解决办法有两个第一个把自定义的 DataSource 配置类删掉让 ShardingSphere 接管数据源管理第二个如果你的业务确实需要访问原始 DataSource比如做一些监控统计在注入处加Lazy注解打破循环。我选择了前者简化配置减少维护点。6. 压测验证与后续优化基础配置只是开始6.1 打开 sql-show 观察真实路由逻辑接入 ShardingSphere 后第一件该做的事就是打开sql-show: true。启动服务后执行一条查询控制台会打印出逻辑 SQL 和实际路由到物理表的 SQL。我强烈建议你在压测前先肉眼确认每个典型 SQL 的路由结果而不是直接压测。比如插入一条 order_id5 的数据理论上应该路由到 ds1.t_order_0日志里会打印类似的执行信息。如果发现路由结果和预期不一致那一定是分片算法配置有问题趁早改别等压测数据一跑发现数据全写偏了再回头查那会非常痛苦。还有一个细节sql-show: true在生产环境一定要关掉。它会把 SQL 内容和参数打印到日志里一是性能有损耗二是有信息泄露风险。我一般是用环境变量来控制测试环境开生产环境关。6.2 带分片键和全路由的性能差距实测我简单做了一组对比测试查询条件分别带和不带 order_id查询方式路由情况耗时WHERE order_id 1001精确路由到单个分片约 10msWHERE user_id 1001全路由四个分片都查约 200ms且耗时随数据量增长明显这个对比足以说明问题不带分片键的查询不是不能用但必须清楚地认识到它的代价。生产环境如果有这种低频后台查询场景建议把结果缓存到 Redis如果是高频查询那说明分片键设计可能不合理需要重新审视。6.3 特殊查询的兜底Hint 强制路由有时候 SQL 里没有分片键但代码里能确定分片值比如先根据 user_id 查出 order_id 的列表再查订单详情。这种情况可以用 Hint 强制路由手动指定分片值HintManager hintManager HintManager.getInstance(); hintManager.addDatabaseShardingValue(t_order, orderId); hintManager.addTableShardingValue(t_order, orderId); try { // 执行查询 t_order 的 SQL } finally { hintManager.close(); }注意HintManager是线程私有的用完之后必须 close否则会泄漏到线程池里的其他请求。我在压测时踩过一次某个线程池复用的请求全部走了错误的 Hint查出来的数据对不上排查了很久才发现是 HintManager 没有关闭。Hint 的另一个典型场景是强制走主库查询。如果主从同步延迟导致刚写入的数据在从库查不到可以在读操作前调用hintManager.setWriteRouteOnly()强制路由到主库保证读的是最新数据。6.4 后续可以怎么扩展基础配置跑通只是第一步后面还有几个明显的扩展方向。如果单库写入压力还在涨可以把 2 库 2 表扩容到 4 库 4 表。ShardingSphere 的 INLINE 算法扩容时要小心order_id % N这种取模算法在 N 变化后已有数据要迁移重新分布代价很大。所以如果在数据量还小的时候建议用 HASH_MOD 或者一致性哈希算法为将来的扩容留好余地。分片键的选择也可以更细。目前我只有单一分片键 order_id但如果业务上需要按范围查订单比如查最近 30 天订单单靠 order_id 做分片是没法避免全路由的。可以考虑复合分片策略比如按 create_time 做时间维度分片配合 order_id 做哈希分片这样时间范围查询也能精准路由到少数几个分片。最后是监控告警。接入分库分表之后数据库实例变多了单靠人工盯 MySQL 监控不够。我给每个物理库都配了独立的监控大盘慢查询、连接数、主从延迟分库展示至少能保证任何一个分片出问题时能快速发现。这一步看似不属于基础配置但长期运维的经验告诉我越早做越好。整套配置跑通下来我的感受是ShardingSphere-jdbc 的基础配置本身不难难的是理解它在路由、归并、事务这几个层面带来的思维转变。分片键设计得好不好直接决定了未来一年你要不要通宵导数据绑定表和广播表配不配直接决定了 JOIN 查询是毫秒级还是秒级。希望这篇实战记录能帮你少走几步弯路尤其是 INLINE 表达式和 worker-id 这两个坑提前避开能省很多事。