
简介本资源是一份基于SpringBoot框架集成Sharding-JDBC实现读写分离与分库分表的完整实践工程面向Java后端开发者及数据库性能优化学习者解决高并发场景下单库单表性能瓶颈与扩展性难题。压缩包共14个文件含5个YAML配置文件承载数据源、分片规则与读写分离策略、4个Java类含Sharding配置类与核心业务逻辑、1个XML依赖声明文件以及CMD启动脚本、.gitignore、Maven包装器和README说明文档结构清晰、开箱即用总大小仅17KB轻量易导入。已有183人学习下载适合希望快速掌握Sharding-JDBC在SpringBoot中落地细节的中级开发者。读者可直接复用配置模板、理解分片键设计逻辑、参考多数据源注册方式并结合代码与配置对照学习分库分表的实际映射关系与透明化访问机制。1. 为什么你配置了 Sharding-JDBC 却没真正实现读写分离和分库分表很多团队在 Spring Boot 项目里引入sharding-jdbc-spring-boot-starter加了几行spring.shardingsphere.rules配置跑通了单表查询就以为“分库分表读写分离”已上线。结果压测一上主库 CPU 持续 95%从库连接池空闲线上慢查日志里全是跨库 JOIN 和全路由查询更隐蔽的是事务中执行INSERT后紧跟SELECT却从从库读到了旧数据——这不是 Sharding-JDBC 的 Bug而是对「逻辑库/表」与「物理库/表」映射关系、路由上下文生命周期、以及读写分离策略触发条件的误判。本文不讲概念复读只聚焦真实落地用 Sharding-JDBC 3.1.0兼容 Spring Boot 2.3.x或 4.1.1适配 Spring Boot 2.4版本在 MariaDB 10.6 主从集群上把读写分离的流量切分、分库分表的分片键路由、以及二者叠加时的优先级冲突全部拆解成可验证、可调试、可回滚的具体操作。适合已部署 MariaDB 主从、正卡在「配置写了但流量不走预期路径」阶段的后端工程师。2. 读写分离不是开关是路由策略的显式声明与上下文绑定Sharding-JDBC 的读写分离能力并非自动启用它依赖于明确的MasterSlaveRuleConfiguration声明并且必须与分片规则协同注册。常见错误是只配了master-slave-rules却没在sharding-rules中引用导致所有 SQL 默认走 master。真正的起点是理解其路由决策链SQL 解析 → 分片键提取 → 分库分表路由 → 读写分离路由仅当无分片键或强制主库读时跳过。因此读写分离生效的前提是该 SQL 不触发分库分表路由如查询未带分片键或虽触发分片路由但目标节点存在从库副本。2.1 在 Spring Boot 中声明 Master-Slave 数据源拓扑需先定义物理数据源 Bean再通过MasterSlaveDataSource封装逻辑数据源。注意Sharding-JDBC 3.x 使用MasterSlaveRuleConfiguration4.x 已废弃该类改用ReadwriteSplittingRuleConfiguration。以下以 4.1.1 版本为例兼容 Spring Boot 2.4# application.yml spring: shardingsphere: # 必须关闭自动创建表否则启动时会向所有从库发 DDL从库不可写 props: sql-show: true check-table-metadata-enabled: false datasource: names: ds-master,ds-slave-0,ds-slave-1 ds-master: driver-class-name: org.mariadb.jdbc.Driver jdbc-url: jdbc:mariadb://192.168.1.10:3306/mydb?useSSLfalseserverTimezoneUTC username: root password: master123 ds-slave-0: driver-class-name: org.mariadb.jdbc.Driver jdbc-url: jdbc:mariadb://192.168.1.11:3306/mydb?useSSLfalseserverTimezoneUTC username: root password: slave123 ds-slave-1: driver-class-name: org.mariadb.jdbc.Driver jdbc-url: jdbc:mariadb://192.168.1.12:3306/mydb?useSSLfalseserverTimezoneUTC username: root password: slave123 rules: - !READWRITE_SPLITTING dataSources: pr_ds: write-data-source-name: ds-master read-data-source-names: [ds-slave-0, ds-slave-1] load-balancer-name: round_robin loadBalancers: round_robin: type: ROUND_ROBIN提示pr_ds是逻辑数据源名后续分库分表规则中的actual-data-nodes必须引用此名如pr_ds.t_order_${0..1}而非物理数据源名ds-master。若此处写错分片路由将找不到底层数据源直接抛No database route info异常。2.2 验证读写分离是否真正生效三步定位法仅靠sql-show: true日志不够需结合数据库连接状态确认。执行以下命令验证# 步骤1在 MariaDB 主库执行查看当前活跃连接来源 SELECT host, user, command, state, info FROM information_schema.processlist WHERE user root AND command ! Sleep; # 步骤2在应用中执行一条无分片键的简单查询如 SELECT COUNT(*) FROM t_order # 观察日志中是否出现 Actual SQL: ds-slave-0 ::: SELECT COUNT(*) FROM t_order # 若日志显示 ds-master则说明路由失败 # 步骤3强制走主库测试对比 # 在代码中使用 HintSharding-JDBC 4.x HintManager hintManager HintManager.getInstance(); hintManager.setWriteRouteOnly(); // 强制所有后续 SQL 走 master orderMapper.selectCount(); // 此次查询必走主库 hintManager.close();注意setWriteRouteOnly()仅对当前线程有效且必须在 DAO 方法调用前设置。若在异步线程如Async中使用需手动传递HintManager上下文否则无效。3. 分库分表的核心是分片键与分片算法的强约束不是配置越多越安全分库分表失效的最常见原因是分片键sharding key未出现在 SQL 的WHERE条件中或分片算法返回了非法分片值如负数、超出预设范围。Sharding-JDBC 不会自动降级为全库路由而是直接报错Can not find table rule for logic table t_order。因此分片设计必须前置验证业务查询是否能天然携带分片键分片算法是否覆盖所有可能取值3.1 基于用户 ID 的分库 订单时间分表双层分片实战假设业务要求按user_id分 4 个库ds_0 ~ ds_3每个库内按create_time年份分 3 张表t_order_2023, t_order_2024, t_order_2025。需定义两级分片规则# application.yml 续写 rules: - !SHARDING tables: t_order: actual-data-nodes: pr_ds.t_order_${0..3}.${2023..2025} table-strategy: standard: sharding-column: create_time 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 取模分库 t_order_table_inline: type: INLINE props: algorithm-expression: t_order_${(new java.time.format.DateTimeFormatterBuilder() .appendPattern(yyyy).toFormatter().format(creationTime)).toInt() % 3 2023} # 注此表达式为示意实际需自定义 Java 类实现时间解析 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 123关键参数说明actual-data-nodes: pr_ds.t_order_${0..3}.${2023..2025}中pr_ds必须与 2.1 节定义的读写分离逻辑数据源名一致algorithm-expression中的user_id和create_time必须与实体类字段名、SQL 参数名完全匹配区分大小写时间分片若需精确到年强烈建议自定义PreciseShardingAlgorithm实现避免 Inline 表达式中复杂日期计算出错。3.2 自定义时间分片算法解决 Inline 表达式无法处理的场景当分片逻辑涉及日期解析、时区转换或非线性映射时Inline 表达式力不从心。以下为PreciseShardingAlgorithm实现示例适配 Sharding-JDBC 4.1.1Component public class OrderTimeShardingAlgorithm implements PreciseShardingAlgorithmDate { private static final String[] TABLES {t_order_2023, t_order_2024, t_order_2025}; Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueDate shardingValue) { Date createTime shardingValue.getValue(); int year LocalDateTime.ofInstant(createTime.toInstant(), ZoneId.systemDefault()) .getYear(); // 映射到预定义表名2023→t_order_2023, 2024→t_order_2024, 其他年份默认到2025 String targetTable t_order_ Math.min(Math.max(year, 2023), 2025); if (!availableTargetNames.contains(targetTable)) { throw new UnsupportedOperationException(Table targetTable not in available list: availableTargetNames); } return targetTable; } }在 YAML 中引用该算法sharding-algorithms: t_order_table_custom: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.example.sharding.OrderTimeShardingAlgorithm注意availableTargetNames是 Sharding-JDBC 传入的、当前分片键对应的所有合法表名集合由actual-data-nodes解析得出。算法必须返回其中一项否则抛异常。此机制强制校验分片结果的有效性避免写入不存在的表。4. 读写分离与分库分表叠加时的路由优先级与事务陷阱当同时启用读写分离和分库分表时Sharding-JDBC 的路由决策存在明确优先级分库分表路由 读写分离路由。这意味着若一条 SQL 带有分片键如WHERE user_id 123则先根据user_id定位到具体库如ds_1再在ds_1的主从拓扑中按读写分离策略决定走ds_1的 master 还是 slave。但这一流程在事务中会被打破——任何在事务内执行的写操作会将当前连接绑定到主库后续同事务内的读操作也强制走主库以保证事务一致性。这是正确行为但常被误认为“读写分离失效”。4.1 事务内读写分离失效的验证与规避方案创建一个典型事务方法Transactional public void createOrderAndQuery(Long userId, String orderNo) { Order order new Order(); order.setUserId(userId); order.setOrderNo(orderNo); order.setCreateTime(new Date()); orderMapper.insert(order); // 写操作绑定主库连接 // 此处查询理论上可走从库但因在事务中强制走同一主库连接 Order queried orderMapper.selectByOrderNo(orderNo); }验证方式开启sql-show观察日志中两次 SQL 的Actual SQL是否都指向ds-master。若确认是此行为需评估业务是否允许方案A推荐读写分离 最终一致性将查询拆出事务用Transactional(propagation Propagation.NOT_SUPPORTED)标记查询方法使其在无事务上下文中执行从而启用读写分离Transactional public void createOrder(Long userId, String orderNo) { // ... insert logic } Transactional(propagation Propagation.NOT_SUPPORTED) public Order queryAfterCreate(String orderNo) { return orderMapper.selectByOrderNo(orderNo); // 此时可走从库 }方案BHint 强制从库读慎用在事务内手动指定从库但需确保业务能容忍短暂不一致Transactional public void createOrderAndStaleRead(Long userId, String orderNo) { orderMapper.insert(...); HintManager hintManager HintManager.getInstance(); hintManager.setReadDataSourceOnly(); // 强制后续读走从库 Order stale orderMapper.selectByOrderNo(orderNo); // 可能读到旧数据 hintManager.close(); }4.2 分库分表下的跨库 JOIN 与聚合查询为什么必须禁止Sharding-JDBC 对JOIN、GROUP BY、ORDER BY等操作的支持有限。例如SELECT o.*, u.name FROM t_order o JOIN t_user u ON o.user_id u.id若t_user未分库而t_order已分库则u.id无法路由到单一库Sharding-JDBC 会发起全库广播查询性能灾难。解决方案只有两个业务层拆解先查t_user获取u.name再用user_id查询分片后的t_order冗余字段在t_order表中增加user_name字段写入时同步填充查询时避免 JOIN。验证跨库查询是否发生开启sql-show后若日志中出现Actual SQL: ds-master ::: ...和Actual SQL: ds-slave-0 ::: ...多条记录且Logic SQL中含JOIN即表明已触发全库路由。此时应立即重构查询逻辑。5. 生产环境必须做的三件事连接池隔离、分片键监控、从库延迟感知配置完成不等于高可用。MariaDB 主从复制存在天然延迟Seconds_Behind_Master若从库延迟 5 秒读写分离会返回过期数据。Sharding-JDBC 本身不提供延迟感知需结合外部手段。5.1 HikariCP 连接池隔离避免主从连接混用不同数据源必须使用独立连接池否则连接可能被复用到错误节点。在application.yml中显式配置spring: datasource: hikari: # 主库连接池 ds-master: maximum-pool-size: 20 connection-timeout: 30000 # 从库连接池独立配置 ds-slave-0: maximum-pool-size: 15 connection-timeout: 30000 ds-slave-1: maximum-pool-size: 15 connection-timeout: 30000提示从库连接池maximum-pool-size应小于主库因读流量通常大于写流量但单个从库承载能力有限需根据压测结果调整。5.2 分片键缺失告警用 AOP 拦截无分片键的查询在 Controller 或 Service 层对关键查询方法进行 AOP 切面检查参数中是否包含分片键。若缺失且方法名含List、Page等字样记录告警日志Aspect Component public class ShardingKeyCheckAspect { Around(annotation(org.springframework.web.bind.annotation.GetMapping) args(..,userId,..)) public Object checkUserIdInGet(ProceedingJoinPoint joinPoint, Long userId) throws Throwable { if (userId null || userId 0) { log.warn(GET method {} called without valid user_id, may trigger full routing!, joinPoint.getSignature().getName()); } return joinPoint.proceed(); } }5.3 从库延迟主动探测集成到健康检查端点编写一个HealthIndicator定期查询各从库的Seconds_Behind_MasterComponent public class MariaDBReplicationHealthIndicator implements HealthIndicator { Autowired private JdbcTemplate slave0JdbcTemplate; // 指向 ds-slave-0 Override public Health health() { try { Integer delay slave0JdbcTemplate.queryForObject( SHOW SLAVE STATUS, (rs, rowNum) - rs.getInt(Seconds_Behind_Master) ); if (delay null || delay 30) { // 延迟超30秒视为不健康 return Health.down().withDetail(replication_delay_seconds, delay).build(); } return Health.up().build(); } catch (Exception e) { return Health.down().withException(e).build(); } } }暴露/actuator/health端点运维可通过 Prometheus 抓取该指标延迟超标时自动告警并临时关闭读写分离。最后验证部署后执行curl http://localhost:8080/actuator/health确认status为UP且details中replication_delay_seconds值合理通常为 0 或个位数。这标志着 MariaDB 读写分离与分库分表已在生产就绪状态稳定运行。本文还有配套的精品资源点击获取