ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ShardingSphere-jdbc 分库分表实战:从依赖配置到核心改造

ShardingSphere-jdbc 分库分表实战:从依赖配置到核心改造 如果你的项目里单表数据量已经跑到千万级甚至亿级写入并发一上来数据库的CPU和IO就开始报警了那么分库分表这件事迟早要面对。我目前在生产的核心订单链路用的就是 ShardingSphere-jdbc 5.5.0 Spring Boot 这套组合从单库单表平滑切到分库分表整体改动量和风险控制都还算理想。这篇就专门写给那些正准备上手 ShardingSphere-jdbc 的 Java 后端同学把从依赖引入、配置编写到核心代码改造的完整链路走一遍并且把我实际踩过的坑也一并交代清楚。如果你是第一次接触 ShardingSphere-jdbc也没关系这篇文章不假设你有任何分库分表基础只要求你熟悉 Spring Boot 和 MyBatis 的基本使用。我会把逻辑表、真实表、分片算法、分布式主键这些容易绕晕的概念用最直白的方式讲明白。看完之后你应该能独立完成一套可运行的基础分库分表配置并能根据业务需求扩展出读写分离、强制路由、绑定表等高阶玩法。1. 分库分表整体思路与版本选型1.1 为什么是 ShardingSphere-jdbc 而不是 ShardingSphere-proxy先解决一个最常见的疑问同样是 ShardingSphere 生态有 jdbc 和 proxy 两种形态到底选哪个。它们俩的定位差异非常本质搞清楚了你后续的架构方向就不会跑偏。ShardingSphere-jdbc 是客户端模式本质是一个增强版的 JDBC 驱动。你的应用直接连它它内部帮你管理多个真实的数据库连接然后把你在代码里写的 SQL 做解析、改写、路由、归并最后把结果返回给你。从应用视角看它就是一个普通数据源。好处是性能损耗极低因为不需要额外的网络跳转而且可以拿到应用线程上下文做 hint 强制路由之类的精细化控制。ShardingSphere-proxy 是服务端模式它独立部署成一个代理服务你的应用连的是 proxyproxy 再连真实的数据库。好处是异构语言友好不用改应用代码但多一跳网络延迟会高一些而且在复杂查询的归并能力上jdbc 模式依然更强一些。我的建议很直接如果团队技术栈统一是 Java且对性能敏感无脑选 jdbc 模式。这篇文章所有配置也是基于 ShardingSphere-jdbc 来写的。另外补充一句ShardingSphere-jdbc 5.x 的版本号演进很快5.5.0 是目前比较稳定的一个版本修复了不少之前的 NPE 和配置兼容问题锁这个版本没毛病。1.2 核心概念扫盲逻辑表、真实表、数据节点新手第一次看 ShardingSphere 的配置十有八九被一堆“表名”搞晕。这里我用订单表来举例一次性讲透。真实表actual table数据库里真实存在的表比如t_order_0、t_order_1、t_order_2、t_order_3这就是你在 MySQL 里实际建的表。逻辑表logic table你在代码里操作的表名比如t_order。你的 SQL 只写select * from t_order where order_id 1至于它实际落到哪张真实表由 ShardingSphere 根据分片算法算出来。数据节点data node真实表的位置描述通常写成db$-{0..1}.t_order_$-{0..3}这个表达式表示两个库db0、db1每个库 4 张分片表一共 8 个数据节点。理解这三者的关系是配置分片规则的第一步。逻辑表是你的业务视角真实表是存储视角数据节点是映射关系。配置的核心就是告诉 ShardingSphere逻辑表t_order对应哪些真实表以及用哪一列、按什么算法算出路由目标。1.3 选用标准分片策略还是自定义策略ShardingSphere 5.x 内置了多种分片算法其中最常用的是HASH_MOD哈希取模和INLINEGroovy 表达式另外还有MOD、RANGE_MOD、COMPLEX_INLINE、HINT_INLINE等。实际项目里大部分场景用HASH_MOD就够了它会把分片键的哈希值对分片总数取模分布相对均匀。不过我建议你在正式上生产前一定要考虑“分片键选择”和“数据增长”这两个问题。比如你用order_id做分片键那所有查询最好都带上order_id否则就走全路由性能大打折扣。数据量增长后初始分片数是 8 个想扩到 16 个HASH_MOD会面临大规模数据迁移。这个时候可以在设计初期就预估三年的数据量把分片数一次性定得大一点比如 64 或 128避免中途扩容。基础配置阶段用HASH_MOD完全没问题但脑子里要绷着这根弦。2. 环境准备与依赖引入2.1 软件版本对照与兼容性说明先说环境版本这地方踩坑的人特别多。ShardingSphere-jdbc 5.5.0 对 Spring Boot 的版本兼容性是没有问题的Spring Boot 2.7.x 和 3.x 都能用不过两者引入的依赖坐标不同下面会细说。数据库建议 MySQL 5.7 及以上驱动用mysql-connector-j8.0.33 或更高版本。JDK 至少 8如果是 Spring Boot 3.x那 JDK 要求 17。这里必须提醒一个老坑网上大量教程还在教com.mysql.jdbc.Driver这个类在 MySQL Connector/J 8.x 里已经废弃了正确写法是com.mysql.cj.jdbc.Driver。配置错了项目启动就会报ClassNotFoundException而且报错信息不够直观很容易让人怀疑是 ShardingSphere 的锅实际上是驱动类名的问题。我的测试环境如下建议你直接用这个组合省心组件版本JDK1.8 或 17Spring Boot2.7.18 或 3.2.xShardingSphere-jdbc5.5.0mysql-connector-j8.0.33MyBatis-Plus3.5.5可选HikariCPSpring Boot 内置2.2 Maven 依赖坐标的坑与正确姿势ShardingSphere-jdbc 5.5.0 的 Maven 坐标有变化如果你的项目是 Spring Boot 3.x那依赖要用shardingsphere-jdbc-spring-boot-starter4.x 时代的shardingsphere-jdbc-core-spring-boot-starter在 5.x 已经不存在了。dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-spring-boot-starter/artifactId version5.5.0/version /dependency如果你用的是 Spring Boot 2.x官方文档建议使用shardingsphere-jdbc-spring-boot-starter搭配spring-boot-starter-jdbc。我的实际经验是直接引入上面这个坐标然后确认项目中已经有spring-boot-starter-jdbc或mybatis-spring-boot-starter即可一般不会冲突。另外要说一个细节ShardingSphere-jdbc 5.5.0 会和 Druid 连接池打架。如果你以前的项目用了 Druid引入 ShardingSphere 后启动时会报数据源类型错误。建议要么改用 HikariCPSpring Boot 默认要么在配置里显式指定type: com.zaxxer.hikari.HikariDataSource。这块我踩过一次后续都在配置里统一写了 Hikari再没出过问题。2.3 前置准备两个库 8 张表的建表脚本为了演示效果我准备了一个经典的订单场景两个数据库db0和db1每个库各 4 张订单分片表另加一个广播表t_dict用于字典数据同步。先创建数据库CREATE DATABASE IF NOT EXISTS db0 DEFAULT CHARSET utf8mb4; CREATE DATABASE IF NOT EXISTS db1 DEFAULT CHARSET utf8mb4;每个库中创建订单表注意真实表名是t_order_0到t_order_3CREATE TABLE IF NOT EXISTS db0.t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_amount DECIMAL(10,2) DEFAULT NULL, status INT DEFAULT NULL, create_time DATETIME DEFAULT NULL, PRIMARY KEY (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三张表的建表语句除了表名不同结构完全一致。db1库中也执行同样的四张表。另外每张表都要记得加上普通索引尤其是按user_id查询的索引否则分片后查询效率会很难看。分库分表只能通过分片键做到局部路由真正落到单表后的查询优化还是得依靠索引。广播表t_dict也需要在两个库中都创建CREATE TABLE IF NOT EXISTS db0.t_dict ( dict_id BIGINT NOT NULL, dict_type VARCHAR(32) DEFAULT NULL, dict_value VARCHAR(128) DEFAULT NULL, PRIMARY KEY (dict_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3. 基础配置实战从单库到分库分表3.1 数据源配置多数据源的定义方式先看 Spring Boot 的application.yml完整配置。注意一旦使用 ShardingSphere-jdbc你的数据源就不再是直接配置在 Spring 容器里的单个数据源了而是全部收编到spring.shardingsphere.datasource下面。原有的spring.datasource.url配置要删掉或注释掉否则会出现数据源重复初始化的问题。spring: shardingsphere: datasource: names: db0, db1 db0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db0?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root123 max-pool-size: 20 min-pool-size: 5 db1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db1?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root123 max-pool-size: 20 min-pool-size: 5 rules: sharding: # ... 分片规则下面详解 props: sql-show: true这里的jdbc-url不是url我自己第一次写的时候写成url启动直接报错。另外max-pool-size和min-pool-size是 HikariCP 的参数建议每个库的连接数不要设得太大因为分库后连接数是乘以库数量的2 个库就是双倍连接。如果你的服务实例很多连接数控制不好数据库会被打满连接。生产上单库 20 个连接足够没必要追求大池子。3.2 分片规则配置分片算法、分片策略与绑定表现在到最关键的部分。分片规则配置里同时包含了分片算法、分片策略、绑定表、广播表等信息。我把配置写全然后逐一拆解。spring: shardingsphere: datasource: names: db0, db1 # ... 上面已给出 rules: sharding: tables: t_order: actual-data-nodes: db$-{0..1}.t_order_$-{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake t_order_item: actual-data-nodes: db$-{0..1}.t_order_item_$-{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake binding-tables: - t_order, t_order_item broadcast-tables: - t_dict sharding-algorithms: t_order_hash_mod: type: HASH_MOD props: sharding-count: 8 t_user_id_mod: type: HASH_MOD props: sharding-count: 8 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true逐个解释。actual-data-nodes定义了逻辑表映射到哪些真实表db$-{0..1}是db0、db1的简写t_order_$-{0..3}是四张真实表。这个表达式看起来像正则实际是 Groovy 模板语法注意中间不要随便加空格否则解析直接失败。table-strategy里定义的standard策略表示标准分片用单分片键。sharding-column: order_id指定分片键sharding-algorithm-name指向下方自定义的算法t_order_hash_mod。算法类型用HASH_MOD配置sharding-count: 8也就是把所有数据均匀分布到 8 张分片表中。注意这里的分片算法是按全量 8 张表来取模的那怎么确定数据落到哪个库呢其实 ShardingSphere 的默认数据节点路由不区分库8 个数据节点就是 8 张表。如果你想按user_id再做一次库路由比如db$-{user_id % 2}那就需要自定义多分片键或复合分片算法。基础配置阶段用 8 个数据节点的HASH_MOD已经足够跑通全流程但真实业务往往会有更复杂的路由需求比如先按用户维度分库再按订单维度分表。binding-tables非常关键一定要配置。绑定表是指分片规则完全一致的一组表例如t_order和t_order_item都按order_id分片那么它们 join 查询时ShardingSphere 可以保证关联数据落在同一个数据节点上避免笛卡尔积跨库 join性能差别巨大。不配置绑定表join 查询会把 8 张订单表与 8 张订单明细表做全组合关联SQL 会被改写得极其恐怖。broadcast-tables是广播表通常放字典表、配置表这类小表。广播表会在每个库中都保留一份完整数据写入时同步到所有库查询时随机路由到任意一个库。3.3 读写分离场景下的数据源配置追加如果你的库已经做了主从复制想在分库分表的基础上叠加读写分离配置也不复杂。核心点在于先定义一个逻辑数据源比如ds_0这个数据源包含写库和读库然后分片规则里的数据节点引用这个逻辑数据源。spring: shardingsphere: datasource: names: db0, db0_read, db1, db1_read db0: type: com.zaxxer.hikari.HikariDataSource # ... db0_read: type: com.zaxxer.hikari.HikariDataSource # ... # db1、db1_read 同理 rules: readwrite-splitting: >TableName(t_order) public class Order { TableId(type IdType.INPUT) private Long orderId; private Long userId; private BigDecimal orderAmount; private Integer status; private LocalDateTime createTime; }注意两个细节。第一TableName里必须写逻辑表名t_order不能写真实表名t_order_0。第二主键类型要用IdType.INPUT因为主键由 ShardingSphere 的分布式主键生成器来生成如果还让它走 MyBatis-Plus 的默认自增策略会与 ShardingSphere 的主键生成逻辑冲突。我在 MyBatis-Plus 3.5.x 下实测如果设置成ASSIGN_ID虽然 MyBatis-Plus 会生成雪花 ID但 ShardingSphere 也会尝试生成结果就是主键列被覆盖或插入报错。Mapper 接口写法不用变Mapper public interface OrderMapper extends BaseMapperOrder { ListOrder selectByUserId(Param(userId) Long userId); }XML 里的 SQL 同样只写逻辑表名select idselectByUserId resultTypecom.example.demo.entity.Order select * from t_order where user_id #{userId} /select这里必须提醒如果你在 XML 里写了t_order_0这样的真实表名ShardingSphere 照样会解析和改写但路由规则会变成“指定表路由”也就是说它不再根据分片键计算而是直接路由到你写死的那张表。这在某些特殊场景是故意为之但在业务开发中大概率是个 bug因为一旦真实表名写错数据就查不到或者插错了。4.2 分布式主键的配置与踩坑记录分库分表之后数据库自增主键彻底失效因为每个库的自增 ID 会重复。ShardingSphere 默认提供雪花算法Snowflake作为分布式主键生成器配置方式上面已经给出了。雪花算法生成的 ID 是一个 64 位 Long 型整数由时间戳、机器 ID、序列号组成。它在基础配置阶段使用起来很简单但有两个隐患必须提前知道。第一个隐患是时钟回拨。如果部署应用的机器出现 NTP 时间回拨雪花算法可能生成重复 ID。ShardingSphere 5.5.0 对时钟回拨有一定处理但并不能保证 100% 安全。生产环境建议对应用服务器做时间同步配置避免时间跳跃。第二个隐患是前端精度丢失。雪花 ID 是 19 位 LongJavaScript 的 Number 类型只能安全表示 2^53 以内的整数19 位 ID 传给前端会丢失精度。解决方案有两种一种是在后端序列化时转成 String这个在 Jackson 里配置一下就行另一种是干脆不用雪花算法改用UUID或自定义的号段模式。我用的是雪花 ID 转字符串的方案侵入性最小。4.3 写入数据的完整调用链路为了让新手对整体流程有个直观感受我给一个完整的 Service 层写入例子Service public class OrderService { Resource private OrderMapper orderMapper; public void createOrder(Order order) { order.setOrderId(null); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); } }如果orderId为 nullShardingSphere 会调用配置好的snowflake生成器生成主键。如果orderId不为 null则直接用传入值作为主键。这里有一个容易被忽略的细节分片键order_id如果是传入的那么 ShardingSphere 不会对该值做合法性校验它只负责拿这个值做哈希取模路由。所以你的代码里必须保证orderId有值且全局唯一否则插入后会出现主键冲突或者路由到错误的表。写入完成后可以用sql-show日志确认实际插入到了哪张表。我在本地测试时经常看到类似这样的输出Actual SQL: db1 ::: insert into t_order_3 (order_id, user_id, order_amount, status, create_time) values (7834019283741016064, 1001, 199.00, 0, 2025-01-01 12:00:00)如果日志显示的表名和数据量级符合预期说明整条链路已经跑通了。接下来就可以开始做各种复杂查询的验证了。4.4 绑定表 join 查询的代码示例基础功能跑通后很多人第一个面临的需求就是订单表和订单明细表的 join 查询。如果没有配置绑定表这个查询会被改写成 8 张订单表 × 8 张订单明细表的全排列SQL 膨胀到几十行执行效率惨不忍睹。配置了绑定表之后代码完全不用特殊处理Mapper public interface OrderItemMapper extends BaseMapperOrderItem { // 继承 BaseMapper 即可 }写 XML 时照常 joinselect idselectOrderWithItem resultTypemap select o.order_id, o.user_id, i.item_name from t_order o inner join t_order_item i on o.order_id i.order_id where o.order_id #{orderId} /select关键点在于join 的关联字段必须是分片键。这样 ShardingSphere 才能根据order_id把两个逻辑表映射到同一个真实数据节点上join 操作在单库内完成性能最优。如果你的 join 字段不是分片键绑定表配置就形同虚设跨库 join 的问题会重新暴露出来。这个设计约束最好在表结构设计阶段就想清楚。5. 常见问题与排查技巧实录5.1 启动报错Data source is not supported这是一个出现频率极高的报错。启动时类似Caused by: org.apache.shardingsphere.infra.exception.core.external.sql.type.generic.UnsupportedSQLOperationException: Data source is not supported我见到这个报错通常会按两步排查。第一步看配置里type字段是否写的是com.zaxxer.hikari.HikariDataSource。如果是druid且项目里没有引入 Druid 依赖或者类型名写错就会报不支持。第二步确认是否同时保留了原生的spring.datasource.url配置。ShardingSphere-jdbc 启动时会尝试接管数据源如果检测到多个数据源定义就会发生冲突。把原生数据源配置注释掉只保留spring.shardingsphere.datasource下的定义重启一般就正常了。5.2 数据查不到或写错库的排查思路这类问题最隐蔽症状是应用不报错但数据没有出现在你预期的表里。排查思路按以下顺序来确认sql-show日志里的 Actual SQL 走的是哪张表。核对actual-data-nodes里的库名、表名是否与实际数据库一致尤其注意db$-{0..1}这个写法括号里是从 0 开始的下标不是库名前缀。确认分片键的值类型。order_id如果是 String 类型而配置的分片算法是HASH_MOD那么取模的哈希值是基于字符串的可能与数值类型的预期不一致导致分布不符合直觉。解决方案是让分片键类型统一或者在算法层做类型转换。检查是否存在广播表污染。如果t_dict这类表没有出现在广播表配置里它会被当成普通逻辑表来处理写入时只会路由到一张真实表其他库查不到数据。5.3 SQL 报错Table not found 与逻辑表名有时候 SQL 里明明写的表名没有问题但 ShardingSphere 却报 Table not found。这种情况多半是因为你在 SQL 里使用了数据库名前缀比如select * from db0.t_order_0 where order_id 1一旦 SQL 里带了db0和真实表名t_order_0ShardingSphere 会认为你已经指定了物理库表不会再做逻辑路由。但如果你拿这个 SQL 去查db1库里的数据自然就查不到甚至直接报表不存在。解决办法是业务 SQL 永远只写逻辑表名不加库名前缀把路由的事完全交给 ShardingSphere。5.4 分片键缺失导致的全路由查询性能瓶颈最后聊一个性能问题。当你的查询条件里没有分片键时比如select * from t_order where user_id 1001ShardingSphere 无法根据分片键定位到具体数据节点只能把这条 SQL 广播到所有 8 张真实表上执行再把结果合并返回。这在数据量小的时候感觉不出来表一多、数据一多性能会急剧下降。优化手段一般是让按照用户维度查询的场景直接以user_id作为分片键重新设计分片策略或者引入user_id - order_id的映射表先查到order_id列表再走分片键查询。基础配置阶段不用过于追求极致的查询性能但在建表设计时一定要意识到分片键选的越贴近核心查询维度后续的 SQL 就越容易写、越容易优化。5.5 旧资料与 5.x 版本的配置差异提醒现在的网上的教程鱼龙混杂很多还是 4.x 的写法。4.x 时代的分片配置前缀是spring.shardingsphere.rules.sharding5.x 已经移除了rules之前的错误前缀正确的路径是直接在spring.shardingsphere.rules.sharding下配置别被旧文章带偏了。此外5.x 的分片算法类型名也变了比如inline改成了INLINEhash_mod改成了HASH_MOD大小写不敏感但最好按官方文档来。还有一个差异是 5.x 版本用sharding-algorithms来配置算法4.x 里是写在props里的结构完全不同。遇到配置不生效时先去官方文档确认一下当前版本的 YAML 结构网上随便找的配置大概率已经过时。写在最后说实话ShardingSphere-jdbc 这套东西边界情况非常多光靠配置一次就跑通是不现实的。我自己在生产上踩过最狠的一个坑是绑定表配置漏了导致 join 查询爆炸排查了整整一个下午。所以如果你刚开始接触我的建议是先不要直接在你的核心业务表上动刀找一张低频的表按照这篇文章从头到尾跑一遍把路由原理和配置结构摸清楚再逐步扩大到核心表。另一个建议是分库分表属于一旦上线就很难回退的架构改造上线之前一定要想清楚容量规划、数据迁移方案和灰度方案千万不要抱着“先上线再优化”的心态。希望这篇实战记录能帮你少走一些弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进