资讯动态

ShardingSphere分库分表实战:连接池、事务与SQL路由避坑指南

发布时间:2026/10/27 13:01:30 来源:尧图企业网站定制
简介本资源是一套基于Spring Boot的多数据源与分库分表实战项目面向Java后端开发者及数据库架构进阶学习者聚焦高并发场景下数据库水平扩展的核心痛点。项目完整集成Dynamic-Datasource实现多数据源动态路由采用Sharding-JDBC完成逻辑表到物理分片的透明映射并通过Druid连接池统一管理数据库连接同时引入MyBatis-Plus简化CRUD、Lombok减少模板代码辅以JUnit单元测试保障核心逻辑可靠性。压缩包共162个文件含115个XML映射文件定义SQL与分片规则、14个Java业务类如OrderController、UserTestServiceImpl等、6个YML配置含多源与分片策略、以及日志、测试脚本和构建文件整体仅164KB轻量易读。已有624人学习下载提供开箱即用的可运行工程结构、清晰的分层模块设计、典型分表场景如订单、用户的完整实现链路以及可直接复用的数据源切换与分片键配置范例。1. 多数据源 数据库分库分表不是加个注解就能跑通的“高可用幻觉”而是必须亲手拆开连接池、事务边界和 SQL 路由逻辑的硬核工程你是不是也见过这样的场景Spring Boot 项目刚上线时单库扛得住QPS 300半年后订单量翻 5 倍慢 SQL 报警天天弹窗DBA 在群里发截图说“主库 CPU 98% 持续 2 小时”技术方案会上有人脱口而出“上分库分表吧”——结果一落地分布式事务报错、跨库 JOIN 直接失效、MyBatis 的SelectProvider生成的 SQL 在分片键缺失时默默查全库……这不是架构升级这是把单点故障扩散成多点雪崩。这份资源不是讲 ShardingSphere 官方文档的搬运工而是一线工程师在模拟项目X中真实踩坑、反复回滚、最终稳定支撑日均 800 万订单写入的完整实践包它包含可直接运行的 Spring Boot 2.7.x ShardingSphere-JDBC 5.3.2 多数据源配置模板、基于真实业务字段user_id order_no的分片策略代码、绕过Transactional传播陷阱的手动事务管理封装、以及最关键的——一份能定位“为什么这条 SQL 没走分片路由”的日志增强补丁。适合正在评估分库分表可行性、已卡在事务一致性或查询路由失败阶段的后端开发者尤其适合没有 DBA 配合、需自主掌控数据流向的中小团队。2. 分库分表不是选型游戏是连接池、SQL 解析与路由规则的三重校准2.1 为什么必须放弃 Druid MyBatis 原生组合连接池与分片代理的底层冲突ShardingSphere-JDBC 是以 JDBC Driver 形式嵌入应用的轻量级分片中间件它要求所有数据库连接必须通过其代理 Driverorg.apache.shardingsphere.driver.jdbc.core.datasource.ShardingSphereDataSource创建。而传统 Druid 连接池com.alibaba.druid.pool.DruidDataSource会直接向物理数据库发起连接请求完全绕过 ShardingSphere 的 SQL 解析与路由逻辑。现象是你的application.yml里明明配置了sharding: default-database-strategy: ...但实际执行的 SQL 却直连到ds_0库且不按分片键路由。提示ShardingSphere-JDBC 5.x 后不再兼容 Druid 1.x 的init阶段连接预热若强行注入 Druid DataSource会导致ShardingSphereDataSource初始化失败抛出NullPointerExceptionatDataSourceManager.createDataSource()。正确做法是彻底弃用 Druid改用 ShardingSphere 自带的数据源工厂。它内部封装了 HikariCP默认并确保每个物理数据源都经过ShardingSphereDataSource统一管理# application.yml spring: shardingsphere: mode: Standalone datasource: 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: 123456 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: 123456 rules: - !SHARDING tables: t_order: actual-data-nodes: ds_${0..1}.t_order_${0..3} table-strategy: standard: sharding-column: order_no sharding-algorithm-name: t_order_table_inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_database_inline sharding-algorithms: t_order_database_inline: type: INLINE props: algorithm-expression: ds_${user_id % 2} t_order_table_inline: type: INLINE props: algorithm-expression: t_order_${order_no % 4}这段配置声明了 2 个物理数据源ds_0,ds_1t_order表按user_id分库模 2、按order_no分表模 4共生成 2×48 个实际表节点。关键点在于所有Mapper接口的操作都必须通过ShardingSphereDataSource获取的 Connection 执行。Spring Boot 自动装配会将该数据源注册为DataSourceBeanMyBatis-Spring-Boot-Starter 会自动识别并使用它无需额外配置SqlSessionFactoryBean.setDataSource()。2.2 分片键不是随便挑一个字段从 SQL 解析器视角看user_id和order_no的不可替代性ShardingSphere 的 SQL 解析器基于 Antlr4在执行前会对 SQL 进行词法分析提取WHERE、INSERT INTO ... VALUES (...)、UPDATE ... SET ... WHERE ...中的分片键值。若分片键未出现在解析上下文中路由将退化为广播broadcast或报错。我们曾用create_time作为分表键结果发现SELECT * FROM t_order WHERE create_time 2024-01-01无法路由——因为create_time是范围查询ShardingSphere 5.3.2 默认不支持范围分片需自定义ComplexKeysShardingAlgorithm复杂度陡增。而user_id和order_no是理想候选user_id是等值查询高频字段如WHERE user_id ?且业务上天然具备数据隔离性一个用户的所有订单应落在同一库order_no是全局唯一、无业务含义的字符串如ORD20240520123456789可通过截取后缀数字做模运算避免热点如Long.parseLong(order_no.substring(10)) % 4。在t_order_database_inline算法中user_id % 2必须保证user_id是数值类型。若数据库中user_id是 VARCHAR需在分片算法中显式转换public final class UserIdDatabaseShardingAlgorithm implements StandardShardingAlgorithmComparable? { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueComparable? shardingValue) { // 强制转 long避免 NumberFormatException long userId Long.parseLong(shardingValue.getValue().toString()); return ds_ (userId % 2); } // ... 其他方法省略 }此处availableTargetNames是[ds_0, ds_1]shardingValue.getValue()是传入的user_id参数值。若传入null或非数字字符串Long.parseLong会抛异常导致整个 SQL 执行失败——这正是我们需要的强校验而非静默路由到错误库。2.3 为什么Transactional在跨库场景下是“定时炸弹”手动事务管理的必要性Spring 的Transactional默认使用DataSourceTransactionManager它依赖单一DataSource的Connection对象。当 ShardingSphere 路由到多个物理库如ds_0.t_order_1和ds_1.t_order_2时事务管理器只能绑定其中一个Connection另一个库的操作将脱离事务控制形成“半提交”状态。典型翻车场景下单接口需同时插入t_order和t_order_item后者按order_id分片。若t_order路由到ds_0t_order_item路由到ds_1Transactional仅对ds_0生效ds_1的插入成功但ds_0因校验失败回滚最终产生脏数据。解决方案是放弃声明式事务改用 ShardingSphere 提供的ShardingTransactionManager需启用 XA 或 Seata或更轻量的“本地事务补偿”模式。本资源采用后者封装TransactionTemplate在业务方法内显式控制每个分片键对应的事务Service public class OrderService { Resource private JdbcTemplate jdbcTemplate; // 使用 ShardingSphere 注入的 JdbcTemplate public void createOrder(Order order, ListOrderItem items) { // 步骤1插入主订单获取分片后的实际库表名 String dbNode ds_ (order.getUserId() % 2); String tableNode t_order_ (Long.parseLong(order.getOrderNo().substring(10)) % 4); // 步骤2手动开启事务针对 ds_0 或 ds_1 TransactionStatus status transactionManager.getTransaction( new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED) ); try { // 插入 t_order路由由 ShardingSphere 自动完成 jdbcTemplate.update(INSERT INTO t_order (...) VALUES (...), order.getUserId(), order.getOrderNo(), ...); // 插入 t_order_item需确保 order_id 与 t_order 同库 jdbcTemplate.update(INSERT INTO t_order_item (...) VALUES (...), order.getOrderId(), ...); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new RuntimeException(Order creation failed, e); } } }注意JdbcTemplate必须是 Spring 容器注入的、由ShardingSphereDataSource创建的实例否则update()调用不会触发分片路由。transactionManager需配置为DataSourceTransactionManager且setDataSource()指向ShardingSphereDataSource。3. 分片策略不是写死的模运算是业务增长曲线与数据倾斜的动态平衡3.1 从 2 库 4 表到 4 库 8 表不停机扩容的三步迁移法当ds_0库磁盘使用率达 90%你不能简单地把user_id % 2改成user_id % 4——历史数据仍在原库新数据却写入新库查询时user_id1001原属ds_0会被路由到ds_1查不到记录。真实扩容必须分三阶段双写阶段Write Both新老分片规则并存所有写操作同时发往旧库ds_0/1和新库ds_2/3。通过 AOP 切面拦截insert方法根据user_id计算新旧库目标用JdbcTemplate分别执行。数据迁移阶段Migrate用 Flink CDC 或自研脚本将ds_0中user_id % 4 2的数据迁至ds_2ds_1中user_id % 4 3的数据迁至ds_3。迁移期间持续双写确保增量不丢。读切换阶段Read Switch修改分片算法为user_id % 4但先将读流量切 10% 到新库验证查询正确性无误后逐步提升至 100%最后停掉双写逻辑。本资源提供DualWriteAspect.java切面代码通过Around(annotation(org.springframework.transaction.annotation.Transactional))拦截带事务注解的方法解析 SQL 中的user_id值调用shardingAlgorithm.calculateNewTarget()获取新库名执行二次插入。关键参数dual-write.enabledtrue控制开关dual-write.delay-millis100避免主库压力过大。3.2 分表键选择失误导致的“热点库”如何用t_order_no替代create_time做冷热分离某次大促前运营同学要求“查最近 1 小时订单”开发同学将create_time设为分表键并用HOUR(create_time)函数路由。结果大促开始所有订单集中在t_order_0对应 HOUR20该表锁表超时QPS 断崖下跌。根本原因是create_time是时间序列天然存在写热点。正确做法是引入业务维度的“冷热分离”。t_order_no作为分表键本身无热点再配合 TTLTime-To-Live机制将 3 个月前的旧表归档到历史库。具体实现在t_order表增加archive_flag TINYINT DEFAULT 0字段每日凌晨执行存储过程将create_time DATE_SUB(NOW(), INTERVAL 3 MONTH)的数据更新archive_flag1并同步删除物理表查询时应用层判断archive_flag1则走历史库history_ds否则走分片库。本资源的ArchiveScheduler.java包含 Quartz 定时任务每 5 分钟扫描一次t_order批量更新archive_flag并通过ShardingSphere的HintManager强制路由到历史库public void archiveOldOrders() { HintManager hintManager HintManager.getInstance(); hintManager.setDatabaseShardingValue(history_ds); // 强制指定库 hintManager.setTableShardingValue(t_order_history); // 强制指定表 try { jdbcTemplate.update(UPDATE t_order SET archive_flag 1 WHERE create_time ?, DateUtils.addMonths(new Date(), -3)); } finally { hintManager.close(); // 必须关闭否则影响后续请求 } }HintManager是 ShardingSphere 提供的强制路由工具适用于 DML 和 DDL但需注意它基于 ThreadLocal必须在同一线程内close()否则会导致后续请求被错误路由。3.3 分布式 ID 不是雪花算法就万事大吉order_no的全局唯一与可分片性矛盾解法雪花算法Snowflake生成的order_no是 64 位 long可直接用于order_no % 4计算。但业务方要求order_no是字符串如ORD20240520123456789且前缀ORD 时间戳部分需保留可读性。问题来了字符串无法直接模运算若用hashCode()不同 JVM 实例结果不一致导致同一order_no在不同节点路由到不同表。解法是提取字符串中的数字部分做哈希。本资源采用MurmurHash3比String.hashCode()更均匀public class OrderNoShardingAlgorithm implements StandardShardingAlgorithmString { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueString shardingValue) { String orderNo shardingValue.getValue(); // 提取纯数字ORD20240520123456789 → 20240520123456789 String digits orderNo.replaceAll(\\D, ); if (digits.isEmpty()) { throw new IllegalArgumentException(Invalid order_no: orderNo); } // MurmurHash3 生成 64 位 hash取低 32 位避免负数 int hash (int) MurmurHash3.murmurhash3_x86_32(digits.getBytes(), 0, digits.length(), 0); int tableIndex Math.abs(hash) % 4; // 4 张分表 return t_order_ tableIndex; } }MurmurHash3是 ShardingSphere 内置的哈希算法保证相同字符串在任何环境生成相同 hash 值。Math.abs(hash) % 4避免负数取模结果为负Java 中-5 % 4 -1确保表名始终为t_order_0~t_order_3。4. 避坑那些让团队加班到凌晨三点的分库分表血泪现场4.1 现象SQL 执行成功但日志显示Actual SQL: ds_0.t_order_0而预期是ds_1.t_order_2原因分片键未参与 SQL 构建。例如 MyBatis 的Select(SELECT * FROM t_order)未带WHERE条件或Insert的#{userId}参数名与分片策略中配置的sharding-column: user_id不一致少了个下划线。ShardingSphere 在无分片键时默认路由到第一个数据源ds_0的第一个表t_order_0。解决开启 ShardingSphere SQL 日志设置props.sql-showtrue检查日志中Logic SQL是否包含分片键条件确认Param(userId)与 XML 中parameterTypemap的 key 名严格匹配。4.2 现象SELECT COUNT(*) FROM t_order WHERE user_id ?返回结果比实际少原因COUNT(*)是聚合函数ShardingSphere 需将各分片结果汇总。若未配置sql-showtrue日志中看不到Actual SQL的广播执行过程更隐蔽的是当某个分片库宕机ShardingSphere 默认跳过该分片只返回其他分片的COUNT和导致总数偏小。解决在application.yml中添加props.check-table-metadata-enabledtrue启动时校验所有分片表结构一致性生产环境必须配置props.executor-size16线程池大小避免因线程不足导致分片查询超时被跳过。4.3 现象Transactional方法内调用另一个Transactional方法子方法事务不生效原因Spring 事务基于代理Proxy内部方法调用this.methodB()不经过代理对象事务注解失效。当methodA()插入t_ordermethodB()插入t_order_item两者分片不同methodB()的操作脱离事务。解决将methodB()提取为独立 Service并通过Autowired注入自身用this.serviceB.methodB()调用或改用TransactionTemplate手动控制子事务边界。4.4 现象INSERT INTO t_order SELECT ... FROM t_order_item报错Cannot route any table for table t_order_item原因ShardingSphere 5.3.2 不支持INSERT ... SELECT跨分片表。SELECT的t_order_item表按order_id分片但INSERT的t_order按user_id分片两者的分片键不匹配SQL 解析器无法确定目标分片。解决拆分为两步先SELECT获取数据到内存再循环INSERT。虽牺牲性能但保证数据一致性。本资源的BatchOrderImporter.java提供分页SELECT 批量INSERT模板每批 100 条避免 OOM。4.5 现象应用启动时报java.lang.NoClassDefFoundError: org/apache/shardingsphere/infra/route/context/RouteContext原因ShardingSphere-JDBC 5.3.2 与 Spring Boot 2.7.x 的spring-boot-starter-jdbc版本冲突。ShardingSphere 依赖shardingsphere-jdbc-core-spring-boot-starter若项目中又引入了spring-boot-starter-data-jpa其传递依赖的hibernate-core会加载旧版javax.persistence与 ShardingSphere 的jakarta.persistence冲突。解决在pom.xml中排除hibernate-core的javax.persistence依赖并强制指定shardingsphere-jdbc-core-spring-boot-starter版本dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId exclusions exclusion groupIdorg.hibernate/groupId artifactIdhibernate-core/artifactId /exclusion /exclusions /dependency dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version /dependency5. 日志即真相用三行配置打开 ShardingSphere 的黑匣子让每次 SQL 路由都可追溯5.1 开启 SQL 解析与路由全链路日志不只是Logic SQL更要Actual SQL和Route UnitsShardingSphere 默认日志级别为INFO仅输出Logic SQL逻辑 SQL和简单路由结果。要真正定位问题必须开启DEBUG级别并配置sql-show和executor相关属性。本资源的logback-spring.xml已预置关键配置!-- logback-spring.xml -- logger nameorg.apache.shardingsphere levelDEBUG additivityfalse appender-ref refCONSOLE/ /logger logger nameorg.apache.shardingsphere.infra.route.engine.impl.StandardRoutingEngine levelDEBUG additivityfalse appender-ref refCONSOLE/ /logger同时在application.yml中启用spring: shardingsphere: props: sql-show: true # 显示 Logic SQL 和 Actual SQL executor-size: 16 # 设置线程池大小避免路由超时 check-table-metadata-enabled: true # 启动时校验分片表结构开启后控制台将输出类似内容[INFO] Logic SQL: SELECT * FROM t_order WHERE user_id ? AND order_no ? [INFO] SQLStatement: SelectStatementContext [DEBUG] Route context: RouteContext{routeUnits[RouteUnit{dataSourceNameds_1, tableUnitTableUnit{logicTableNamet_order, actualTableNamet_order_2}}]} [INFO] Actual SQL: ds_1 ::: SELECT * FROM t_order_2 WHERE user_id 1001 AND order_no ORD20240520123456789这里RouteContext明确告诉你user_id1001路由到ds_1order_no的哈希值决定表为t_order_2。若RouteUnits为空则说明分片键未被识别若dataSourceName与预期不符检查user_id % 2计算是否正确。5.2 自定义日志增强给每条 SQL 打上“请求 ID”和“分片键值”水印默认日志无法关联业务请求。我们在ShardingSphere的SQLExecutionHook扩展点注入自定义钩子在 SQL 执行前后打印上下文Component public class TraceableSQLExecutionHook implements SQLExecutionHook { private static final Logger LOGGER LoggerFactory.getLogger(TraceableSQLExecutionHook.class); Override public void beforeExecute(String sql, ListObject parameters, String dataSourceName, String actualTableName) { String traceId MDC.get(traceId); String shardKey getShardKeyFromParameters(parameters); // 从 parameters 提取 user_id/order_no LOGGER.debug([TRACE {}] Before execute: {} | Params: {} | Target: {}.{}, traceId, sql, parameters, dataSourceName, actualTableName); } private String getShardKeyFromParameters(ListObject params) { if (params.size() 2 params.get(0) instanceof Number) { return user_id params.get(0); } return unknown; } }配合 Spring Cloud Sleuth 的MDC每条 SQL 日志自动携带traceId可在 ELK 中按traceId聚合整个请求的全部 SQL 路由路径快速定位“为什么这个请求查不到数据”。5.3 用HintManager做灰度验证不改代码临时强制路由到新分片上线新分片策略前需小流量验证。HintManager可在不修改业务代码的情况下通过 HTTP Header 注入路由指令。我们在网关层如 Spring Cloud Gateway添加过滤器Component public class HintRoutingFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String hintDb exchange.getRequest().getHeaders().getFirst(X-Sharding-DB); String hintTable exchange.getRequest().getHeaders().getFirst(X-Sharding-TABLE); if (hintDb ! null || hintTable ! null) { HintManager hintManager HintManager.getInstance(); if (hintDb ! null) hintManager.setDatabaseShardingValue(hintDb); if (hintTable ! null) hintManager.setTableShardingValue(hintTable); } return chain.filter(exchange); } }测试时发送请求curl -H X-Sharding-DB: ds_2 -H X-Sharding-TABLE: t_order_5 http://api/order/1001即可强制将user_id1001的请求路由到ds_2.t_order_5验证新分片逻辑是否正确无需发布新版本。从那以后我每次上线分片策略变更都强制走一遍HintManager灰度验证 全链路日志比对哪怕多花 2 小时。因为线上修一个分片路由 bug 的成本远高于提前 2 小时发现它。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑