ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ShardingSphere-JDBC读写分离实战:从架构选型到踩坑复盘

2026/10/5 8:10:06 拓冰建站 浏览量
ShardingSphere-JDBC读写分离实战:从架构选型到踩坑复盘 最近在给一个订单系统做数据库优化单库单表的压力已经很明显了高峰时段主库CPU能冲到80%以上慢查询日志里开始频繁出现熟悉的面孔。在设计改造方案时我选了ShardingSphere-JDBC来落地读写分离整体跑下来效果不错前前后后也踩了不少坑。这篇把我从方案选型到落地实操的核心内容整理出来包括路由机制、配置细节、主从延迟处理和版本迁移陷阱希望能帮你少走弯路。本文主要围绕三个问题展开为什么需要读写分离、ShardingSphere-JDBC怎么配置才能跑起来、线上环境有哪些坑是文档里不会告诉你的。适合正在做Java应用数据库优化、想在不引入独立中间件的情况下实现读写拆分的团队参考。1. 读写分离的需求背景与方案选型1.1 单库单表的压力到底出在哪儿大部分业务系统上线初期是单库单表后期性能瓶颈通常会集中在读请求上。我见过一个典型的电商订单库读写比例能到7:3甚至8:2更极端的报表类业务能到9:1。主库既要处理事务写操作又要扛住大量查询InnoDB的锁竞争和缓冲池压力都会随之上升。尤其到了大促或者活动场景一个热门商品的查询接口流量可以瞬间翻几倍主库CPU跟着飙起来紧接着就是连接数被打满、应用侧报连接超时。解决这类问题通常路径是加缓存、做读写分离、然后才是分库分表。缓存适合抗热点读但缓存命中率不稳定穿透时流量照样打到数据库。读写分离则是从架构层面把读流量从主库上卸掉——主库只负责写从库通过binlog复制同步数据负责读。这套方案实施成本低、对业务代码侵入小是第二优先级非常值得做的事。1.2 为什么选ShardingSphere-JDBC而不是独立中间件业内做读写分离的路径有几种应用层自己写路由、部署独立数据库中间件比如MyCat、或者嵌入客户端组件比如ShardingSphere-JDBC。三者各有适用场景我最终选客户端组件核心原因是它在Java项目里接入成本最低不用额外维护一套服务。方案部署形态代码改动运维成本适合场景应用层手动路由无每个DAO都要改无简单业务、从库数量少独立中间件独立服务改连接串高需要监控和扩容多语言团队、集中管理ShardingSphere-JDBC应用内嵌JAR改数据源配置低跟着应用走Java技术栈、轻量落地应用层手动路由最大的问题是容易漏新增的查询方法如果忘记走读库流量就悄悄落在了主库时间久了主库压力还是上不去下不来。独立中间件则多了一层网络转发链路变长还引入了中间件本身的运维复杂度。ShardingSphere-JDBC直接在JDBC层拦截SQL业务代码里还是用原来的DataSource、原来MyBatis的Mapper接口只需把数据源替换成ShardingSphere包装过的逻辑数据源读写路由的活框架帮你干完了。我用的版本是5.3.x这也是目前5.x系列里比较稳的版本。如果你刚接触建议直接上5.x4.x的配置结构和API差异很大下面会专门讲版本迁移的坑。2. 环境准备主从复制与工程接入2.1 MySQL主从复制快速打通ShardingSphere-JDBC本身不负责主从数据同步它只做读写流量的路由。所以第一步是先把MySQL主从环境准备好。这里以MySQL 8.0为例GTID模式配置。主库的my.cnf需要开启binlog[mysqld] server-id 1 log-bin mysql-bin binlog_format ROW gtid_mode ON enforce_gtid_consistency ON从库的my.cnf设置不同的server-id通常还会开read_only 1防止从库被误写。主库创建复制账号CREATE USER repl% IDENTIFIED WITH mysql_native_password BY Repl123; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;从库执行CHANGE MASTER指令CHANGE MASTER TO MASTER_HOST 192.168.1.10, MASTER_USER repl, MASTER_PASSWORD Repl123, MASTER_AUTO_POSITION 1; START SLAVE; SHOW SLAVE STATUS\G看到Slave_IO_Running: Yes和Slave_SQL_Running: Yes两个线程都在正常运行主从链路就通了。这块建议单独用压测或者造数据验证一下延迟情况有个20秒的基准数据心里有底。2.2 引入依赖与最小化配置ShardingSphere-JDBC不是独立服务它以一个JAR包的形式嵌入应用因此不需要额外部署。Maven项目引入依赖即可dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version /dependency如果是纯Spring项目用shardingsphere-jdbc-core即可。引入依赖后做一份最小配置这个配置定义了逻辑数据源order_rw写数据源指向primary读数据源列表是replica_0和replica_1读流量走轮询算法。spring: shardingsphere: datasource: names: primary,replica-0,replica-1 primary: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 replica-0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 replica-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.12:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 rules: readwrite-splitting: >load-balancers: readwrite_ds_weight: type: WEIGHT props: replica-0: 2 replica-1: 1这个配置表示读流量按2:1分配到replica-0和replica-1。权重适合从库机器配置不对称的场景比如一个库是SSD一个是普通SATA或者一个节点上还跑了其他业务。我实际中习惯先用轮询跑一阵看看监控里各节点负载差异再决定要不要加权重因为权重毕竟是静态配置如果流量模型本身波动大调权重的收益有限。3.3 SQL路由判定与事务级主库接管路由的核心逻辑并不复杂ShardingSphere-JDBC解析SQL语句的第一个关键字SELECT走读库INSERT、UPDATE、DELETE走主库DDL也走主库。再往深处看一步真正决定路由归属的不只是SQL类型还包括当前是否处于事务中。当你没有手动开启事务时每条SQL独立判断选择走主库还是某个从库。可一旦代码里开启了事务比如Spring的Transactional情况就变了事务边界内的所有SQL都会被强制路由到主库即使你执行的是SELECT。这个机制是安全兜底因为在事务隔离级别下如果读到数据从从库来主库刚写入的数据还没同步过去就可能出现不可重复读甚至读到不存在的数据。还有一个容易被忽略的点Transactional(readOnly true)的只读事务在ShardingSphere-JDBC里默认也会走主库。很多人以为只读事务应该读从库但框架的判定逻辑就是事务开启则主库路由并不区分你是否只读。所以如果单纯想读从库别加事务注解MyBatis默认就是每条SQL一个独立连接路由才能发挥效果。4. 实践踩坑延迟、强制路由与版本迁移4.1 写后立即读的防延迟方案读写分离落地后第一个暴露出的问题就是主从延迟。我这边遇到的具体场景是用户下单支付成功后页面跳转到订单详情页详情页查询走从库。如果主库刚commit的订单数据还没同步到从库用户看到的详情页就提示订单不存在这种体验基本属于事故级别。解决这个问题的思路不是消灭延迟因为MySQL主从延迟受网络、从库负载、大事务影响不可能完全归零。更务实的做法是区分业务场景对于写后必须能读到的强一致读场景强制这条查询走主库对于浏览历史订单、查询数据报表这类允许最终一致性的场景照常走从库。在ShardingSphere-JDBC里用HintManager可以强制某条SQL走主库try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); // 这里执行订单详情查询强制走主库 Order order orderMapper.selectById(orderId); }我在订单创建接口里就用了这个方式保证用户下单跳到详情页时数据一定可见。注意HintManager要放在try-with-resources块中用完必须关掉它基于ThreadLocal实现不关闭的话当前线程后续所有的查询都会一直走主库主库压力会被莫名放大。4.2 HintManager的坑作用域与释放HintManager的使用有几个容易踩雷的细节我一个个说。第一它只对当前线程生效。如果你在Service方法里设置了写路由然后异步线程里执行查询那个异步线程是不受影响的。第二一个线程同时只能存在一个HintManager实例如果你在同一个线程里创建了两次而没有关闭第一个第二次getInstance()会直接抛出异常。第三try-with-resources关闭不只是推荐而是强烈建议——我线上排查过一个主库CPU飙高的故障最后定位到是某位同事在代码里调了setWriteRouteOnly()但没close线程池里这个线程被复用后后续所有SQL都被强制路由到主库从库的读流量几乎归零。所以我的实践准则是强制路由的生命周期越短越好最好精确到一条SQL绝不在循环体外面长时间开着HintManager。4.3 4.x到5.x的配置迁移要点如果你的项目之前用的是ShardingSphere 4.x升级5.x时配置结构变化很大不能直接复制粘贴。最核心的变更是读写分离的规则名称从!MASTER_SLAVE换成了!READWRITE_SPLITTING对应的数据源属性也从masterDataSourceName、slaveDataSourceNames改成了writeDataSourceName、readDataSourceNames。配置项4.x5.x规则关键字!MASTER_SLAVE!READWRITE_SPLITTING主数据源属性masterDataSourceNamewriteDataSourceName从数据源属性slaveDataSourceNamesreadDataSourceNames负载均衡算法属性loadBalanceAlgorithmTypeloadBalancerName4.x里负载均衡算法是直接写在规则内部的loadBalanceAlgorithmType字段5.x则拆出了独立的load-balancers配置段可复用性更强但也意味着改动范围更大。我遇到不少朋友升级后启动报错大多是因为把4.x的配置原封不动搬过来字段对不上导致的。如果你也在做这个升级建议先用5.x的官方示例配置跑通一个最小demo再逐步迁移生产配置。4.4 连接池参数与主从差异化调优读写分离后主库和从库的流量特征完全不同连接池参数理应差异化设置。我见过很多团队新老配置一套参数包打天下导致从库连接过多浪费、主库连接又不够。实践中主库maximum-pool-size可以控制在10到20因为主库只需要处理写事务和少量强制走主库的读连接多了反而增大锁竞争。从库则可以按读流量规模调大我常用的从库maximum-pool-size是30到50。HikariCP几个参数里minimum-idle和maximum-pool-size的大小对并发影响最明显。还有个容易被忽略的JDBC参数jdbc-url里建议加上rewriteBatchedStatementstrue批量INSERT的写入性能会有明显提升这个在主库场景尤其重要。至于连接超时时间建议从库的connection-timeout稍微放宽到5000毫秒以上避免从库瞬时负载高时获取连接失败。5. 日志观测与实测验证5.1 sql-show到底能看到什么props.sql-show: true是调试阶段最有用的配置没有之一。开启后ShardingSphere-JDBC会把每条SQL的路由信息打印到日志里。下面是我截取的一段真实日志Logic SQL: SELECT * FROM t_order WHERE order_id 1001 Actual SQL: replica-0 ::: SELECT * FROM t_order WHERE order_id 1001Logic SQL是应用实际执行的SQLActual SQL前面是实际路由到的物理数据源。通过这段日志可以快速判断路由是否符合预期如果一条INSERT的Actual SQL前缀是primary没问题如果一条SELECT在开启事务的Service方法里出现前缀也是primary说明事务内读主库的机制在生效。线上环境建议把sql-show关掉它会影响性能且日志量很大。我更推荐的方式是依赖监控系统采集数据源层的指标从库和主库各自的连接数、QPS、慢查询数量都要分开看才能真正判断路由是否均衡。5.2 读写分离后带来了多少收益最后说下我这边实测的数据供参考。优化前单库直连峰值时主库QPS约3500CPU使用率接近80%。接入ShardingSphere-JDBC做一主两从读写分离后压测阶段读QPS提升到接近10000主库CPU稳定在20%到30%从库CPU在40%左右浮动。这个结果符合预期因为读流量被分摊到了两个从库主库从繁重的查询中解放出来写事务的处理能力也有了余量。需要说明的是读写分离提升的是读吞吐上限和主库稳定性它不会降低单个查询的延迟也解决不了分库分表才能解决的问题。如果某天你的读QPS再翻几倍接下来要思考的是加从库节点或者对核心表做分片规划了。最后分享一个我个人的配置习惯无论从库数量多少我都坚持在readwrite-splitting里把load-balancer-name配置为显式的负载均衡器而不是依赖框架某个隐含默认值。项目后期如果调整从库节点或者切换算法配置一处修改即可生效排查问题的时候逻辑也更清晰。ShardingSphere-JDBC在5.x之后整体稳定性和路由性能都相当能打只要把延迟控制、事务边界、Hint释放这几件大事守住读写分离落地并没有想象中那么复杂。