多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

ShardingSphere-JDBC 5.5.0 + Spring Boot 3 分库分表配置实战与避坑指南

ShardingSphere-JDBC 5.5.0 + Spring Boot 3 分库分表配置实战与避坑指南 1. 为什么是 ShardingSphere-JDBC单表瓶颈与选型逻辑1.1 单库单表在什么阶段真的需要分片先说结论不是所有项目第一天上 ShardingSphere-JDBC我这边是在单表数据量接近千万、业务侧频繁出现慢查询之后才动手做的迁移。这段时间最典型的几个信号是连接数常年告警、某些范围查询即便走索引也要几百毫秒、大促期间主库 IO 明显打满、单表索引再怎么加都救不回来。这时候你再继续把资源砸在单库单表这条路上收益已经很低。很多人会误以为分库分表是数据量到千万才做的事其实更准确的判断标准是数据库成为性能瓶颈的时刻。如果你的业务只差几个慢查询先优化索引、做读写分离、加缓存就能撑很长时间。我自己判断是否需要分片核心看三条单表数据量增速是否不可控、单库连接是否持续逼近上限、是否有某些业务必须全表扫描且逻辑上无法避免。我这次做的是订单模块订单表天然适合分片因为 buyer_id用户 ID是很稳定的分片键按用户维度切分之后同一个用户的所有订单都会落到同一个库的同一张分表上后续 join 明细表也方便。1.2 JDBC 模式、Proxy 模式我为什么选了 JDBCShardingSphere 有两个主要形态ShardingSphere-JDBC 是轻量级的 JDBC 驱动模式在应用进程内完成分片路由ShardingSphere-Proxy 则是一个独立部署的中间件服务对外模拟 MySQL 协议业务方通过普通连接方式访问。我用一个表格把两者的核心差异列出来这也是我当时做决策的最主要依据对比项ShardingSphere-JDBCShardingSphere-Proxy部署形态打进应用进程无独立中间件节点独立部署服务业务方网络连接应用侵入低只替换数据源和驱动非常低连接方式都不用改性能开销应用内做 SQL 解析与路由无网络跳转多一层网络转发压测下有一定损耗多语言支持仅 Java 生态更顺滑语言无关任何 MySQL 客户端都可以连运维成本无额外组件升级跟随应用需要单独维护 Proxy 集群与配置中心我当时选择 JDBC 模式的核心原因有两个一是我们整个技术栈就是 Java Spring BootJDBC 模式不需要额外部署一套 Proxy运维上省事很多二是 JDBC 模式的路由在应用内存里完成少了中间层网络开销对订单这种写多读多的场景更友好。顺便说下网上有些人拿 ShardingSphere 和 MyCat 对比早期的 MyCat 在纯分片场景确实很常见但 SQL 兼容性、权限支持和社区活跃度这些年明显不如 ShardingSphere。如果不是已经在 MyCat 体系里重度投入新项目从 ShardingSphere 起步是更稳妥的选择。1.3 5.x 相对 4.x 的配置变化决定你照着谁抄这次做基础配置之前我在网上搜到大量 ShardingSphere 4.x 的教程照着抄基本都会翻车。5.x 之后配置体系有一个非常明顯的变化配置前缀和规则层级完全不一样。4.x 是这么写的spring: shardingsphere: sharding: tables: t_order: actual-data-nodes: ds0.t_order_05.x 把tables放进了rules.sharding.tables下面spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1}这种变化直接导致你在 4.x 时代的百度结果里复制过来的配置在 5.5.0 下大概率启动失败报错信息又长又难懂。所以这篇实战笔记里所有配置我都是按 5.x 的规则结构写的你如果之前只接触过 4.x请先忘掉旧结构以这里的配置为准。2. 5.5.0 Spring Boot 3 的依赖落地与工程初始化2.1 版本基线JDK 17 Spring Boot 3.xShardingSphere-JDBC 5.5.0 对应的 Java 生态基线比较明确JDK 至少要 17Spring Boot 建议使用 3.2 或更高版本。我本地环境是 JDK 17 Spring Boot 3.3.x MySQL 8整个组合跑下来没有版本层面的兼容问题。如果你还在用 Spring Boot 2.x那么不需要硬上 5.5.0它对应的是 4.x 到 5.x 过渡期的老 API。Spring Boot 3 下使用 ShardingSphere-JDBC 的 starter核心依赖就一个dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-spring-boot-starter/artifactId version5.5.0/version /dependency这里有个非常坑的点网上很多文章还在用shardingsphere-jdbc-core-spring-boot-starter这个坐标它是 5.1.x 及更早版本里的命名。5.5.0 时代官方推荐的坐标是shardingsphere-jdbc-spring-boot-starter如果你按老坐标引Maven 可能给你解析到一个版本依赖完全不一致的包启动时整出各种莫名其妙的NoClassDefFoundError。所以依赖这块建议直接去官方 Maven 仓库搜 5.5.0 对应 artifacts别盲抄旧博客。2.2 Spring Boot 自动装配的影响引入 starter 后ShardingSphere 会自动接管 Spring 的数据源装配。它会把你在spring.shardingsphere.datasource下定义的多个物理数据源包装成一个逻辑数据源然后注入到 Spring 容器中。对你来说原来Autowired一个DataSource的地方不用改MyBatis、JdbcTemplate、Spring Data JPA 这些底层框架拿到的就是 ShardingSphere 的逻辑数据源业务代码对分片完全无感知。这里要提醒一点项目里不要同时配置spring.datasource.*和spring.shardingsphere.datasource.*。ShardingSphere 的 starter 会创建一个优先级很高的逻辑数据源 Bean如果业务里还有其他独立数据源配置Spring 容器里会出现多个 DataSource事务管理器或者某些自动装配逻辑会拿错连接轻则日志一堆警告重则直接路由失败。我进这个坑的时候表象是某个自定义 Mapper 突然连不上库了排查了半天才发现是配置重叠导致的 Bean 冲突。2.3 物理库表初始化先手动建好两个库和四张表ShardingSphere-JDBC 只是一个数据访问层的分片中间件它不会在 MySQL 里自动建库建表。分片规则里的actual-data-nodes只负责描述逻辑表 t_order 对应哪些真实表这些真实表必须预先存在否则第一次路由过去就会报Table xxx.t_order_0 doesnt exist。我这次的演示场景是两个物理库、每库两张订单分表CREATE DATABASE IF NOT EXISTS sharding_demo_ds0 DEFAULT CHARSET utf8mb4; CREATE DATABASE IF NOT EXISTS sharding_demo_ds1 DEFAULT CHARSET utf8mb4; CREATE TABLE IF NOT EXISTS sharding_demo_ds0.t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_amount DECIMAL(10,2), order_status TINYINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- sharding_demo_ds0.t_order_1、sharding_demo_ds1.t_order_0、sharding_demo_ds1.t_order_1 -- 建表语句与上面一致只是库名和表名不同。注意主键我用的是BIGINT因为后面配置分布式 ID 时默认用雪花算法生成的 ID 是 64 位长整型如果用 INT 会直接溢出。DDL 这块在启动验证前搞定不要等到代码跑起来才想起来初始化。3. 配置逐字段拆解数据源、分片规则与分布式 ID3.1 datasource 配置Hikari 连接池与 jdbc-urlShardingSphere 5.5.0 在 Spring Boot 里的配置入口是spring.shardingsphere.datasource。多个物理库用names声明然后每个库独立配置。下面是完整 YAML我逐个字段注释了用途spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sharding_demo_ds0?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 maximum-pool-size: 20 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sharding_demo_ds1?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 maximum-pool-size: 20有两个细节值得说明。第一type这里我直接用了 HikariCP因为 Spring Boot 3 默认连接池就是 Hikari没必要换其他类型。第二注意是jdbc-url而不是url。ShardingSphere 的 datasource 定义不是 Spring Boot 原生的spring.datasource配置它要求每个数据源实例有自己的连接池属性字段用jdbc-url才对。有些博客写url也能跑那是运气好严格模式下绑定会失败。3.2 数据节点表达式$-{0..1}到底怎么读分片规则中最容易让新手犯迷糊的是actual-data-nodes ds$-{0..1}.t_order_$-{0..1}。这个表达式拆开看并不复杂。ds和t_order_是字面前缀中间的$-{...}是 ShardingSphere 的行内表达式语法用来生成 0 到 1 的枚举范围。所以ds$-{0..1}会被展开成ds0和ds1t_order_$-{0..1}展开成t_order_0和t_order_1两个范围组合起来就是四张真实表ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1。这里有个 YAML 解析层面的坑很容易踩$-{0..1}里的花括号在 YAML 里会被某些解析器当作 flow mapping 的开始导致整个配置绑定失败。解决办法很简单——把这个值用单引号包裹起来actual-data-nodes: ds$-{0..1}.t_order_$-{0..1}我第一次没加引号启动时 Spring Boot 直接报Failed to bind properties一长串花了半小时才定位到是表达式没加引号的问题。3.3 分片策略与分片算法库策略和表策略必须分开配5.x 的分片规则把策略和算法分层设计。策略描述按哪个列分片用哪个算法算法描述具体怎么计算路由目标。以订单表为例我的设计是库策略按user_id取模 2决定落到 ds0 还是 ds1表策略按order_id取模 2决定落到 t_order_0 还是 t_order_1对应配置如下rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: database_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline sharding-algorithms: database_inline: type: INLINE props: algorithm-expression: ds$-{user_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 2}为什么库和表要分开配置因为分片键可能不一样。订单表按order_id分表天经地义但是同一个买家的订单如果散落在多个库后续按买家维度做跨库查询就非常麻烦所以我额外用user_id做库路由让同一个用户的订单尽量落在同一个库里这样后续订单详情、订单明细 join 都不会跨库。INLINE算法是 5.x 最常用的分片算法本质就是一个 Groovy 表达式求值。表达式里%是取模运算计算结果是 0 或 1刚好对应 ds0/ds1、t_order_0/t_order_1。这里同样要加单引号ds$-{user_id % 2}里的花括号和空格在无引号状态下可能被 YAML 误解析。还需要特别提一下INLINE 定位是单值路由算法适合等值查询。如果你业务里大量使用BETWEEN、IN这种范围查询INLINE 的应对能力比较有限可能出现退化为全节点扫描的情况。正式设计分片规则时建议针对范围查询业务单独配置标准算法或者自定义算法基础配置阶段先知道这个边界就行。3.4 key-generate-strategy雪花算法与自动填充分表之后order_id作为表分片键必须保证全局唯一不能依赖数据库自增主键因为两个库两张表各自自增必然冲突。ShardingSphere 的解决方案是为逻辑表配置分布式 ID 生成器默认提供雪花算法SNOWFLAKE。配置如下tables: t_order: key-generate-strategy: column: order_id key-generator-name: snowflake key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1这里有一个很多人第一次用会困惑的点插入数据时业务代码里到底要不要给orderId赋值我的经验是不需要而且尽量不要手动赋值。ShardingSphere 在执行 INSERT 时发现 SQL 列清单里没有order_id但逻辑表配置了key-generate-strategy它会在 SQL 解析和改写阶段自动生成雪花 ID把这个值填充进 INSERT 语句然后用这个生成的 ID 参与表路由。你在 Java 代码里把orderId设成 null配合 MyBatis 的useGeneratedKeys插入完成后还能把生成的主键回填到实体上整体链路是通的。不过要注意雪花 ID 是大整数如果业务上还有地方依赖顺序递增或者需要短 ID雪花 ID 不一定适合那就需要自己实现一个自定义 ID 生成器或者用 UUID 但把分片键改成其他字段。基础配置先别整太复杂雪花 ID 够用。3.5 广播表配置字典表这类数据怎么处理分片之后还有个很容易忽略的场景字典表、配置表这种数据量很小、几乎不会增长、但几乎所有业务表都要关联查询的表怎么放方案是广播表。把t_dict配置到广播表里ShardingSphere 会让这张表的完整数据在 ds0 和 ds1 两个库各存一份写入时同步写所有库查询时从当前路由到的库直接读避免跨库 joinbroadcast-tables: - t_dict基础配置阶段我建议凡是那种哪里都要查、数据量很小、更新不频繁的表都考虑设置成广播表。否则一旦业务开始在分片表上 join 字典表而字典表没同步到所有库你会在某个分片库上看到报错Table t_dict doesnt exist。3.6 绑定表配置防笛卡尔积的订单与订单明细如果只是单表分片绑定表可以不着急配。但订单模块往往还有订单明细表t_order_item它跟订单表通常是一对多关系而且按同样的规则分片。如果不配置绑定表ShardingSphere 对两张分片表做 join 时会默认做笛卡尔积路由比如查询语句可能同时去查询ds0.t_order_0joinds0.t_order_item_1这种明明不存在关联数据的组合结果慢且错。配置绑定表让两张表按同样的分片键保持一致的路由结果binding-tables: - t_order, t_order_item注意表名之间的逗号配置项是一个字符串数组但这里实际上是逗号分隔的列表写法。绑定表要求两张表的分片键类型、分片算法完全一致否则路由结果对不齐配置了也起不到防笛卡尔积的效果。4. 跑通一条真实 SQL启动、插入、查询的路由验证4.1 开启 sql-show确认启动过程没有报错配置全部写完后第一步不是写业务代码而是把 ShardingSphere 的 SQL 执行日志打开方便看到路由过程props: sql-show: true加完这个配置后启动 Spring Boot如果配置没问题启动日志里能看到 ShardingSphere 的数据源初始化、分片规则加载、广播表注册这些信息。但我要提醒你启动成功不代表两个物理库都能连上。HikariCP 默认是懒加载连接ShardingSphere 启动时只是定义连接池并不会立刻发起网络连接所以第一个 SQL 执行时可能才暴露库连不上、表不存在的问题。所以我的验证习惯是启动通过后先写一个最简单的接口强制它走一次 SQL确认链路真正是通的再继续后面的开发。4.2 INSERT 数据观察 order_id 自动生成与路由这个阶段我用 MyBatis 做了一个最简单的 OrderMapper测试方法里循环插入 10 条订单user_id 从 1 到 10SpringBootTest public class OrderMapperTest { Autowired private OrderMapper orderMapper; Test void testInsert() { for (long userId 1; userId 10; userId) { Order order new Order(); order.setUserId(userId); order.setOrderAmount(new BigDecimal(99.50)); order.setOrderStatus(1); // orderId 不手动设置留给 ShardingSphere 生成 orderMapper.insert(order); } } }SQL 日志开启后插入时能看到类似下面的输出Logic SQL: insert into t_order (user_id, order_amount, order_status) values (?, ?, ?) Actual SQL: ds1 ::: insert into t_order_1 (user_id, order_amount, order_status, order_id) values (?, ?, ?, ?) ::: [3, 99.50, 1, 8901234567...]这行日志是最直观的验证Logic SQL是你业务里写的逻辑表 SQLActual SQL是 ShardingSphere 改写后真正在物理库执行的 SQL它自动追加了order_id列并填入了雪花 ID。同时ds1这个前缀说明库路由生效user_id 为 3 的订单被路由到了 ds1再配合t_order_1说明表路由也生效了。如果你发现所有 INSERT 都只落在同一张表先检查分片表达式的取模逻辑和值域是否匹配比如user_id % 2的结果是 0 还是 1是否跟你预期的库编号一致。4.3 SELECT 查询逻辑表到真实表的归并查询验证我用的是 SQLTest void testSelectByUserId() { ListOrder orders orderMapper.selectByUserId(3L); orders.forEach(o - System.out.println(o.getOrderId())); }对应日志Logic SQL: select * from t_order where user_id ? Actual SQL: ds1 ::: select * from t_order_1 where user_id ? ::: [3] Actual SQL: ds1 ::: select * from t_order_0 where user_id ? ::: [3]这里有两个细节值得注意。第一按user_id查询时因为库策略已经路由到 ds1ShardingSphere 只需要在 ds1 的两张表里分别执行查询再归并结果不会去 ds0 白白查一遍。第二如果你没有指定分片键比如直接select * from t_orderShardingSphere 会把所有真实表全部查一遍日志里会出现四条 Actual SQL。这种全节点查询在数据量不大的演示环境没问题但生产环境要尽量避免无分片键的查询它是分片方案里最典型的性能杀手。验证完插入和查询基础配置这块就算真正跑通了。后面才是把项目往生产形态推进时容易踩进去的深坑。5. 真正容易踩的五个坑5.1 分片表不会自动建JPA 的 ddl-auto 也别指望前面说过 ShardingSphere 不负责建表但实际项目中经常有人漏掉初始化脚本。更隐蔽的问题是如果你用了 Spring Data JPA并把ddl-auto配成updateJPA 在启动时会尝试用逻辑表名去建表它根本不认识t_order_0、t_order_1更不会帮你把分表全部建出来结果就是启动时一堆建表语句报错。我的做法是分库分表环境的表结构一律用 Flyway 或者手工 SQL 管理每张真实分表都显式建好绝不依赖 ORM 的建表能力。这一点在团队协作时尤其重要新人入职如果不知道这个边界往往会在为什么我的表没建出来上卡半天。5.2 YAML 引号与旧版配置前缀的双重陷阱这个坑我在第三节里分散提过但值得单独汇总一下。一个是表达式必须加引号。我见过同事把actual-data-nodes、algorithm-expression的值原样裸写启动报mapping values are not allowed in this context几乎每次都会栽一次。另一个是 5.x 与 4.x 的结构前缀差异。网上搜到的大批老教程配置结构是spring.shardingsphere.sharding.tables而 5.x 是spring.shardingsphere.rules.sharding.tables。你把老配置贴进来很多字段直接失效ShardingSphere 可能安静地走默认配置也可能启动时就报绑定失败。判断一个教程是不是 5.x 的最快的方法就是看它有没有rules:这一层。没有这个层级直接劝退。5.3 跨库 join 不会自动发生设计分片键要想清楚ShardingSphere-JDBC 并不是万能的它支持在分片表之间做 join但有个大前提join 的表必须能路由到同一个数据源路由不到就报错或者产生笛卡尔积。我这次设计订单表时把库策略设为user_id取模就是为了保证同一个用户的订单表、订单明细表落在同一个库。如果你把库策略设为order_id表策略也设为order_id那么关联t_order和t_order_item时如果两者的 order_id 一致理论上是能路由到同一库的但一旦查询加了非分片键维度的关联条件路由就乱了。这是分片设计里最考验经验的部分。做基础配置时可以不去深究所有边界但心里要清楚先分片再关联永远要确认关联键和分片键能对得上。5.4 Transactional 不是分布式事务别拿它当跨库原子性保障ShardingSphere-JDBC 默认的事务模式是本地事务。什么叫本地事务就是每个物理数据源内部的事务。如果你的业务在一个事务里同时更新了 ds0 和 ds1 的数据Transactional 只能保证每个库内部各自的原子性两个库之间的写入是没有全局原子性的。A 库提交成功B 库提交失败不会自动回滚 A 库。5.x 要真正解决跨库事务方案是接入 Seata 或者采用 XA 分布式事务。基础配置阶段通常不会碰到这种场景但迟早会。我的建议是在设计阶段就避免单事务跨库把涉及多个库的写操作拆成独立事务通过消息表、本地消息表这类方式做最终一致性而不是指望一个 Transactional 包住天下。5.5 分页插件、逻辑删除与 ShardingSphere 一起用要验一下MyBatis-Plus 的分页插件PaginationInnerInterceptor跟 ShardingSphere 一起用多数情况下是兼容的但你不能默认它一定没问题。ShardingSphere 对分页查询的 LIMIT 处理有自己的归并逻辑而 MyBatis-Plus 的分页插件也会改写 SQL两者在复杂分页语句上偶尔会互相干扰出现分页数据不准、count 查询异常之类的现象。我的做法是涉及分片表的分页查询先关掉 MyBatis-Plus 的分页插件直接用 ShardingSphere 原生的 LIMIT 支持来跑等确认查询结果正确再决定要不要打开插件。同样逻辑删除字段是 MyBatis-Plus 的常见功能它在 SQL 改写阶段追加的deleted 0条件对 ShardingSphere 来说只是普通的 WHERE 条件一般没问题但插入和更新时如果涉及逻辑删除字段的默认值还是要实际执行一遍看看结果。我把这段时间实战下来最有感触的一点放在最后说吧。ShardingSphere-JDBC 5.5.0 的基础配置本质上就是三件事建好物理表、配好数据源、写对分片表达式。前两件事按部就班就能完成第三件事才真正决定你后续的业务能否平安跑起来。我自己的建议是第一次上手先不要追求复杂算法用最朴素的取模路由跑通一条插入、一条查询亲眼看到Actual SQL打印出正确的目标表和目标库再逐步引入广播表、绑定表、分布式事务这些高阶能力。基础链路通了后面所有问题都有清晰的排查起点基础链路不通任何高级配置都是在给下一次故障埋单。
返回列表