
简介本资源是一个基于SpringBoot、ShardingJDBC与MyBatis的轻量级分库分表实战项目面向Java后端中级开发者及分布式数据库学习者聚焦高并发场景下单库性能瓶颈的工程化解决方案。压缩包共15个文件61KB含5个核心Java类实现数据源配置、分片规则与业务逻辑、2个XML映射文件定义MyBatis SQL与结果映射、1个application.yaml集中管理Sharding策略与多数据源、1个SQL建表脚本、1个README.md说明文档及构建相关文件mvnw、pom.xml等结构精简开箱即用。已有188人学习下载适合快速理解ShardingJDBC哈希取模分片、多数据源透明路由、MyBatis与分片引擎协同工作的完整链路。读者可直接运行调试掌握分库分表配置要点、SQL执行日志分析方法及典型事务边界处理思路是入门分布式数据库中间件集成的高价值参考样本。1. SpringBoot ShardingSphere-JDBC MyBatis不是加个依赖就能分库分表的黑匣子你是不是也试过在pom.xml里一口气塞进spring-boot-starter-jdbc、shardingsphere-jdbc-core-spring-boot-starter和mybatis-spring-boot-starter配好application.yml里的数据源和分片规则一跑单元测试——查单表正常一执行INSERT INTO t_order (...) VALUES (...)就报No route to datasource或者更玄学的是本地mvn test全绿部署到测试环境后t_order_item的分片键order_id明明是long类型却总被路由到ds_1.t_order_item_0而不是按order_id % 4算出的t_order_item_2这不是配置漏了是 ShardingSphere-JDBC 在 SpringBoot 生命周期里悄悄“改口”了——它不认你写的Select(SELECT * FROM t_order)但认Select(SELECT * FROM t_order WHERE order_id #{orderId})里那个#{}绑定的参数类型。这份资源不是教你怎么抄配置而是把某跨平台系统里真实压测过 32 节点、日均 800 万订单的分片链路拆开给你看从ShardingSphereDataSource怎么接管SqlSessionFactory的DataSource到StandardShardingAlgorithm的doSharding方法里CollectionString availableTargetNames为什么必须包含所有逻辑表名再到 MyBatis 的ParameterHandler如何把Long值透传给分片算法。适合正在写分库分表方案、卡在「能连上库但路由错乱」、或准备用 ShardingSphere 替换旧分表中间件的 Java 后端工程师。2. 分片能力落地三要素数据源初始化、分片策略绑定、SQL 语法兼容性校验ShardingSphere-JDBC 不是代理层它是 JDBC Driver 的增强实现。这意味着它的分片逻辑必须在Connection创建前就完成注册且所有 SQL 解析都发生在Statement#execute()之前。很多翻车现场根源在于没理清 SpringBoot 自动装配和 ShardingSphere 初始化的时序冲突。2.1 数据源初始化别让DataSource成为「先有鸡还是先有蛋」的死循环SpringBoot 默认会自动装配DataSource而 ShardingSphere-JDBC 要求你提供一个ShardingSphereDataSource实例作为最终的DataSource。如果直接在Configuration里Bean一个ShardingSphereDataSource又在里面引用了DataSource比如想复用 HikariCP 配置就会触发循环依赖。正确做法是放弃spring.datasource.*开头的自动配置手动构造底层数据源再交由 ShardingSphere 封装。# application.yml —— 关键禁用原生 DataSource 自动配置并显式声明 ShardingSphere 配置 spring: autoconfigure: exclude: com.zaxxer.hikari.HikariConfig main: allow-bean-definition-overriding: true # ShardingSphere-JDBC 核心配置注意这里不写 spring.datasource props: sql-show: true max-connections-size-per-query: 1 check-table-metadata-enabled: false dataSources: ds_0: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_0?useSSLfalseserverTimezoneUTC username: root password: root ds_1: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_1?useSSLfalseserverTimezoneUTC username: root password: root rules: - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order_${0..3} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_table_inline databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: t_order_database_inline t_order_item: actualDataNodes: ds_${0..1}.t_order_item_${0..3} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_item_table_inline databaseStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_item_database_inline defaultDatabaseStrategy: none: defaultTableStrategy: none: shardingAlgorithms: t_order_table_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 4} t_order_database_inline: type: INLINE props: algorithm-expression: ds_${user_id % 2} t_order_item_table_inline: type: INLINE props: algorithm-expression: t_order_item_${order_id % 4} t_order_item_database_inline: type: INLINE props: algorithm-expression: ds_${order_id % 2}提示props.check-table-metadata-enabled: false是关键开关。ShardingSphere 启动时默认会查询information_schema获取表结构元数据但在分库场景下各物理库的表结构可能不完全一致比如某些库做了字段灰度开启此选项会导致启动失败。关掉它意味着你必须确保逻辑 SQL 中引用的所有列在目标物理表中真实存在。2.2 分片策略与 MyBatis 的耦合点#{}vs${}的生死线MyBatis 的#{}是预编译占位符$是字符串拼接。ShardingSphere-JDBC 的分片路由只识别#{}绑定的参数值对$拼接的字符串完全无感。这是最隐蔽的坑——你写SELECT * FROM t_order WHERE order_id ${orderId}ShardingSphere 看不到orderId的值只能走默认路由通常是ds_0导致查不到数据。验证方法很简单打开sql-show: true观察日志里打印的 SQL 是SELECT * FROM t_order WHERE order_id ?还是SELECT * FROM t_order WHERE order_id 123456。前者能分片后者必错。// ✅ 正确使用 #{} Select(SELECT * FROM t_order WHERE order_id #{orderId} AND status #{status}) ListOrder selectByOrderIdAndStatus(Param(orderId) Long orderId, Param(status) String status); // ❌ 错误使用 ${}ShardingSphere 无法解析参数值 Select(SELECT * FROM t_order WHERE order_id ${orderId}) ListOrder selectByOrderIdWrong(Param(orderId) Long orderId);更深层的原因是ShardingSphere 的SQLRewriteContext在PreparedStatement执行前会调用ParameterMetaData获取?对应的 Java 类型和值再传给ShardingRouteEngine做分片计算。而${}拼接的 SQL 已经是完整字符串绕过了 JDBC 的预编译流程ShardingSphere 根本没机会介入。2.3 SQL 语法兼容性哪些语法 ShardingSphere 能“看懂”哪些它直接放弃ShardingSphere-JDBC 的 SQL 解析器基于 Druid支持大部分标准 SQL但对复杂嵌套、特定数据库方言、以及部分 DDL 语句支持有限。常见不支持场景包括SQL 特性是否支持原因说明替代方案SELECT * FROM t_order o JOIN t_order_item i ON o.order_id i.order_id跨库 JOIN❌分库后t_order和t_order_item可能在不同物理库ShardingSphere 不做分布式 JOIN改为应用层两次查询或冗余必要字段到主表INSERT INTO t_order SELECT ... FROM t_order_backupINSERT ... SELECT❌解析器无法确定SELECT子句的分片键来源拆成SELECTINSERT两步手动控制分片ALTER TABLE t_order ADD COLUMN remark VARCHAR(255)⚠️ 部分支持仅当actualDataNodes包含所有目标物理表时才执行否则静默跳过必须确保所有物理库都执行 DDL建议用 Flyway 等工具统一管理SELECT COUNT(*) FROM t_order GROUP BY status ORDER BY COUNT(*) DESC LIMIT 10✅聚合分页已支持但需注意COUNT(*)是逻辑表聚合非物理表精确值生产环境建议加sharding-column过滤条件避免全库扫描注意sql-show: true日志里如果出现Actual SQL行说明该 SQL 被成功路由如果只有Logic SQL没有Actual SQL大概率是语法不支持ShardingSphere 直接抛异常或返回空结果。3. 自定义分片算法实战从INLINE到CLASS_BASED的平滑升级路径INLINE算法够快但硬编码在 YAML 里无法做动态规则、无法打日志、无法接入配置中心。当业务要求「用户 ID 尾号为 0-4 的订单路由到 ds_05-9 的路由到 ds_1且该规则下周要切换为按用户注册时间分片」时INLINE就成了技术债。CLASS_BASED是唯一出路。3.1StandardShardingAlgorithm接口详解doSharding方法的三个入参怎么用自定义算法必须实现StandardShardingAlgorithm单分片键或ComplexKeysShardingAlgorithm多分片键。核心是doSharding方法public class UserIdDatabaseShardingAlgorithm implements StandardShardingAlgorithmLong { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueLong shardingValue) { // availableTargetNames: 当前可用的物理数据源名集合如 [ds_0, ds_1] // shardingValue: 分片键值封装含 logicTableName, columnName, value Long userId shardingValue.getValue(); String targetDs ds_ (userId % 2); // ⚠️ 关键校验必须确保返回值在 availableTargetNames 内 if (!availableTargetNames.contains(targetDs)) { throw new UnsupportedOperationException( String.format(Target datasource %s not in available list: %s, targetDs, availableTargetNames)); } return targetDs; } Override public CollectionString doSharding(CollectionString availableTargetNames, RangeShardingValueLong shardingValue) { // 范围查询如 BETWEEN的分片逻辑此处简化为全路由 return availableTargetNames; } Override public void init() { // 初始化逻辑如加载配置、连接 Redis 缓存分片规则 System.out.println(UserIdDatabaseShardingAlgorithm initialized); } Override public String getType() { return USER_ID_DATABASE; } }参数说明availableTargetNames不是你配置的全部ds_0,ds_1而是当前健康、可连通的数据源列表。ShardingSphere 会定期探活如果ds_1网络不通这里就只有[ds_0]。你的算法必须检查返回值是否在其中否则抛SQLException。PreciseShardingValue对应,IN等精确查询value字段就是#{userId}的实际值类型已由 MyBatis 保证为Long。RangeShardingValue对应BETWEEN,,等范围查询getValueRange()返回Range对象需遍历availableTargetNames返回所有可能匹配的库。3.2 在application.yml中注册自定义算法class-based的完整配置YAML 配置里不能写 Java 类全限定名必须通过type属性关联rules: - !SHARDING # ... 其他表配置保持不变 shardingAlgorithms: t_order_database_custom: type: USER_ID_DATABASE # 必须与 getType() 返回值一致 props: # 可选传递自定义参数如 redis.host127.0.0.1 redis-host: 127.0.0.1同时需要在 Spring 容器中注册该算法的 BeanShardingSphere 会自动扫描Configuration public class ShardingAlgorithmConfig { Bean public StandardShardingAlgorithmLong userIdDatabaseShardingAlgorithm() { return new UserIdDatabaseShardingAlgorithm(); } }血泪经验getType()返回的字符串必须全大写、无下划线且与 YAML 中type值严格一致。曾有团队因写成user_id_database小写导致启动时报Cannot find sharding algorithm type排查 3 小时才发现是大小写问题。3.3 动态分片规则用HintManager绕过默认路由强制指定库表有些场景无法靠 SQL 参数分片比如后台运营系统要「强制查所有库的 t_order 表汇总数据」或灰度发布时「新用户走新分片规则老用户走旧规则」。这时要用HintManager它通过 ThreadLocal 注入分片上下文优先级高于 SQL 解析。Service public class OrderService { Resource private OrderMapper orderMapper; public ListOrder selectAllOrdersWithHint() { // 强制路由到 ds_0 和 ds_1 的所有 t_order 表 HintManager hintManager HintManager.getInstance(); hintManager.addDatabaseShardingValue(t_order, ds_0); hintManager.addDatabaseShardingValue(t_order, ds_1); hintManager.addTableShardingValue(t_order, t_order_0); hintManager.addTableShardingValue(t_order, t_order_1); hintManager.addTableShardingValue(t_order, t_order_2); hintManager.addTableShardingValue(t_order, t_order_3); try { return orderMapper.selectAll(); // 此次查询无视 user_id强制走指定库表 } finally { hintManager.close(); // ⚠️ 必须关闭否则影响后续请求 } } }注意HintManager是线程绑定的close()必须在finally块中执行。若忘记关闭当前线程后续所有数据库操作都会沿用该Hint导致线上事故。4. 避坑指南生产环境踩过的五个真实坑及根因修复分库分表不是配置完就能高枕无忧。以下是在某跨平台系统上线前压测和灰度期间暴露出的典型问题每一条都附带现象、根因和可立即落地的修复命令/配置。4.1 现象INSERT成功但SELECT查不到日志显示Actual SQL路由到ds_0.t_order_1而数据实际在ds_0.t_order_2原因分片键order_id的生成逻辑与分片算法不一致。例如代码里用SnowflakeIdGenerator.nextId()生成order_id但分片算法写的是order_id % 4而 Snowflake ID 的末位是机器 ID不是随机数导致order_id % 4结果严重倾斜如 90% 的 ID 模 4 等于 0。解决统一 ID 生成与分片逻辑。要么改算法为((int) (order_id 22) 0x3)取 Snowflake 时间戳高位要么在插入前用ThreadLocalRandom.current().nextInt(0, 4)随机指定table_suffix并存入字段分片算法改为按该字段路由。4.2 现象UPDATE t_order SET status PAID WHERE order_id 12345执行后SELECT * FROM t_order WHERE order_id 12345返回空原因UPDATE语句的WHERE条件中order_id是String类型如前端传参未转Long而分片算法PreciseShardingValue.getValue()返回String12345.hashCode() % 4与12345L % 4结果不同导致UPDATE和SELECT路由到不同物理表。解决在 MyBatis 的Param注解中强制指定类型或在 Mapper XML 中用bind标签转换select idselectById resultTypeOrder bind nameorderIdLong valuejava.lang.LongparseLong(orderId) / SELECT * FROM t_order WHERE order_id #{orderIdLong} /select4.3 现象应用启动报java.lang.NoClassDefFoundError: org/apache/shardingsphere/infrastructure/merge/MergeEngine但shardingsphere-jdbc-core-spring-boot-starter依赖已引入原因ShardingSphere-JDBC 5.x 与 SpringBoot 2.7 的spring-boot-starter-jdbc存在包冲突。shardingsphere-jdbc-core-spring-boot-starter5.3.2 依赖shardingsphere-infra-merge而该模块在 5.4.0 后被重构类路径变更。解决统一版本。推荐组合spring-boot-starter-parent: 2.7.18shardingsphere-jdbc-core-spring-boot-starter: 5.3.2。若必须用 SpringBoot 3.x则升至shardingsphere-jdbc-core-spring-boot-starter: 6.0.0-alpha注意 API 变更。4.4 现象SELECT COUNT(*) FROM t_order返回结果远小于实际行数且sql-show日志中Actual SQL只出现在ds_0未在ds_1执行原因COUNT(*)是聚合函数ShardingSphere 默认只在第一个匹配的物理库执行ds_0不合并其他库结果。这是设计使然非 Bug。解决启用sql-federation联邦查询props: sql-federation-enabled: true启用后ShardingSphere 会将COUNT(*)下推到所有物理库执行再在内存中聚合。但注意这会增加网络 IO 和应用内存压力大数据量时慎用。4.5 现象Transactional方法内先insert into t_order再insert into t_order_item后者报No route to datasource原因t_order_item的分片键是order_id但插入t_order时order_id是数据库自增 IDAUTO_INCREMENTMyBatis 的useGeneratedKeys获取到的 ID 在事务提交后才可见导致t_order_item插入时order_id为null分片算法无法计算。解决放弃数据库自增改用应用层生成全局唯一 ID如 Snowflake并在t_order插入前就确定order_id确保t_order_item能拿到有效分片键。5. 生产就绪 checklist从本地验证到灰度发布的七步验证法分库分表上线不是git push就完事。我参与过的三次大型分片迁移每次都在这个 checklist 上卡住至少一个环节。现在把它固化成七步每步都有可执行命令和预期输出。5.1 第一步验证分片算法本地可运行脱离 SpringBoot写一个独立的main方法直接调用doSharding输入各种边界值public class ShardingAlgorithmTest { public static void main(String[] args) { UserIdDatabaseShardingAlgorithm algorithm new UserIdDatabaseShardingAlgorithm(); algorithm.init(); // 测试典型值 System.out.println(algorithm.doSharding( Arrays.asList(ds_0, ds_1), new PreciseShardingValue(t_order, user_id, 1001L))); // 应输出 ds_1 // 测试 null 值MyBatis 传 null 时 try { algorithm.doSharding(Arrays.asList(ds_0), new PreciseShardingValue(t_order, user_id, null)); } catch (Exception e) { System.out.println(Null handling OK: e.getMessage()); } } }✅ 预期所有System.out输出符合预期null输入有明确异常。5.2 第二步验证application.yml配置语法正确性ShardingSphere 提供了离线配置校验工具。下载shardingsphere-jdbc-distribution-5.3.2-bin.tar.gz解压后执行cd shardingsphere-jdbc-distribution-5.3.2-bin/bin ./check-config.sh /path/to/your/application.yml✅ 预期输出Configuration validation passed.无ERROR或WARN。5.3 第三步验证 SQL 路由逻辑不连库用 ShardingSphere 提供的SQLParserEngine手动解析Test public void testSQLParse() { SQLParserEngine engine new SQLParserEngine(MySQL); ParseTree parseTree engine.parse(SELECT * FROM t_order WHERE user_id 1001, false); System.out.println(Parse tree: parseTree.toStringTree(Collections.singletonList(engine.getLexerRuleNames()))); }✅ 预期不抛异常parseTree不为null。5.4 第四步本地 H2 数据库验证路由准确性用 H2 内存库模拟两个数据源配置application.yml指向jdbc:h2:mem:ds_0和jdbc:h2:mem:ds_1执行INSERT后查information_schema.tables确认表创建在预期库-- 在 H2 中执行确认 t_order_0 ~ t_order_3 只在 ds_0 创建 SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMAPUBLIC;✅ 预期ds_0库中有t_order_0~t_order_3ds_1库中无这些表或只有t_order_0~t_order_3的子集取决于分片规则。5.5 第五步压测前必做show processlist观察实际连接数在 MySQL 中执行SHOW PROCESSLIST;✅ 预期连接数 max-connections-size-per-query× 并发线程数。若远超此值说明连接池未被 ShardingSphere 正确接管检查ShardingSphereDataSource是否被Primary修饰。5.6 第六步灰度发布时用HintManager强制流量切分在网关层或 Feign Client 拦截器中根据请求 Header如X-Shadow-Flag: true注入 Hintif (true.equals(request.getHeader(X-Shadow-Flag))) { HintManager hintManager HintManager.getInstance(); hintManager.setWriteDataSourceName(ds_1); // 强制写 ds_1 hintManager.addTableShardingValue(t_order, t_order_3); // 强制写 t_order_3 }✅ 预期灰度流量 100% 路由到指定库表主流量走默认规则两者互不干扰。5.7 第七步上线后第一小时盯紧三类日志关键词用grep实时监控# 1. 分片路由失败必须为 0 tail -f app.log | grep Cannot route # 2. SQL 解析异常必须为 0 tail -f app.log | grep SQLParsingException # 3. 连接获取超时50ms 需告警 tail -f app.log | grep HikariPool-1 - Connection add | grep ms✅ 预期上线首小时三类日志行数均为 0且HikariPool-1 - Connection add日志中ms值稳定在 1~10ms。从那以后我每次上线分库分表都强制走一遍这七步哪怕多花两天。因为分片一旦出错修复成本是百倍的——你要导数据、修一致性、回滚服务而一个grep就能拦住的错误不值得赌运气。希望帮到你。本文还有配套的精品资源点击获取