简介:本资源是一套基于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(如InsertStatement)ShardingSQLRouter.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 种混合调用场景的断言验证,包括:@Transactional+shardingTemplate.update()→ ✅ 成功@Transactional+slaveJdbcTemplate.query()→ ❌ 不回滚(日志显示No transaction in progress)@Transactional(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: false)application-ds.yml关键片段:spring: datasource: master: jdbc-url: jdbc:mysql://db-master:3306/order_db?useSSL=false&serverTimezone=UTC 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?useSSL=false&serverTimezone=UTC 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 ThreadLocal<String> 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(需独立部署)仅需数据库支持 XA(MySQL 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{type="database", logic_table="t_order"}:库路由次数sharding_route_count_total{type="table", logic_table="t_order"}:表路由次数sharding_broadcast_query_count_total{logic_table="t_order"}:全库广播查询次数(值 > 0 需立即告警)采集逻辑在ShardingSphereMetricsCollector.collect()中实现:@Override public void collect(MetricsRegistry registry) { // 从 ShardingSphere 内部获取路由上下文 Collection<RouteContext> 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); List<Order> 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并手动传递HintManager:CompletableFuture.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_time=0)。解决:在application-sharding.yml中开启sql-simple: true,输出带参数的 SQL:props: 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.yaml:mode: type: Standalone repository: type: JDBC props: url: jdbc:h2:mem:register;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false 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 流量镜像:用tcpdump+pt-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% 出现在分片后的物理表索引缺失,而不是分片逻辑本身。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是一套基于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(如InsertStatement)ShardingSQLRouter.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 种混合调用场景的断言验证,包括:@Transactional+shardingTemplate.update()→ ✅ 成功@Transactional+slaveJdbcTemplate.query()→ ❌ 不回滚(日志显示No transaction in progress)@Transactional(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: false)application-ds.yml关键片段:spring: datasource: master: jdbc-url: jdbc:mysql://db-master:3306/order_db?useSSL=false&serverTimezone=UTC 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?useSSL=false&serverTimezone=UTC 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 ThreadLocal<String> 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(需独立部署)仅需数据库支持 XA(MySQL 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{type="database", logic_table="t_order"}:库路由次数sharding_route_count_total{type="table", logic_table="t_order"}:表路由次数sharding_broadcast_query_count_total{logic_table="t_order"}:全库广播查询次数(值 > 0 需立即告警)采集逻辑在ShardingSphereMetricsCollector.collect()中实现:@Override public void collect(MetricsRegistry registry) { // 从 ShardingSphere 内部获取路由上下文 Collection<RouteContext> 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); List<Order> 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并手动传递HintManager:CompletableFuture.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_time=0)。解决:在application-sharding.yml中开启sql-simple: true,输出带参数的 SQL:props: 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.yaml:mode: type: Standalone repository: type: JDBC props: url: jdbc:h2:mem:register;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false 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 流量镜像:用tcpdump+pt-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% 出现在分片后的物理表索引缺失,而不是分片逻辑本身。希望帮到你。本文还有配套的精品资源,点击获取