Spring Boot多数据源切换与分库分表实战指南 简介本资源是一份面向Java后端开发者与分布式系统学习者的分库分表实战项目包聚焦企业级大数据量场景下的数据库水平扩展难题以Sharding-JDBC为核心框架实现多数据源动态切换与透明化分片。资源共73个文件涵盖18个Java业务与配置类、21个编译后class文件、18个XML配置与映射文件含MyBatis映射及Sharding规则定义、3个properties配置项及SQL建表脚本等整体仅66KB轻量易导入结构清晰体现SSMDemo典型Maven工程布局含pom.xml、src/main、mybatistest.sql等关键模块。已有2068人学习下载适合中高级开发者快速掌握分片路由逻辑、多数据源初始化方式及Sharding-JDBC在SSM整合环境中的落地细节。读者可直接运行项目理解分库分表配置加载流程、观察SQL自动路由效果并基于现有结构拓展范围分片、分布式事务等进阶实践。1. 分库分表多数据源的切换不是加个注解就完事而是要让每个SQL知道自己该去哪张物理表、哪个数据库里找人“分库分表多数据源的切换”——这八个字在中大型系统架构图里高频出现但真落到代码里它从来不是一句DS(slave1)就能闭环的事。我见过太多团队在压测阶段突然发现订单查不到、库存扣重了、流水对不上账最后追到根上是同一个逻辑事务里A服务用了主库写B服务却从缓存兜底后切到了只读从库查旧快照或是分表键没对齐用户ID取模分了16张表而统计任务按时间范围扫全表时却只连了其中3个分片库漏掉了70%的数据。这不是配置错误是路由策略、事务边界、连接生命周期三者没对齐的系统性失焦。它适合正在把单体MySQL撑到500万行/日写入、或已接入ShardingSphere但总在跨库JOIN和分布式ID生成上反复调试的后端工程师。如果你还在用application.yml里硬写spring.shardingsphere.datasource.namesds0,ds1,ds2就以为搞定了分库分表那这篇笔记就是给你留的后悔药——我们不讲概念直接拆解路由规则怎么写才不漏数据、动态数据源怎么切才不污染线程、XA事务在分库场景下为什么建议关掉、以及最关键的——当运维半夜打电话说“ds1库磁盘满了”你该怎么在不重启服务的前提下把新流量切走。2. 从单数据源到多数据源Spring Boot AbstractRoutingDataSource 的最小可运行骨架分库分表的本质是让应用层感知不到物理库表的分裂但又必须精准控制每条SQL的落点。Spring生态里最轻量、最可控的落地方式不是一上来就堆ShardingSphere而是先用AbstractRoutingDataSource搭出一个“会思考”的数据源代理。它不负责分片逻辑只负责在每次获取Connection前问一句“这次该用哪个真实数据源”。这个决策权交给你自己写。2.1 定义动态数据源上下文与路由键核心是维护一个线程级的“路由线索”。我们不用ThreadLocal手动传而是封装成工具类避免业务代码到处set()和remove()public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER ThreadLocal.withInitial(() - ds_master); public static void setDataSource(String dataSourceName) { CONTEXT_HOLDER.set(dataSourceName); } public static String getDataSource() { return CONTEXT_HOLDER.get(); } public static void clear() { CONTEXT_HOLDER.remove(); } }提示ds_master是默认值必须存在且不能为null否则AbstractRoutingDataSource会抛IllegalStateException。这个默认值不是摆设——它会在全局异常拦截器、定时任务、甚至某些异步线程里成为兜底保障。2.2 实现路由逻辑从Key到真实DataSource的映射继承AbstractRoutingDataSource重写determineCurrentLookupKey()方法。注意返回值类型必须是Object但实际用字符串做key最稳妥Configuration public class DynamicDataSourceConfig { Bean Primary public DataSource dynamicDataSource( Qualifier(masterDataSource) DataSource master, Qualifier(slaveDataSource) DataSource slave) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(ds_master, master); targetDataSources.put(ds_slave, slave); AbstractRoutingDataSource routingDataSource new AbstractRoutingDataSource() { Override protected Object determineCurrentLookupKey() { // 这里返回的key必须和targetDataSources的key完全一致大小写敏感 return DataSourceContextHolder.getDataSource(); } }; routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(master); // 必须设置 return routingDataSource; } }关键参数说明targetDataSourcesMap的key是逻辑名如ds_mastervalue是Spring容器里已注册的真实DataSourceBean。setDefaultTargetDataSource当determineCurrentLookupKey()返回null或未匹配时的兜底数据源必须显式设置否则启动报错。determineCurrentLookupKey()每次getConnection()都会调用此方法。它的执行时机早于MyBatis的SqlSession创建因此能在SQL执行前完成路由。2.3 在DAO层注入路由意图用AOP统一管理读写分离手动在每个Service方法开头DataSourceContextHolder.setDataSource(ds_slave)太脆弱。我们用AOP在方法执行前自动识别读/写意图Aspect Component public class DataSourceAspect { Pointcut(annotation(org.springframework.transaction.annotation.Transactional)) public void transactionalMethod() {} Around(transactionalMethod()) public Object routeByTransaction(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); Transactional tx method.getAnnotation(Transactional.class); if (tx ! null tx.readOnly()) { DataSourceContextHolder.setDataSource(ds_slave); } else { DataSourceContextHolder.setDataSource(ds_master); } try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); // 必须清理否则线程复用时污染后续请求 } } }注意finally块里的clear()是血泪经验。Tomcat默认用线程池处理HTTP请求若不清理下一个请求可能沿用上一个请求的ds_slave导致写操作误发到从库报Read-only database错误。3. 分库分表的核心ShardingSphere-JDBC 的分片策略配置与实战校验当数据量突破单库瓶颈比如订单表超2000万行读写分离已不够必须物理拆分。ShardingSphere-JDBC 是当前Java生态最成熟的分片中间件它以JDBC Driver形式嵌入应用零侵入改造现有DAO。但它的配置不是填空题而是逻辑题——分片键选错、算法写歪会导致数据倾斜、查询全扫、跨库聚合失效。3.1 分片键选择为什么用户ID比订单时间更适合作为分库键分库Database Sharding和分表Table Sharding可以独立配置但分库键必须是分表键的超集否则路由无法收敛。常见误区是用create_time分库——看似均匀实则灾难热点问题大促期间所有订单集中在几秒内全部路由到同一库范围查询失效WHERE create_time BETWEEN 2024-01-01 AND 2024-01-31需要遍历所有库丧失分片价值。正确做法用高基数、稳定、业务强相关的字段如user_id。它天然满足均匀性用户注册是长尾分布ID哈希后各库负载接近关联性订单、地址、积分等表都含user_id可保证“同用户数据落在同库”避免跨库JOIN可预测性前端传user_id后端可提前路由无需查元数据。3.2 配置YAML精准控制分库分表的两层路由以下配置实现按user_id分4库ds_0~ds_3每库内按order_id分8表t_order_0~t_order_7spring: shardingsphere: props: sql-show: true # 开发期必开看实际执行的SQL发往哪个库表 datasource: names: ds_0,ds_1,ds_2,ds_3 ds_0: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://db0:3306/order_db?serverTimezoneUTC username: root password: pwd # ... ds_1 ~ ds_3 同理 rules: - !SHARDING tables: t_order: actual-data-nodes: ds_${0..3}.t_order_${0..7} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_table_inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_db_inline sharding-algorithms: t_order_db_inline: type: INLINE props: algorithm-expression: ds_${user_id % 4} # 分库user_id对4取模 t_order_table_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 8} # 分表order_id对8取模关键参数深挖actual-data-nodes: 模板表达式${0..3}生成ds_0,ds_1,ds_2,ds_3${0..7}生成t_order_0~t_order_7最终组合出32个物理节点sharding-column: 必须是SQL WHERE条件中出现的列否则ShardingSphere无法解析路由algorithm-expression: 表达式里变量名必须和sharding-column值完全一致大小写敏感user_id % 4中user_id必须是实体类字段名不是数据库列名。3.3 执行计划验证用EXPLAIN确认SQL是否真的下推到单库单表配置完别急着压测先用EXPLAIN看路由是否生效-- 在任意ShardingSphere-JDBC代理的数据库连接中执行 EXPLAIN SELECT * FROM t_order WHERE user_id 12345 AND order_id 67890;预期返回| data_node | sql | |-----------|---------------------------------------------------------------------| | ds_1 | SELECT * FROM t_order_2 WHERE user_id 12345 AND order_id 67890 |如果data_node显示ds_0,ds_1,ds_2,ds_3全部出现说明user_id未被识别为分片键——检查实体类TableField是否标注了value user_id或MyBatis XML中是否用了#{userId}而非#{user_id}导致参数名不匹配。4. 避坑指南分库分表与多数据源切换中5个高频翻车现场分库分表项目上线后80%的线上故障源于配置与认知偏差。以下是我在三个模拟项目X中踩过的坑按发生频率排序每条附带可复现的验证步骤。4.1 现象NoNodeAvailableException报错但所有数据库连接测试都通原因ShardingSphere的actual-data-nodes配置中ds_${0..3}生成的库名与datasource.names定义的ds_0,ds_1,ds_2,ds_3不一致如少了个下划线写成d0,d1。ShardingSphere在启动时不会校验节点是否存在直到第一条SQL执行才尝试连接此时找不到对应数据源。解决检查datasource.names与actual-data-nodes中的库名模板是否字符级一致启动时加JVM参数-Dorg.apache.shardingsphere.mode.repository.typeZooKeeper若用ZK模式可提前暴露配置错误在application.yml中开启sql-show: true观察启动日志是否有Cant find data source字样。4.2 现象分页查询LIMIT 10,10结果重复或漏数据原因ORDER BY create_time LIMIT在分库环境下各库返回自己的前20条合并后全局序错乱。ShardingSphere默认不改写此类SQL需显式启用pagination功能。解决在application.yml中添加spring: shardingsphere: props: query-with-cipher-column: false # 必须开启分页修正 sql-show: true rules: - !SHARDING # ... 其他配置 binding-tables: t_order,t_order_item # 若有关联表必须声明绑定关系并在SQL中强制使用ORDER BYLIMIT组合避免无序分页。4.3 现象Transactional方法内调用另一个Transactional方法从库读取到未提交数据原因Spring默认PROPAGATION_REQUIRED内层事务复用外层Connection。但AbstractRoutingDataSource的determineCurrentLookupKey()在Connection创建后即固定不会因内层方法Transactional(readOnlytrue)而切换。解决方案1推荐将读操作抽离为独立Service用REQUIRES_NEW传播行为确保新开Connection方案2在AOP中增强逻辑检测嵌套事务时强制重置DataSourceContextHolder但需谨慎处理异常回滚。4.4 现象批量插入INSERT INTO t_order VALUES(...),(...)路由到多个库性能暴跌原因ShardingSphere对批量SQL的路由是逐条计算的。若order_id分散100条INSERT可能打到8个不同库网络往返激增。解决业务层预聚合按user_id % 4分组每组内再按order_id % 8分表生成8个独立INSERT语句或改用sharding-jdbc-spring-boot-starter4.1.1版本开启rewrite-batch-inserts: true需MySQL驱动8.0.21。4.5 现象SELECT COUNT(*) FROM t_order返回结果远小于实际行数原因COUNT聚合未下推到各库执行ShardingSphere默认只在单库执行并返回。这是设计使然非Bug。解决方案1用SELECT COUNT(*) FROM t_orderUNION ALL手写跨库聚合不推荐方案2生产首选接入Elasticsearch同步订单数据COUNT走ES聚合方案3接受近似值在application.yml中配置props.sql-show: true观察日志中各库返回的COUNT手动相加仅限离线校验。5. 生产就绪动态数据源热切换与分片元数据一致性保障当DBA通知“ds_2库磁盘告警需迁移10%流量到ds_4”或者业务方要求“新注册用户全部路由到新库”硬重启服务是下策。真正的生产就绪能力是让分片策略和数据源列表支持运行时变更且不中断任何请求。5.1 数据源热加载基于Spring Cloud Config RefreshScope 的动态刷新ShardingSphere本身不支持运行时增删数据源但我们可以在其外层再包一层动态代理。核心思路让AbstractRoutingDataSource的targetDataSourcesMap支持运行时更新并触发afterPropertiesSet()重初始化。Component RefreshScope // 关键使Bean支持配置刷新 public class HotSwappableDataSource extends AbstractRoutingDataSource { private final MapObject, Object dynamicTargetDataSources new ConcurrentHashMap(); PostConstruct public void init() { // 初始加载 reloadFromConfig(); } public void reloadFromConfig() { // 从Config Server拉取最新数据源配置构建新的targetDataSources MapObject, Object newSources fetchLatestDataSources(); dynamicTargetDataSources.clear(); dynamicTargetDataSources.putAll(newSources); // 强制ShardingSphere重新加载需反射调用 try { Field field AbstractRoutingDataSource.class.getDeclaredField(resolvedDataSources); field.setAccessible(true); field.set(this, newSources); } catch (Exception e) { log.error(Failed to refresh resolvedDataSources, e); } } Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }配合Spring Cloud Config当application-dev.yml中spring.shardingsphere.datasource.ds_4新增时调用/actuator/refresh端点即可触发reloadFromConfig()。5.2 分片策略热更新用ZooKeeper存储分片规则监听节点变化ShardingSphere原生支持ZooKeeper作为注册中心存储分片规则。启用后所有分片策略如algorithm-expression都存于ZK节点/sharding-rules/t_order/database-strategy/standard。运维可通过ZK客户端直接修改ShardingSphere会自动监听并重载。# application.yml spring: shardingsphere: mode: type: Cluster repository: type: ZooKeeper props: namespace: sharding-demo server-lists: zk1:2181,zk2:2181,zk3:2181 retry-intervalMilliseconds: 500 time-to-live-seconds: 60提示ZK模式下actual-data-nodes必须用ds_${0..3}.t_order_${0..7}这种静态模板不能用ds_${online_status true ? 0 : 1}等动态表达式否则ZK无法序列化。5.3 元数据一致性校验用Python脚本每日比对各库表结构与行数分库后最怕“某库表结构漏改”。我们用一个轻量脚本每天凌晨扫描所有分片库输出差异报告#!/usr/bin/env python3 # check_sharding_consistency.py import pymysql import sys CONFIGS { ds_0: {host: db0, db: order_db}, ds_1: {host: db1, db: order_db}, # ... ds_2, ds_3 } def get_table_schema(conn, table_name): with conn.cursor() as cur: cur.execute(fSHOW CREATE TABLE {table_name}) return cur.fetchone()[1] def get_row_count(conn, table_name): with conn.cursor() as cur: cur.execute(fSELECT COUNT(*) FROM {table_name}) return cur.fetchone()[0] if __name__ __main__: schemas {} counts {} for ds_name, conf in CONFIGS.items(): conn pymysql.connect(**conf, charsetutf8mb4) schemas[ds_name] get_table_schema(conn, t_order_0) counts[ds_name] get_row_count(conn, t_order_0) conn.close() # 比对schema base_schema list(schemas.values())[0] for ds, schema in schemas.items(): if schema ! base_schema: print(f❌ Schema mismatch in {ds}) # 比对行数允许5%误差 base_count list(counts.values())[0] for ds, cnt in counts.items(): if abs(cnt - base_count) / base_count 0.05: print(f⚠️ Row count skew in {ds}: {cnt} vs {base_count})将此脚本加入Crontab输出重定向到企业微信机器人异常时秒级告警。我习惯在每次上线分片策略前先跑一遍这个脚本再用EXPLAIN验证3个典型SQL的路由路径。不是信不过配置是信不过人脑对复杂表达式的穷举能力。分库分表没有银弹只有把每一步的“为什么这样”刻进肌肉记忆才能在半夜告警电话响起时手指不抖地敲出curl -X POST http://localhost:8080/actuator/refresh。希望帮到你。本文还有配套的精品资源点击获取