做后端这些年我最怕听到一句话这个表数据量有点大了我们分库分表吧。很多人把分库分表当成数据库性能问题的万能药结果一拆就伤筋动骨。今天聊的ShardingSphere是目前国内用得最多的分库分表中间件之一它有两种接入形态ShardingSphere JDBC和ShardingSphere Proxy。前者以Jar包嵌入Java应用后者是独立服务。这篇文章我不打算照着官方文档念而是把JDBC和Proxy怎么选分库和分表的分界线到底在哪里怎么把读写分离和分库分表联动起来这三个问题一次性讲透配合可以直接抄的配置和我自己踩过的坑。1. ShardingSphere JDBC与Proxy两种形态先搞清楚1.1 JDBC模式Jar包嵌入应用改造门槛最低ShardingSphere JDBC是一种以Jar包形态嵌入业务应用的数据访问增强层。它的原理是实现了标准JDBC接口在应用与真实数据库之间插入了一层逻辑路由你拿到的DataSource其实是ShardingSphere的增强实现创建Connection、执行Statement、遍历ResultSet时ShardingSphere会解析SQL、根据分片规则计算目标库表、再把SQL发往真实数据库执行。应用侧的MyBatis、MyBatis-Plus、Spring Data JPA都不需要改动只要把数据源配置替换成ShardingSphere的数据源就行。对于Java技术栈的团队这是我认为改造门槛最低的方案。接入方式也很简单使用Maven引入shardingsphere-jdbc-core依赖然后通过Java API或Spring Boot配置文件创建逻辑数据源。以ShardingSphere 5.x为例核心一行代码就大致长这样DataSource dataSource ShardingSphereDataSourceFactory.createDataSource( modeConfig, dataSourceMap, ruleConfigs, props );后续业务代码从Spring容器拿到的DataSource就是路由后的逻辑数据源本地事务、连接池这些全部照旧。JDBC模式最大的优势是链路短、性能损失小因为没有额外网络跳数SQL解析和路由发生在应用进程内。我实测在普通查询下相比直连数据库的损耗可以控制在个位数百分比内具体取决于SQL复杂度和分片数。缺点也明显规则必须跟着应用走如果有多套微服务都需要分片同一份分片规则要复制到每个服务后续改规则容易漏而且只对Java友好其他语言接不了。1.2 Proxy模式独立服务业务无感知接入ShardingSphere Proxy是另一条路线它不是嵌入应用的Jar包而是一个独立部署的数据库服务端。ShardingSphere Proxy实现了MySQL和PostgreSQL通信协议从客户端视角看它就是一个普通数据库实例。应用把JDBC连接地址从真实数据库改成Proxy的地址和端口逻辑库名、用户名密码按规则填写就能像连普通数据库一样使用分片后的表。业务代码几乎不用动换一下连接配置即可这是它与JDBC模式最大的区别。正因为对外暴露的是标准数据库协议所以客户端选择非常自由Java、Go、Python、PHP都能接甚至DBA可以直接用Navicat、DBeaver去连Proxy查看逻辑表结构、执行SQL。规则集中在Proxy的配置文件里管理多个应用共享同一套分片和读写分离规则后续调整规则不需要每个应用发版本运维视角统一很多。代价是多了一次网络转发所有SQL要先到Proxy再由Proxy转发到真实数据库链路变长、延迟增加同时SQL兼容性受解析器限制个别复杂SQL直连数据库没问题但走Proxy就可能解析不了或者路由不出来。另外Proxy自身也需要维护5.x之后支持集群模式部署但比纯Jar包要重一些。1.3 一张表看清差异选型不再纠结把两种模式的核心差异放到一张表里选型的时候直接对着看对比维度ShardingSphere JDBCShardingSphere Proxy部署形态Jar包嵌入应用进程独立服务单独部署连接方式应用内数据源替换修改JDBC连接地址和端口网络链路应用直连数据库应用 - Proxy - 数据库性能损耗极低进程内路由有一跳转发损耗相对更大语言支持仅Java任何支持MySQL/PostgreSQL协议的语言规则维护随应用分散配置集中配置统一管理SQL兼容性较完整依赖解析协议兼容性稍弱运维成本低应用自带需要单独维护Proxy集群适合场景Java微服务、性能敏感业务多语言团队、DBA集中管控、管理平台入口我自己的选型经验是如果团队是纯Java且对性能特别敏感优先JDBC如果团队里多种语言并存或者需要DBA能直接连上去管理分片表就上Proxy。还有一种常见组合后面会细说应用内用JDBC模式处理高并发交易链路管理端和报表端连Proxy做统一查询入口。2. 分库/分表边界动手之前先算清这笔账2.1 分表触发条件单表数据量到底多大了才需要分很多文章喜欢拿MySQL单表超过500万或者2000万就必须分表说事这是典型的贫血结论。真实情况是单表能不能扛取决于三件事数据行数和行大小、访问模式是否走索引、写入并发有多高。我见过三千万行的表在SSD和合理索引下依然跑得飞快也见过才两百万行的表因为SQL没走索引被慢查询拖垮。所以分表之前先把慢查询日志和索引设计理清楚别拿分表当索引问题的遮羞布。如果你实在需要一个量化参考我一般这么估算单表容量。假设订单表的平均行大小为1KB加上二级索引后存储放大按2倍算那么单表500万行的物理体积大约为5000000 * 1KB * 2 10GB这个量级对现代数据库还算轻松到5000万行体积大约100GB这时候单表扫描、索引维护、备份恢复都会变慢分表的必要性才真正出现。公式很简单单表容量 预估行数 × 平均行大小 × 冗余系数冗余系数一般取1.5到2.5。再结合P99延迟和写入峰值如果单表超过千万行、并且持续增长同时慢查询比例明显上升就可以启动分表评估。分表解决的核心问题是降低B树索引高度、减小单表锁竞争、让数据分布到更多物理文件上方便冷热归档。但分表也带来一个硬约束——普通查询最好都带分片键否则就要广播到所有分片再归并。这是分表的边界你能不能在业务SQL里稳定拿到分片键。拿不到分表就只适合做归档型拆分不适合做在线交易改造。2.2 分库触发条件单实例扛不住才是分库的理由分库解决的不是单表大的问题而是单实例扛不住的问题。最典型的是连接数打满一个8核16G内存的MySQL实例即使把max_connections提到2000实际能稳定维持的活跃连接也就几百。假设每个微服务连接池配20个连接50个服务同时连上来就已经1000个连接了更别提还有定时任务、管理后台、数据同步工具。连接数一满新请求立刻排队表现就是接口毛刺、连接超时。这种场景下哪怕每张表数据量很小也必须考虑分库。另一个分库触发点是存储与IO瓶颈单实例磁盘空间有限大表占几个TB之后备份恢复都是灾难或者磁盘IOPS被打满主从同步延迟持续拉高。分库之后数据分散到多个实例每个实例的CPU、内存、磁盘压力都摊薄了故障爆炸半径也变小。但分库的代价比分表高出一个量级跨库事务需要分布式事务方案跨库join基本做不了全局唯一主键要另想办法数据迁移也复杂。所以我的判断顺序是先做索引再做缓存然后做主从读写分离最后才是分库分表。分库和分表通常会一起做因为分片规则是一致的。比如订单表按user_id分片既把表拆成16张也把不同数据路由到不同物理库用ShardingSphere表达式可以写得很灵活规则本身并不比单纯分表复杂多少但物理资源确实隔离了。2.3 分片键与算法边界划在哪数据就散在哪分片键是整个分库分表方案里唯一不能拍脑袋定的东西。我的标准有三个高频查询条件、取值离散、值不可变。高频查询条件决定分片键有没有用如果所有查询都走user_id而分片键是order_id那每次查询都要查全部16个分片再归并还不如不分。取值离散决定数据是否倾斜比如按省份分片广东一个省的数据量可能是其他省的几十倍热点瞬间打爆一个分库。值不可变是硬约束主键可以改的场景很少但业务键如果会被修改分片后数据迁移会让你痛不欲生。分片算法我实际用过的主要有四种MOD取模适合整型键且分片数固定HASH_MOD适合字符串键先哈希再取模分布均匀RANGE适合按范围查询的业务比如订单按时间区间查但区间边界容易产生热点INTERVAL是时间片分片天然适合日志和流水表冷数据可以整片归档。配置上ShardingSphere 5.x定义名称再引用sharding-algorithms: t_order_hash_mod: type: HASH_MOD props: sharding-count: 16算法选型时一定要考虑扩容边界。取模分片最大的坑是分片数一变大部分数据的路由位置都会变。比如原来按user_id mod 4分4片扩容到8片后原来落在0号片的数据因为mod 8的结果可能是0或4需要把新片的数据迁过去整体迁移量接近一半。很多团队因此不敢动分片数。更优的做法是分片数始终设计成2的幂次用位运算代替取模。比如16片用 user_id 15扩容到32片用 user_id 31每个旧分片只需要拆出一半数据给新分片迁移可控得多。这个设计细节早期多花十分钟扩容的时候能省一个通宵。2.4 第一个要动的其实是读写分离说完分库分表的边界必须泼一盆冷水绝大多数业务系统在数据量真正大到非拆不可之前最大的瓶颈其实是读压力。一个典型的订单系统读请求往往是写请求的十倍以上查询列表、详情、统计都是读。这种场景下性价比最高的第一刀不是分库分表而是读写分离搭一个主库负责写入挂两个只读从库分担查询一主两从的读能力就是原来的三倍成本却比分库分表低得多。ShardingSphere对读写分离的支持非常成熟而且它允许读写分离和分库分表叠加使用分片规则决定SQL去哪个逻辑库读写分离规则再决定这个逻辑库下面的SQL去主库还是从库。换句话说你完全可以先做主从读写分离缓解读压力等写并发和单表容量确实顶不住了再叠加分库分表。这个渐进式演进路径比一上来就设计十六库三十二表要稳妥太多。3. 读写分离联动分库分表配置一次跑通3.1 主从架构下读写分离是怎么工作的先说原理。使用读写分离前你得先准备好MySQL主从复制环境主库开启binlog从库change master to指向主库业务上保证主库只写、从库只读。ShardingSphere本身不生产主从关系它只负责路由解析SQL后如果是INSERT/UPDATE/DELETE路由到write-data-source-name指定的主库如果是SELECT则按负载均衡算法从read-data-source-names指定的从库列表里挑一个执行。需要注意一个细节ShardingSphere默认让事务内的所有SQL都走主库。这是故意的。因为事务内的写操作提交后从库的binlog同步存在延迟如果事务内紧接着查询走到了从库很可能读到旧数据破坏事务隔离性。所以一旦进入事务读写分离规则就自动失效所有SQL统一走主库保证同一个事务里看到的数据是一致的。非事务情况下才按SELECT/非SELECT来区分路由。理解了这一点后面排查为什么我在事务里的查询也打到主库了就非常快。3.2 JDBC模式完整配置分片读写分离联动下面是我实际用过的ShardingSphere 5.x Spring Boot YAML配置把分片和读写分离放在一起。为了演示假设物理环境是主库ds0写两个从库ds0_slave0、ds0_slave1读订单表t_order拆成16张表spring: shardingsphere: datasource: names: ds0, ds0_slave0, ds0_slave1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: order_writer password: xxx ds0_slave0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: order_reader password: xxx ds0_slave1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.12:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: order_reader password: xxx rules: readwrite-splitting: >load-balancers: read_balancer: type: WEIGHT props: ds0_slave0: 2 ds0_slave1: 1配置上线后建议把sql-show先打开观察每条SQL实际路由到哪个数据源、执行的是哪张物理表确认无误再关闭避免生产环境日志量爆炸。3.3 Proxy模式完整配置独立服务接入如果走ShardingSphere Proxy路线思路完全一样只是配置从应用里搬到了Proxy安装包的conf目录。分发版解压后conf下主要有server.yaml和config-sharding.yaml。server.yaml里设置逻辑库的访问账号和权限示例rules: - !AUTHORITY users: - root%:root - sharding%:sharding provider: type: ALL_PERMITTEDconfig-sharding.yaml里定义逻辑库名、物理数据源、读写分离和分片规则。逻辑库名我习惯取sharding_db下面其实是完整的配置骨架schemaName: sharding_db dataSources: ds0: url: jdbc:mysql://192.168.1.10:3306/order_db?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_writer password: xxx ds0_slave0: url: jdbc:mysql://192.168.1.11:3306/order_db?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_reader password: xxx ds0_slave1: url: jdbc:mysql://192.168.1.12:3306/order_db?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_reader password: xxx rules: - !READWRITE_SPLITTING dataSources: ds0: static: writeDataSourceName: ds0 readDataSourceNames: - ds0_slave0 - ds0_slave1 loadBalancerName: read_balancer loadBalancers: read_balancer: type: ROUND_ROBIN - !SHARDING tables: t_order: actualDataNodes: ds0.t_order_${0..15} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: t_order_hash_mod shardingAlgorithms: t_order_hash_mod: type: HASH_MOD props: sharding-count: 16这里要注意Proxy的YAML语法和JDBC模式稍有不同规则声明是!READWRITE_SPLITTING、!SHARDING这种带感叹号的标签数据节点表达式里的行内枚举用${0..15}而不是$-{0..15}。我把两套配置写出来就是为了提醒你网上搜到的配置五花八门先看准版本再抄。配置完成后在bin目录执行启动脚本bin/start.sh默认监听3307端口。应用侧只需要把JDBC连接改成jdbc:mysql://127.0.0.1:3307/sharding_db驱动还是普通的mysql-connector-java不需要引入任何ShardingSphere依赖也不需要改业务代码。这种接入方式对存量系统最友好很多团队选Proxy就是看重这一点。3.4 联动验证看路由结果和强制主库配置完之后别急着上线先手工验证一遍路由。开启sql-show或Proxy的日志执行一条插入、一条普通查询、一条事务内查询观察输出。JDBC模式下开sql-show日志会打印逻辑SQL和实际SQL、路由到的数据源。以插入为例实际执行会看到数据源是ds0主库物理表是t_order_2这样具体一张表普通查询大概率看到数据源是ds0_slave0或ds0_slave1而如果查询被包在Transactional里数据源又会回到主库。三层路由逻辑全部对上再放量上线。有些读场景对实时性要求非常高比如支付成功后立刻查订单状态即使只延迟几十毫秒也不能忍。ShardingSphere JDBC模式下可以用HintManager强制某条SQL走主库try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); Order order orderMapper.selectById(orderId); }HintManager用完会自动关闭作用范围仅限于当前线程和当前代码块不会污染其他请求。要注意的是别把它放到全局拦截器里统一强制主库那就等于把读写分离关了从库全部白搭。正确姿势是只在真正需要写后立即读的接口里局部使用。4. 高频踩坑与排查技巧实录4.1 分布式主键雪花算法不是配完就完事分库分表之后数据库自增主键就不能用了每个分片的自增都从1开始跨片必然重复。ShardingSphere内置了雪花算法snowflake作为默认主键生成器你只需要在表规则里声明主键列tables: t_order: keyGenerateStrategy: column: order_id keyGeneratorName: snowflake keyGenerators: snowflake: type: SNOWFLAKE但雪花算法有隐坑。它由时间戳、机器码、序列号组成同一时刻同一机器生成的序列号递增如果提前配好了多个应用实例必须在每个实例上配置不同的worker-id否则不同实例可能生成相同主键。5.x里通过props配置worker-id实例多的时候要有一个分配机制别全用默认值。另一个坑是时钟回拨如果操作系统时钟往前跳雪花算法可能生成重复ID或者拒绝生成。生产环境建议开启NTP时间同步并在代码里对时钟回拨做兜底实在怕麻烦可以自定义KeyGenerator改用号段模式。4.2 分页、排序、聚合跨分片分库分表之后LIMIT的分页语义变得很贵。比如limit 100000, 10表面上是拿最后10条但ShardingSphere无法预知每个分片里最后10条是哪些只能先把每个分片的前100010条取回来在内存里归并排序再截取最后10条。数据量一大内存开销和网络开销都爆炸接口延迟会从几十毫秒涨到好几秒。我见过有团队上线后深分页直接拖垮Proxy内存最后被迫改造。深分页的常见解法是游标分页用上次查询结果里的最大ID或业务排序字段作为下一次查询的起点避免大步长的offset。例如第一次查询返回最后一条order_id890123下一页就查order_id小于890123的记录天然跳过offset问题。聚合统计也类似COUNT、SUM、AVG在分片下都要先在各分片执行再归并汇总如果每个分片的数据量巨大建议把统计结果落到汇总表或者用离线计算引擎做别拿在线接口硬扛。4.3 SQL兼容性与连接报错排查分库分表中间件本质上是SQL解析器加路由引擎SQL语法支持范围再宽也有天花板。JDBC模式由于直接嵌入应用对常用ORM框架生成的SQL兼容性相对好Proxy模式因为要完整解析经过网络的SQL兼容性弱一些。我实际遇到过的典型报错有三类。第一类是连接报错比如No suitable driver found for jdbc:原因通常是驱动jar没引入或者JDBC URL前缀与驱动不匹配排查时先确认mysql-connector-java版本和URL写法。第二类是SQL校验报错错误信息里带sql injection violation字样表面看像安全拦截实际是ShardingSphere解析不了你的SQL打开sql-show看原始SQL和解析异常栈把不支持的语法改写掉。第三类是客户端工具的元数据查询问题用DBeaver或Navicat连接Proxy时工具会发一堆系统表查询、外键元数据查询个别版本不在兼容清单里就会报错升级连接工具版本或关闭自动获取元数据功能能绕过。4.4 主从延迟与强制主库路由读写分离最大的天敌是主从延迟。MySQL主从复制默认是异步的主库binlog刷盘后从库同步需要时间。高峰写入时从库延迟可能从几十毫秒拉到几秒。如果业务代码里出现写入后立刻查询最新状态就会撞上从库旧数据产生非常隐蔽的BUG用户下单成功了但页面查询订单列表没看到新订单过几秒刷新才出现。这种问题在测试环境几乎复现不出来生产一放量就冒出来。我的处理方案分三层。第一层业务上区分实时读和最终一致性读支付结果、库存扣减这种强一致场景查询强制走主库列表、统计这类允许秒级延迟的场景放心走从库。第二层用HintManager.setWriteRouteOnly()在局部强制主库具体代码上面已经写了。第三层做主从延迟监控定期从从库执行SHOW STATUS LIKE Seconds_Behind_Master超过阈值就报警同时可以在负载均衡策略上做文章延迟超过阈值的从库临时下线等追平再恢复。这个第三层我是在踩了几次延迟坑之后才补上的建议你在一开始就规划进去。4.5 扩容路上的坑取模分片扩容迁移分库分表最难受的运维操作就是扩容。如果你早期图省事用了MOD取模分片数从4扩到8理论上接近一半的数据要迁移到新位置。当年我经历过的扩容流程大致是先把新分片库表结构建好启动数据同步任务把历史数据搬过去做数据校验保证行数和关键字段一致然后在低峰期切换分片规则观察业务SQL是否全部命中新规则最后回收旧分片。全程要停写或者双写压力非常大。后来我学乖了新项目一律按2的幂次设计分片数用位运算取模。原因前面说过分片数从4扩到8时每个旧分片只需要拆出一半数据到对应新分片迁移范围是可控的。如果你用的是ShardingSphere Proxy官方还提供了弹性迁移能力和DistSQL在线修改规则的能力可以动态调整分片配置而不需要重启整个Proxy集群。但底层仍然要做数据迁移只是操作上友好了一些。所以分片键和取模方式一定要在项目第一天就定好这决定了一年后的运维幸福感。4.6 分布式事务从本地事务到XA/BASE分表不分库的时候如果所有分片都在同一个物理库普通Transactional还能凑合用一旦分了库事务就变成跨库事务本地事务彻底失效。ShardingSphere 5.x支持两种分布式事务方案XA和BASE。XA基于两阶段提交内置了Atomikos和Narayana实现特点是强一致、实现简单但两阶段提交的锁时间更长只适合事务短、并发不太高的场景。BASE接入的是Seata采用AT模式做最终一致业务侵入小适合长事务和跨服务场景但会有中间态对一致性要求极高的支付类核心链路要慎重。我的建议是尽量在业务设计层面减少跨库事务把需要强一致的数据放到同一个分片里。比如一个用户的所有订单都按user_id分片单用户的多个订单天然落在一个库很多事务就不跨库了。这比在系统里引入分布式事务框架要可靠得多。ShardingSphere配置分布式事务只是给了一个兜底能力别把它当成常规手段来用。5. 决策清单你的场景该用哪套方案把前面所有内容浓缩成一张决策清单方便你直接套用。我在实际项目里基本就是按照这张表来定的场景推荐方案理由Java技术栈、微服务多、性能敏感JDBC模式 读写分离 分表链路短、损耗低规则随应用走但可集中管理多语言团队、规则需要统一维护Proxy模式业务无感知接入DBA可直连管理表格数据量过千万、查询慢先查索引再考虑分表分表不是慢查询的遮羞布连接数打满、实例IO打满分库摊薄实例压力但接受分布式事务成本写后立即读、实时性要求高读写分离 局部强制主库用HintManager细粒度控制路由需要动态调整分片规则、扩缩容Proxy 2的幂次分片利用DistSQL在线调整迁移可控这张表不是银弹但至少能让你在做技术方案评审的时候有理有据地告诉产品经理现在这个阶段该不该上分库分表该选哪种形态。记住一句话分库分表是手段不是目标如果加个从库、加个缓存、优化两条索引就能解决的事别急着上重武器。6. 一点个人体会分库分表这件事真正的门槛不在于配置怎么写而在于你愿不愿意为未来三年做设计。我见过太多项目在数据量两百万的时候就上了十六库三十二表结果业务复杂度翻倍、跨库统计痛不欲生最后又花半年时间把分片改回去。我自己现在做技术方案一定是先问三个问题读瓶颈还是写瓶颈单表数据量三年后会到多少业务SQL能不能稳定携带分片键这三个问题有了明确答案JDBC和Proxy怎么选、分库还是分表、要不要联动读写分离根本不需要纠结。最后再分享一个小技巧无论最后选了JDBC还是Proxy都记得把分片规则的变更纳入发布流程管理。规则文件要进Git做版本管理改规则要像改代码一样走评审、走灰度。数据是公司最值钱的资产路由错了不会马上报错但会悄悄产生脏数据真到发现的时候加班补数据的就是你。
