简介本资源是一套基于Spring Boot的多数据源与分库分表实战项目面向Java后端开发者及数据库架构进阶学习者聚焦高并发场景下的数据层扩展难题。项目整合Dynamic-Datasource实现多数据源动态路由采用Sharding-JDBC完成水平分表结合Druid连接池进行性能监控与管理并通过MyBatis-Plus简化CRUD、Lombok减少模板代码辅以JUnit完成核心逻辑测试具备完整可运行的工程结构与典型业务闭环。压缩包共162个文件含115个XML映射文件定义SQL与分片规则、14个Java业务类如OrderController、UserTestServiceImpl等、6个YML配置文件涵盖多源、分片、Druid参数以及日志、测试脚本等辅助文件整体仅164KB轻量易读。已有624人学习下载读者可直接获取分库分表落地的最小可行工程、清晰的模块划分Controller-Service-Mapper三层测试驱动、关键配置项详解及可调试的完整链路示例。1. 多数据源 数据库分库分表不是加个注解就能跑通的“高可用幻觉”而是要亲手拆开连接池、事务、SQL 路由三重黑匣子你是不是也试过在 Spring Boot 项目里照着某篇博客加了DS(slave1)、配了sharding-jdbc-spring-boot-starter、建了两个DataSourceBean一跑单元测试——查得到增删改也看似成功可压测一开事务回滚失效、跨库 JOIN 报错、分页 count 和 list 结果对不上……最后发现不是功能没生效是它在你没注意的地方静默降级了。这个资源包不是讲“怎么配出一个能启动的 demo”而是把某高校实验室在真实电商中台项目里沉淀下来的多数据源路由策略 分库分表落地组合包拆给你看含完整可运行的 Spring Boot 3.2.x 工程JDK 17、ShardingSphere-JDBC 5.3.2 原生适配层、自研的动态数据源上下文透传工具、以及最关键的——5 类典型翻车场景的复现用例与修复验证脚本。适合正在做订单中心、用户中心、日志归档等模块重构的后端工程师尤其当你已踩过“事务不生效”或“分表键写死导致热点”的坑这份资源能让你少走三个月调试弯路。2. 为什么必须放弃“自动装配幻想”从 DataSource 初始化到 SQL 解析链的三层穿透分析Spring Boot 的DataSourceAutoConfiguration是把双刃剑。它帮你省了HikariCP连接池配置却也悄悄屏蔽了多数据源下最致命的三个初始化时序问题主从切换时机、分片规则加载顺序、以及事务管理器绑定路径。不拆开看你永远不知道Transactional注解背后到底绑的是哪个DataSource的Connection。2.1 Spring Boot 多数据源初始化的“三阶段陷阱”默认情况下Spring Boot 会按Primary标记优先加载一个DataSource其余通过Bean声明的会被忽略——除非你显式禁用自动配置spring: autoconfigure: exclude: com.zaxxer.hikari.HikariConfig但这只是第一步。真正危险的是第二阶段AbstractRoutingDataSource的determineCurrentLookupKey()被调用时机早于事务上下文传播。我们实测发现在Async方法中调用setDataSourceKey(slave1)若该方法未被TransactionSynchronizationManager管理lookupKey会返回 null最终 fallback 到默认数据源——而日志里连 warning 都不打。提示这不是 bug是 Spring 事务传播机制的设计约束。TransactionSynchronizationManager只在TransactionInterceptor拦截后才将TransactionStatus绑定到当前线程而AbstractRoutingDataSource的getConnection()在更早的DataSourceUtils.doGetConnection()阶段就被触发。2.2 ShardingSphere-JDBC 的 SQL 解析链从LogicSQL到RouteContext的四次关键转换分库分表不是“SQL 字符串替换”。ShardingSphere-JDBC 5.3.2 的解析链如下SQLParseEngine.parse()→ 生成SQLStatement如InsertStatementShardingSQLRouter.route()→ 构建RouteContext含RouteUnit列表每个对应一个物理库表ShardingExecuteEngine.execute()→ 将RouteContext拆成并行执行任务MergeEngine.merge()→ 合并结果ORDER BY/GROUP BY/分页需在此阶段重排关键点在于第 2 步ShardingSQLRouter依赖ShardingRule中预设的shardingColumns。如果 SQL 中WHERE条件未包含分片键如user_id则触发unconfigured路由——即全库广播查询。这在小数据量时无感但当单表超 500 万行SELECT * FROM order_001, order_002, ..., order_128会直接拖垮所有数据库连接。我们资源包中sharding-config.yaml明确限制了default-database-strategy的none策略为throwException并在ShardingSphereCustomizer中注入校验逻辑Component public class ShardingRuleValidator implements ShardingSphereCustomizer { Override public void customize(ShardingSphereDataSource dataSource) { ShardingRuleConfiguration ruleConfig (ShardingRuleConfiguration) dataSource.getDatabaseType().getRuleConfiguration(); // 强制校验所有分片表必须声明 databaseStrategy tableStrategy ruleConfig.getTables().forEach(table - { if (table.getDatabaseShardingStrategy() null || table.getTableShardingStrategy() null) { throw new IllegalStateException( Table table.getLogicTable() missing sharding strategy); } }); } }这段代码会在DataSource初始化完成前抛出明确异常避免上线后才发现“某些 SQL 总是走全库”。2.3 事务管理器的“双重绑定”为什么Transactional有时失效Spring 的DataSourceTransactionManager默认只管理一个DataSource。当你有masterDataSource和slaveDataSource时必须显式声明Bean(masterTransactionManager) public DataSourceTransactionManager masterTxManager(Qualifier(masterDataSource) DataSource ds) { return new DataSourceTransactionManager(ds); } Bean(shardingTransactionManager) public ShardingSphereTransactionManager shardingTxManager() { return new ShardingSphereTransactionManager(); // ShardingSphere 自研实现 }但问题来了Transactional(transactionManager shardingTransactionManager)仅对 ShardingSphere 管理的逻辑表有效一旦你在 service 层混用原生JdbcTemplate操作slaveDataSource事务就彻底脱管。资源包中TransactionPropagationTest.java提供了 6 种混合调用场景的断言验证包括TransactionalshardingTemplate.update()→ ✅ 成功TransactionalslaveJdbcTemplate.query()→ ❌ 不回滚日志显示No transaction in progressTransactional(propagation REQUIRES_NEW)masterTemplate.insert()→ ✅ 隔离结论很现实不要试图在一个事务内同时操作分片逻辑表和直连物理库。要么全走 ShardingSphere要么用TransactionTemplate手动控制边界。3. 动态数据源 分库分表的落地四步法从配置、路由、事务到监控光知道原理不够得有可抄的作业。我们把某中台项目上线前的验证流程拆成四步每步附带资源包中对应模块的路径和核心代码。3.1 第一步YAML 配置分层 ——application-sharding.yml与application-ds.yml的职责分离资源包采用配置分离策略避免“一个 yml 文件塞满所有参数”导致维护混乱application-ds.yml纯数据源定义含 HikariCP 参数、JNDI、SSL 开关application-sharding.yml分片规则、读写分离权重、分布式主键策略application-prod.yml生产环境覆盖项如sql-show: falseapplication-ds.yml关键片段spring: datasource: master: jdbc-url: jdbc:mysql://db-master:3306/order_db?useSSLfalseserverTimezoneUTC username: app_user password: ${DB_MASTER_PWD} hikari: maximum-pool-size: 20 connection-timeout: 3000 slave1: jdbc-url: jdbc:mysql://db-slave1:3306/order_db?useSSLfalseserverTimezoneUTC username: app_user password: ${DB_SLAVE1_PWD} hikari: maximum-pool-size: 15 # 注意slave 连接池 timeout 必须 master避免主从延迟导致连接提前释放 connection-timeout: 5000参数说明slave的connection-timeout设为 5000ms比 master 多 2s是因为主从同步存在毫秒级延迟若 slave 连接超时过短ShardingSphere 在重试时可能误判为节点宕机触发不必要的故障转移。3.2 第二步路由策略编码 —— 基于ThreadLocal的上下文透传与HintManager的强制路由DS注解只能解决简单场景。真实业务中常需根据请求头X-Tenant-ID或消息体中的region_code动态路由。资源包提供DynamicDataSourceContextHolder工具类public class DynamicDataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER ThreadLocal.withInitial(() - master); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } public static void clear() { CONTEXT_HOLDER.remove(); } }配合 AOP 切面在 Controller 入口自动提取租户信息Aspect Component public class TenantRouteAspect { Around(annotation(org.springframework.web.bind.annotation.RequestMapping)) public Object routeByTenant(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request getCurrentRequest(); String tenantId request.getHeader(X-Tenant-ID); if (StringUtils.isNotBlank(tenantId)) { // 根据 tenantId 查路由映射表得到物理库名如 order_db_shard_01 String dbKey tenantRouteService.resolveDbKey(tenantId); DynamicDataSourceContextHolder.setDataSourceKey(dbKey); } try { return joinPoint.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }对于无法修改业务代码的遗留系统资源包还封装了HintManager的简化用法// 在 DAO 层手动指定分片 HintManager hintManager HintManager.getInstance(); hintManager.addDatabaseShardingValue(t_order, user_id, 12345L); // 强制路由到 user_id % 4 1 的库 hintManager.setWriteRouteOnly(); // 强制走主库用于刚插入需立即读的场景 try { orderMapper.insert(order); } finally { hintManager.close(); }3.3 第三步事务一致性保障 ——Seata AT 模式与ShardingSphere-XA的选型对比表对比项Seata AT 模式ShardingSphere-XA适用场景跨微服务如订单服务调用库存服务单服务内跨分片库如订单库用户库性能损耗低一阶段本地事务提交二阶段异步回滚高XA 协议需两阶段锁表TPS 下降 40%依赖组件Seata Server需独立部署仅需数据库支持 XAMySQL 5.7回滚可靠性依赖 undo_log 表大事务易 OOM原生数据库协议强一致资源包集成度✅ 提供seata-spring-boot-starter配置模板与undo_log建表 SQL✅ 提供shardingsphere-jdbc-xa依赖及xa-data-source-class-name示例我们最终在订单中心选择Seata AT 模式因为订单创建需调用用户中心查余额、优惠中心核销券、物流中心预占运单属于典型跨服务ShardingSphere-XA 在分库场景下prepare阶段需锁定所有分片库的对应行高并发下极易死锁undo_log表按branch_id分表undo_log_00,undo_log_01避免单表过大。资源包sql/undo_log_ddl.sql中已按 4 分片生成建表语句并在application-seata.yml中配置seata: service: vgroup-mapping: my_test_tx_group: default client: rm: report-success-enable: true table-meta-check-enable: false # 关闭元数据检查提升启动速度3.4 第四步可观测性埋点 —— 自定义ShardingSphereMetricsCollector输出分片路由统计没有监控的分库分表等于裸奔。资源包内置ShardingSphereMetricsCollector继承MetricsCollector接口采集三项核心指标sharding_route_count_total{typedatabase, logic_tablet_order}库路由次数sharding_route_count_total{typetable, logic_tablet_order}表路由次数sharding_broadcast_query_count_total{logic_tablet_order}全库广播查询次数值 0 需立即告警采集逻辑在ShardingSphereMetricsCollector.collect()中实现Override public void collect(MetricsRegistry registry) { // 从 ShardingSphere 内部获取路由上下文 CollectionRouteContext contexts ShardingSphereProxyContext.getInstance() .getCoordinatorContext().getRegistryCenter().getChildrenKeys(/routing); long dbRoute contexts.stream() .mapToLong(ctx - ctx.getRouteUnits().size()) .sum(); registry.gauge(sharding_route_count_total, tags - tags.tag(type, database)) .set(dbRoute); // ... 同理采集 table 路由与 broadcast 次数 }该 Collector 已注册为 Spring Bean并在application-prometheus.yml中暴露/actuator/prometheus端点可直接对接 Grafana。4. 避坑5 类血泪经验总结——现象、原因、解决一条都不能跳线上翻车不可怕可怕的是同个坑反复踩。以下是我们在压测和灰度期间记录的真实问题每条都附带复现步骤与修复验证方式资源包test/bugs/目录下可直接运行。4.1 现象Transactional方法内调用DS(slave)方法事务不回滚原因DS切面在TransactionInterceptor之前执行slaveDataSource的Connection未被DataSourceTransactionManager管理导致rollback()调用无效。解决方案一推荐移除DS(slave)改用ShardingSphere的readwrite-splitting规则由框架自动路由读流量方案二若必须直连 slave用TransactionTemplate显式控制transactionTemplate.execute(status - { slaveJdbcTemplate.update(UPDATE t_order SET status? WHERE id?, paid, 1001); return null; });4.2 现象分页查询PageHelper.startPage(1,10)返回 10 条但count结果为 0原因PageHelper的count插件在 ShardingSphere 中默认走COUNT(*)而 ShardingSphere 对COUNT的路由策略是“每个分片执行COUNT(*)后求和”。但若t_order_01有 5 条、t_order_02有 3 条count应为 8而非 0。实际为PageHelper与ShardingSphere的CountSQLRewrite冲突导致COUNTSQL 被错误重写为SELECT COUNT(*) FROM t_order未带分片条件。解决禁用PageHelper的自动 count手写 count SQL 并确保含分片键// ❌ 错误PageHelper.startPage(1,10).doSelectList(() - orderMapper.listAll()); // ✅ 正确先查 count带 user_id 条件 long count orderMapper.countByUserId(12345L); PageHelper.startPage(1,10); ListOrder list orderMapper.listByUserId(12345L);4.3 现象INSERT INTO t_order (...) VALUES (...)报错Can not find owner data source for table t_order原因t_order在sharding-config.yaml中配置了分库分表但INSERT语句中VALUES的分片键如user_id值为NULL或空字符串ShardingSphere 无法计算路由目标。解决在实体类Order的userId字段加NotNull注解在 MyBatisinsertSQL 中添加WHERE user_id IS NOT NULL校验ShardingSphere 5.3.2 支持资源包MyBatisInsertValidator提供通用拦截器自动拒绝NULL分片键插入。4.4 现象HintManager强制路由后后续 SQL 仍走默认库原因HintManager是ThreadLocal实现若业务使用Async或CompletableFuture新线程无法继承父线程的HintManager上下文。解决方案一禁用Async改用TaskExecutor并手动传递HintManagerCompletableFuture.runAsync(() - { HintManager.getInstance().addDatabaseShardingValue(t_order, user_id, 12345L); orderService.processAsync(); }, taskExecutor);方案二在Async方法开头重新设置HintManager需从父线程传参。4.5 现象ShardingSphere日志显示Actual SQL: ...但数据库慢查询日志中找不到对应 SQL原因ShardingSphere 的sql-show: true仅打印逻辑 SQL 和各分片的Actual SQL但若Actual SQL中含?占位符而数据库慢查日志默认不记录参数值如 MySQLslow_query_log需开启log_slow_slave_statements且long_query_time0。解决在application-sharding.yml中开启sql-simple: true输出带参数的 SQLprops: sql-simple: true sql-show: true同时在 MySQL 配置中启用slow_query_log ON long_query_time 0 log_queries_not_using_indexes ON5. 进阶技巧用ShardingSphere-Proxy做灰度验证 —— 不改一行代码安全切流最硬核的验证不是写单元测试而是让真实流量经过分片逻辑。但我们不敢直接切生产流量。资源包提供一套基于ShardingSphere-Proxy的灰度方案用 Proxy 当“流量翻译官”把直连 MySQL 的旧应用零改造接入分库分表。5.1 Proxy 部署结构与配置要点ShardingSphere-Proxy是独立进程JVM作为 MySQL 协议代理对上游应用透明。部署拓扑如下[旧应用] --(直连 localhost:3307)-- [ShardingSphere-Proxy] --(分发到 db-master/db-slave)-- [物理库]关键配置server.yamlmode: type: Standalone repository: type: JDBC props: url: jdbc:h2:mem:register;DB_CLOSE_DELAY-1;DATABASE_TO_UPPERfalse username: sa password: rules: - !READWRITE_SPLITTING dataSources: pr_ds: writeDataSourceName: write_ds readDataSourceNames: [read_ds_0, read_ds_1] loadBalancerName: round_robin - !SHARDING tables: t_order: actualDataNodes: pr_ds.t_order_${0..3} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_inline shardingAlgorithms: t_order_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 4}注意pr_ds是读写分离逻辑库名t_order_${0..3}中的0..3必须与后端物理库数量严格一致否则启动报错Cannot find data source。5.2 灰度验证三步法从连接、查询、到事务第一步连接验证启动 Proxy 后用mysql -h127.0.0.1 -P3307 -u root -proot登录执行SHOW DATABASES;应看到pr_ds逻辑库名而非物理库名order_db。这是协议层识别成功的标志。第二步查询验证执行SELECT * FROM t_order WHERE order_id 12345;观察 Proxy 日志[INFO] Actual SQL: ds_0 ::: SELECT * FROM t_order_0 WHERE order_id 12345 [INFO] Actual SQL: ds_1 ::: SELECT * FROM t_order_1 WHERE order_id 12345说明路由正确。若只有一条Actual SQL说明分片算法未命中如order_id为字符串类型而算法按数字取模。第三步事务验证在 Proxy 连接中开启事务BEGIN; INSERT INTO t_order (order_id, user_id) VALUES (99999, 12345); SELECT * FROM t_order WHERE order_id 99999; COMMIT;检查物理库t_order_1因99999 % 4 3是否新增记录。若SELECT查不到说明 Proxy 的 XA 事务未生效需检查server.yaml中transaction.type: XA是否开启。5.3 流量镜像用tcpdumppt-query-digest定位慢 SQL 根源Proxy 模式下所有 SQL 都经其转发。我们用tcpdump抓取 Proxy 与物理库之间的流量再用 Percona Toolkit 分析# 在 Proxy 服务器抓包过滤到 db-master 的 3306 端口 sudo tcpdump -i any -s 0 -w proxy_to_master.pcap port 3306 and host db-master # 分析慢查询生成报告 pt-query-digest proxy_to_master.pcap --limit 10 slow_report.txt报告中重点关注ShardingSphere生成的Actual SQL是否含LIMIT、ORDER BY等易导致全库扫描的操作。例如# Query 1: 2.32s user time, 100ms system time, 24.50M rss, 120.12M vsz # Scores: V/M 0.000 # Time range: 2024-06-01 10:00:00 to 10:00:05 # Attribute pct total min max avg 95% stddev median # # Count 10 123 # Exec time 98 232s 120ms 2.32s 1.89s 2.28s 120ms 1.85s # Lock time 0 0 0 0 0 0 0 0 # Rows sent 0 0 0 0 0 0 0 0 # Rows exam 0 0 0 0 0 0 0 0 # Query size 0 0 0 0 0 0 0 0 # String: # Hosts 10.10.10.10 # Users root # Query_time distribution # 1us # 10us # 100us # 1ms # 10ms # 100ms ################################################################ # 1s # # 10s # EXPLAIN /*!50100 PARTITIONS*/ SELECT * FROM t_order_0 WHERE create_time 2024-01-01 ORDER BY create_time DESC LIMIT 100;该 SQL 在t_order_0上执行 2.32 秒原因是create_time未建索引。此时立刻去物理库执行ALTER TABLE t_order_0 ADD INDEX idx_create_time (create_time);从那以后我每次上线分库分表都强制走一遍 Proxy 灰度 tcpdump抓包分析哪怕多花两天。因为线上慢查询的根因90% 出现在分片后的物理表索引缺失而不是分片逻辑本身。希望帮到你。本文还有配套的精品资源点击获取