ShardingSphere选型与分库分表读写分离实战指南

ShardingSphere选型与分库分表读写分离实战指南 做中间件选型这么多年我有个挺固执的习惯先别对着功能清单猛看先想清楚自己站在哪、要去哪。ShardingSphere 能火是因为它把分库分表这个原本很“侵入”的事变成了两种可选姿势——ShardingSphere-JDBC 和 ShardingSphere-Proxy。一个是把分片能力塞进应用进程里另一个是起一个独立服务对外透明代理。很多团队卡在第一步到底该选哪个再加上读写分离、分库分表边界这些事搅在一起光方案评审就能开三次会。这篇就把我实际落地过的选型逻辑、分片边界判断和读写分离联动配置整套讲清楚适合正在评估 ShardingSphere、或者已经在用但想优化部署形态的后端同学。1. 两种形态的本质区别JDBC 与 Proxy 到底差在哪1.1 ShardingSphere-JDBC 的工作方式ShardingSphere-JDBC 从名字就能看出来它是 JDBC 层面的增强。应用正常引入依赖、正常配置 DataSource但底层拿到的连接已经被包装过。一条 SQL 进来它会先做 SQL 解析根据配置的分片规则把逻辑表路由到真实节点然后改写 SQL、执行最后把多节点返回的结果做归并再交给应用。关键点在于它运行在你的应用进程里没有额外的一跳网络。所以延迟很低问题定位也直观日志和应用报错天然在一起出问题一把 jstack 就能看到线程在干什么。但它只支持 Java 应用而且连接数是按“实例数 × 分片数 × 连接池大小”累加的服务规模一大数据库这边压力先爆。1.2 ShardingSphere-Proxy 的工作方式Proxy 是一个独立进程对外暴露 MySQL/PostgreSQL 协议端口。你的应用不需要引入任何 ShardingSphere 依赖只需要把 JDBC 连接地址从真实数据库改成 Proxy 地址默认端口是 3307。它对客户端完全透明这是它最大的价值非 Java 应用、老系统、BI 工具甚至命令行 mysql 客户端都能直接连。但天下没有免费的午餐。代价是应用到数据库之间多了一层网络延迟会增加Proxy 自身也需要部署、监控、升级、扩容运维成本是实打实的。还有一个容易被忽略的问题Proxy 需要保证自身高可用不然它挂了所有业务连接全都断。1.3 一张表看懂核心差异维度ShardingSphere-JDBCShardingSphere-Proxy部署位置应用进程内独立进程支持语言仅 Java任意语言/客户端网络延迟低无额外一跳多一跳微秒级差异连接数消耗实例数×分片数×池大小客户端只连 Proxy后端连接池可控侵入性需要改代码、改配置透明改连接地址即可功能完整性全量能力受协议限制个别能力有差异运维成本随应用发布无独立组件需要独立部署、监控、扩缩容适合场景新项目/Java 服务老系统改造/多语言/统一接入层表格只能帮你建立框架真正拍板还得看下面这几个实际问题。2. 选型判断动手之前先回答这几个问题2.1 先看技术栈和改造范围如果团队全是 Java项目是新建的或者老系统你本来就要大改那 ShardingSphere-JDBC 几乎是最优解。依赖打进项目里分片规则跟着配置中心走没有额外组件发布就是普通应用发布心智负担小。如果系统里有 PHP、Go、Python 服务或者有一套你根本不敢动的老业务只想把数据层能力替换掉那就只能走 Proxy。客户端不需要知道分片逻辑连接串一换数据访问路径就变了业务代码一行不用改。我见过不少团队把 Proxy 当“数据网关”用所有新老服务统一走一个入口账号权限、分片规则、读写分离全在这一层收口。2.2 连接数压力这个最容易被忽视这是我在选型评审里见过最多人踩空的点。JDBC 模式下每个应用实例会对每个分片建一个独立连接池。假设你有 8 个分片、50 个服务实例、每个分片连接池上限 50那后端数据库要扛的连接数是 8 × 50 × 50 20000。MySQL 默认 max_connections 才 151你不调参数根本跑不动调了又怕把数据库压垮。Proxy 的连接模型完全不一样。客户端连的是 Proxy后端到每个分片的连接池是 Proxy 统一维护的上限你可以严格控制比如每个分片 50 条8 个分片总共才 400 条后端连接。客户端实例再多后端压力是恒定的。如果你的服务实例数上了几十个量级或者数据库侧经常报 too many connections老老实实考虑 Proxy。2.3 性能敏感度和运维能力也要算进去性能方面不要听网上那些“Proxy 慢到不能用”的夸张说法。实测在正常网络环境下Proxy 多一跳带来的延迟差异通常在亚毫秒到几毫秒之间对绝大多数业务无感。但如果你的链路是那种单次 RT 都要抠到极致的高频接口JDBC 少一跳还是更稳。运维能力是反过来的考量。JDBC 模式没有独立组件自然不用额外运维但每个应用的 ShardingSphere 版本依赖不同升级要跟着应用发布走版本碎片化很常见。Proxy 模式把复杂度集中到一个组件上升级维护是好事也意味着你必须有人真正懂它不然出了问题排障范围反而更大。2.4 我的选型建议直接抄新项目、Java 技术栈、团队对 ShardingSphere 不熟选 JDBC成本最低。存量系统多语言、不想动业务代码选 Proxy。实例规模大、数据库连接数吃紧优先 Proxy或者 JDBC 模式下严格控制连接池上限。大团队想统一数据接入规范Proxy 做统一入口但要在 Proxy 前面再挂负载均衡保证高可用。还有一种很常见的混合形态核心新服务用 JDBC 拿全量能力第三方系统、报表系统走 Proxy。两边用同一套分片规则完全兼容。3. 分库分表的边界到底什么时候该动手3.1 先分清是“库不够用”还是“表不够快”很多人一上来就问“多少数据量要分片”这个问法本身就是错的。分库分表是两个层面的问题触发条件完全不同单表数据量大、查询越来越慢、索引维护成本高、备份耗时长——这是“表”的问题应该分表。数据库连接数打满、写入 QPS 超过单库处理能力、磁盘 IO 到瓶颈——这是“库”的问题应该分库。实操里我一般这么判断单表行数超过 500 万就要开始做准备到 2000 万以上或者超过 20GB基本必须要处理了。但这只是经验值具体还要看你的 InnoDB buffer pool 大小、索引复杂度、查询模式。如果你的表一直在做全表扫描还只有 100 万行那分片救不了你先优化 SQL 才是正路。3.2 分片键选不好后面全是坑分片键是分库分表最核心的决策没有之一。选错了后面所有查询都难受。标准就三条访问频率高、分布均匀、值不可变。拿订单场景举例。如果按 order_id 分片那所有按用户查订单的请求都变成全路由扫描如果按 user_id 分片那单查一笔订单也要带上 user_id 或者做二次路由。没有完美的答案只有符合业务主访问路径的答案。我的原则是优先满足最高频的查询其他查询通过中间件归并或者建立索引来解决。还有一点经常被忽略分片键的值一定不能变。比如用户在表里存了 user_id 做分片哪天产品说“允许用户改 ID”这就是灾难。变更分片键意味着数据要跨节点搬迁成本极高。3.3 分片算法选型取模、哈希与范围ShardingSphere 内置的算法里日常用得最多的是这几个MOD直接对分片键取模适合整型且分布均匀的字段规则简单最容易排查。HASH_MOD先对分片键做哈希再取模适合字符串字段能把分布打散。RANGE / INTERVAL按时间或 ID 区间分片适合归档类、时序类数据但容易产生热点分片。我的建议是业务表优先用 MOD 或 HASH_MOD别用纯 RANGE 做在线交易表。原因很简单区间分片在新数据集中在尾部时热点全压在一个分片上分库分表就白做了。RANGE 更适合日志、流水这种“只写最近、查最近”的场景。3.4 分片数量定多少才不给扩容留隐患分片数量我强烈建议选 2 的幂比如 4、8、16。原因跟扩容有关取模算法下当分片数从 N 翻倍到 2N 时每个旧分片的数据只会拆成两半落到两个新分片上只需要迁移一半数据。如果你当初选了 6 个分片想扩到 12 个逻辑上也成立但想继续扩大到 24 就不太顺了不如一开始就用 2 的幂。另外永远不要指望分片数能经常动态调整。虽然 ShardingSphere 5.x 提供了数据迁移和弹性伸缩能力但生产上扩容仍然是一件高风险的事binlog 同步、灰度切流、数据校验每一步都容易出问题。我见过太多团队一拍脑袋定了 4 个分片结果两年后就撑不住。宁可前期多分一点比如一次到位 16 个甚至 32 个分片也别让自己陷入频繁扩容的泥潭。4. 读写分离联动实战一套配置双能力4.1 联动架构是怎么串起来的分片和读写分离不是两套独立的东西而是可以叠在一层的。ShardingSphere 的逻辑是先把物理库组成一个读写分离的逻辑数据源比如rw_ds分片规则里的actualDataNodes直接引用这个逻辑数据源。这样每个分片后面都自动挂上自己的从库写操作走主库读操作根据负载均衡策略走从库。这条链路你要理解成逻辑数据源读写分离 → 物理节点分片。分片负责把数据拆开读写分离负责让每个分片的读写压力分散。两者各管一层的规则在配置上通过数据源名字关联起来。4.2 ShardingSphere-JDBC 完整配置Spring Boot 方式下面是一份我在生产环境验证过的配置骨架逻辑库rw_ds下面是两个物理分片每个分片配了一个从库spring: shardingsphere: datasource: names: ds-0,ds-0-slave-1,ds-1,ds-1-slave-1 ds-0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/ds_0?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 ds-0-slave-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/ds_0?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 ds-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.20:3306/ds_1?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 ds-1-slave-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.21:3306/ds_1?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 rules: readwrite-splitting: >authority: users: - user: root% password: root - user: sharding password: sharding privilege: type: ALL_PERMITTED props: max-connections-size-per-query: 1 sql-show: trueconfig-sharding.yaml里配置数据源和规则结构和上面 JDBC 版本类似只是字段名变成驼峰dataSources: ds_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.10:3306/ds_0 username: root password: 123456 ds_0_slave_1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.11:3306/ds_0 username: root password: 123456 ds_1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.20:3306/ds_1 username: root password: 123456 ds_1_slave_1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.21:3306/ds_1 username: root password: 123456 rules: - !READWRITE_SPLITTING dataSourceGroups: rw_ds_0: writeDataSourceName: ds_0 readDataSourceNames: - ds_0_slave_1 loadBalancerName: round_robin rw_ds_1: writeDataSourceName: ds_1 readDataSourceNames: - ds_1_slave_1 loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBIN - !SHARDING tables: t_order: actualDataNodes: rw_ds_${0..1}.t_order_${0..1} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_inline keyGenerateStrategy: column: order_id keyGeneratorName: snowflake shardingAlgorithms: t_order_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 2} keyGenerators: snowflake: type: SNOWFLAKE配置好后启动 Proxy验证也很简单bin/start.sh mysql -h127.0.0.1 -P3307 -uroot -proot进到命令行里执行SHOW TABLES;你会看到逻辑表t_order对它执行查询、插入Proxy 会自动路由到对应分片。要注意的是Proxy 模式下执行的 DDL 会广播到所有分片所以建表改表不需要你手动去每个库执行。4.4 强制主库路由与事务联动细节读写分离联动之后最容易踩的坑是主从延迟。MySQL 主从同步默认是异步的写入主库的数据可能几百毫秒后才到从库。如果你刚插入一条订单立刻按订单号去查被负载均衡路由到从库就查不到。ShardingSphere 有一条自动保护规则如果当前线程开启了事务事务内的读操作会强制走主库。所以最简单粗暴的解决办法就是给这类“写完马上读”的接口包上事务。但事务不是银弹事务里全走主库会让主库压力变大。更精准的做法是使用 Hint 强制路由。JDBC 模式下用 HintManagerpublic OrderDO getOrderAfterCreate(Long orderId) { try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); return orderMapper.selectById(orderId); } }这段代码的意思是这次查询强制走写库。只在必要的时候用别满代码写这个否则从库就白挂了。业务上还有个更优雅的方案把“写后立查”改成异步刷新前端先展示上一次数据后台等同步完成后再更新但这对产品设计有要求不一定每个团队都做得了。另外一个细节是执行引擎的配置。Proxy 模式下如果客户端的 MySQL JDBC 驱动开了服务端预处理某些版本会偶发prepared statement相关异常处理方式是在 JDBC URL 上加参数jdbc-url: jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmtsfalse这个坑在旧版本比较常见升级到 5.x 后期版本会好很多但遇到了别慌先从这个参数查起。5. 实战翻车现场常见问题与排查实录5.1 跨分片查询网络、JOIN 与广播表分片之后最明显的变化是你不能再像单库那样随便 JOIN 了。两张表如果分片键不一致JOIN 会发生笛卡尔积路由比如 t_order 和 t_order_item 各分 4 片JOIN 一次最多可能产生 16 次跨节点查询性能直接崩。解法有两个。第一把经常 JOIN 的两张表配置成绑定表告诉 ShardingSphere 它们的分片键一致JOIN 时只路由到对应的分片组- !SHARDING bindingTables: - t_order, t_order_item broadcastTables: - t_dict第二对于字典类、配置类的小表设置成广播表每个分片都存一份全量数据JOIN 时本地就能完成。广播表的数据量必须小每次更新都会广播到所有分片写放大明显适合几百行级别的表。还有一种常见场景是“按用户查订单列表”因为订单表按 order_id 分片你没法精准路由只能全库扫描。我的建议是引入一个多级索引表订单表和用户关系单独存一张绑定表先查绑定表拿到订单所在分片再精准查询。这算是最简单也最稳的异地路由方案。5.2 主从延迟读到旧数据怎么办主从延迟没办法彻底消灭只能缓解。我常用的三板斧事务内读自动走主库这是配置层面的兜底。关键接口用 HintManager 强制走主库。延迟敏感的数据写入后前端做短暂 loading给同步留窗口。还有一个容易忽略的点从库的负载均衡算法不是越花哨越好。默认的 ROUND_ROBIN 和 RANDOM 足够用WEIGHT 适合主从机器配置不一致的场景。不要为了“均匀”上太复杂的策略出问题反而难排查。5.3 分布式事务到底要不要上分片之后原来在一个库里的多个表更新现在可能落在不同分片。默认的 LOCAL 事务没办法保证跨分片的一致性回滚只能回滚当前分片其他分片就脏了。ShardingSphere 支持两种分布式事务方案XA 和 BASE。XA 走两阶段提交一致性最强但性能和可用性会打折扣BASE 需要集成 Seata走最终一致性适合对实时一致性要求不高的业务。我的经验是能用“单分片事务 补偿”解决的就不要上分布式事务。分片键设计得好很多业务天然能落到同一个分片比如按 user_id 分片后一个用户的所有操作都在一个分片内完成本地事务就够了。分布式事务是最后的手段不是默认配置。5.4 高频报错与解决方案速查现象可能原因处理方式查询性能骤降SQL 路由到所有分片查询条件没带分片键优化查询携带分片键或补异地索引表启动报Inline expression can not be parsed分片表达式语法错误或 Spring 占位符冲突yml 里用$-{}检查语法刚插入的数据查不到主从延迟读走了从库关键读强制走主库或纳入事务Proxy 下连接不正常服务端预处理不兼容JDBC URL 加useServerPrepStmtsfalse跨分片事务部分回滚默认 LOCAL 事务不支持跨分片切换 XA 或 BASE分片后 JOIN 结果异常或极慢未配置绑定表/广播表配置 bindingTables 和 broadcastTables数据量翻倍后热点集中分片键分布不均匀换 HASH_MOD 或重新选分片键连接池被打满JDBC 模式实例数过多压缩连接池上限或改 Proxy 模式排查这类问题第一件事永远是打开 SQL 日志。JDBC 模式在日志里能看到每条 SQL 路由到了哪些节点Proxy 模式把sql-show: true打开控制台会直接打印路由结果。看到 SQL 实际走的路由绝大多数问题就解决一半了。最后再分享一个我个人的体会分库分表这件事方案永远比技术重要。很多团队上来就纠结 JDBC 还是 Proxy结果连分片键都没想清楚上线之后天天在改路由。先用半天想清楚业务的主访问路径再花半天定分片键和分片数剩下的配置工作其实一下午就能完成。工具是死的规则是活的真正值钱的是你对业务数据流的判断。