
接手一个单库单表已经跑到千万级的订单系统第一反应往往不是兴奋而是慌。MySQL CPU 常年 70% 以上慢查询日志十分钟刷一屏业务方还在不断提需求按用户查订单、按订单查详情、运营要拉全链路报表。在正式动手拆分前我在 ShardingSphere JDBC 和 ShardingSphere Proxy 之间反复横跳了两周又花了一个多月把分库分表边界、读写分离联动规则一条条列清楚。这篇文章就是那段时间的完整复盘内容包括两套模式怎么选、分片边界到底画在哪、分库分表和读写分离怎么联动配置以及上线前后我踩过的一堆坑。适合正在做或准备做数据分片的团队参考也适合技术负责人拿自己场景来对照。1. ShardingSphere JDBC 与 Proxy先分清这两种形态再谈选型1.1 两种模式的工作原理ShardingSphere 提供两种部署模式。JDBC 模式本质上是一个增强版的 JDBC 驱动打成 jar 包嵌到你的应用进程里应用通过它访问多个真实数据库。它不额外占用端口不引入中间件节点SQL 在应用内存里完成解析、路由、改写、归并然后再连到真实的 MySQL 或 PostgreSQL 执行。因为数据源都在同一个进程内所以性能损耗很小连接是应用直连数据库。Proxy 模式则是一个独立部署的中间件服务它对客户端暴露标准的 MySQL/PostgreSQL 协议端口。你的应用、BI 工具、数据平台可以像连普通数据库一样连它真正的分片和读写分离逻辑全在 Proxy 进程内完成再转发到后端真实的数据库。可以把它理解成数据库前面的一个总机交换机下游数据库对客户端完全透明。两者不是互斥关系一个团队完全可以同时跑两种模式。JDBC 模式适合核心 Java 服务写路径走性能敏感Proxy 模式适合多语言团队、数据平台、运维统一管理入口。很多线上项目最终就是这种混合形态。1.2 从团队和业务反推选型的核心判断点选型不是看哪个更流行而是看你的约束条件。第一个问题是技术栈如果全链路都是 JavaJDBC 模式直接嵌进应用少一条网络链路无论从延迟还是排障角度都更友好。一旦出现非 Java 服务要访问分片数据比如 Python 脚本、Flink 任务、BI 报表工具这时候 JDBC 模式就没法用了只能靠 Proxy 对外提供标准数据库协议。第二个问题是运维能力。JDBC 模式没有独立节点要运维但它的配置是跟着应用走的改分片规则要重新发版重启应用Proxy 模式的规则集中在独立进程里DBA 可以通过改配置来调整多数场景还能热加载。如果团队里有专职 DBA 或者基础设施负责人Proxy 明显好管理如果是小团队应用和数据库都忙不过来再养一套 Proxy 节点会加重负担。第三个问题是连接数。应用实例数量多的时候JDBC 模式每个实例都会和所有后端库建立连接连接数是「实例数 × 库数」规模一大非常容易把数据库连接池打满。Proxy 模式把连接集中在中间件上后端连接数可以控制得比较可控客户端和 Proxy 之间再走一套连接池。还有一个容易被忽略的点升级。JDBC 模式升级 ShardingSphere 版本必须跟着应用发版节奏走而 Proxy 可以在业务无感的情况下单独升级。版本修 bug 或者上特性Proxy 的运维灵活度要高很多。1.3 两种模式的关键参数对比把核心差异拉一张表方便直接对照。维度ShardingSphere-JDBCShardingSphere-Proxy部署形态应用内嵌 jar无需独立部署独立进程需要单独维护性能直连数据库延迟最低多一跳网络有一定性能损耗接入语言仅 Java任意支持 MySQL/PG 协议的客户端连接管理每个应用实例持有后端连接连接集中在 Proxy可控性好规则生效通常需要应用重启多数配置可热加载兼容性依赖 Java 技术栈对多语言、BI、数据平台友好运维门槛低无额外组件需要监控 Proxy 自身运行状态适合场景核心 Java 服务、低延迟要求多团队共用、非 Java 接入、统一入口这张表没法直接告诉你标准答案但它能帮你把非关键因素剔除掉。比如你团队全是 Java运维又只有两个人那就别为了「统一入口」上 Proxy先 JDBC 起手如果你已经明确有 Flink、Python、BI 要直连那 Proxy 基本是必选项甚至只有 Proxy 一个选项。1.4 我经历过的选型弯路我第一次做这个决定时因为团队是纯 Java完全没犹豫就选了 JDBC 模式。结果上线三个月后数据分析团队要拉订单报表写了个 Python 脚本直连数据库发现连不进来——分片规则在应用内部脚本根本无从感知。最后只能又补了一套 Proxy 对外开放只读入口核心业务写路径仍然走 JDBC等于两套加起来用。这个教训是选型一定要先问「未来谁要访问数据」而不只是「现在谁在写代码」。另外我见过有人单纯用性能评测结果做决定测出来 JDBC 比 Proxy 快 20%就否掉 Proxy结果忽略了团队里有多个语言栈这个硬约束。性能差异确实存在但多数业务系统的瓶颈根本不在这一跳网络的毫秒级开销上与其纠结这两个点的损耗不如先保证架构能覆盖所有人的接入需求。2. 分库分表的边界不是表大了就一定要拆2.1 什么时候才应该动手分库分表在谈边界之前先明确一个前提分库分表是成本很高的方案它会让 SQL 能力退化、事务变成分布式、运维复杂度上升所以动手前必须确认已经到了非拆不可的地步。我常用的判断标准有四条。第一是容量单表数据量逼近千万级甚至更高B 树层级增加查询性能明显劣化或者单库磁盘、连接数、容量规划已经撑不过未来一年。第二是写入吞吐单库写入成为瓶颈CPU、IO 持续高位从库延迟都压不住。第三是连接数应用实例多、数据库最大连接数被打满。第四是业务模型已经出现明显的自然分片维度比如用户、租户、地区每个维度天然可以隔离。如果只是查询慢但数据量不大先做索引优化、冷热数据归档、加缓存如果读多写少先做读写分离很多时候集群读写分离就能顶两三年。我把这理解成「先做低烈度优化再做高烈度拆分」。真正需要分库分表时要同时评估业务改造量、历史数据迁移方案、回滚预案这些成本往往比大家想象的高。2.2 分片键怎么选这是整个方案的命门分片键决定了所有读写 SQL 如何路由。选得不好轻则查询全库扫描重则数据热点把某个分片打到宕机。我把它概括成三个要求高频查询必须携带、值的分布必须均匀、值本身不能频繁变更。拿订单场景举例如果按 order_id 分片那么所有按 user_id 查订单的请求都变成全分片广播查询性能直接崩掉。反过来如果按 user_id 分片那么查单个订单详情时得先从订单里解析出 user_id 才能路由。做订单系统时我见过最实用的做法是订单主表按 user_id 分库、按 order_id 分表同时订单明细按 order_id 做冗余分片核心查询都强制带 user_id实在没有 user_id 的查询走底层宽表或者搜索引擎兜底。实践中还需要给分片键一个「逃生通道」。业务上总有一些低频但重要的查询不带分片键比如后台运营筛某个时间段的所有订单。这类查询 SQL 层面可以配置成广播执行但频率必须压住否则全分片扫描会拖垮整个集群。2.3 分片算法选型取模、区间、时间各有边界ShardingSphere 内置了多种分片算法最常用的几类各有适用边界。取模HASH_MOD理解成本低、实现简单分布也比较均匀适合绝大多数在线交易场景。它的短板在扩容如果一开始定的是 2 个库数据增长后要扩到 4 个库取模结果全部变化意味着大量存量数据需要迁移重分布。区间分片RANGE按某个连续的数值范围分配比如用户 ID 前 1 亿放库 0、后 1 亿放库 1扩容只需要新增一个区间对存量数据友好但容易产生热点比如新用户集中写入时新区间所在的库被打爆。时间分片INTERVAL或按年月做表天然适合日志、流水这类数据冷热分离直观但活跃数据往往集中在最新分区写入热点也需要额外考虑。在线交易我默认推荐取模但要在容量规划时留足余量。比如预估三年数据是 2 个库的量就按 4 个库的规模设计甚至直接用 2 的幂次分片数后续翻倍扩容时可以采用「一致性哈希」或者专门的扩容工具处理数据迁移。这里要特别说一句ShardingSphere 本身不负责移动数据扩容时的存量数据迁移需要额外的数据同步方案这个工作量会被很多人低估。2.4 分库分表后的能力边界什么 SQL 别再想了这是整个方案里最容易踩雷的部分。分库分表不是把 SQL 原样放大到多台机器执行很多单库时代理所当然的能力拆分之后都会打折扣。连接JOIN是第一个限制。跨分片的 JOIN 基本不可用只有在两个表的分片键相同并且分片数一致时ShardingSphere 才能把它们作为绑定表binding tables来优化保证 JOIN 发生在同一个库内。所以表结构设计从拆分第一天就要围绕「同分片键」来组织不能等到上线后才发现关联查询全废了。事务是第二个限制。单库的本地事务只在一个分片内有效跨分片的数据一致性需要分布式事务。ShardingSphere 支持 XA 和 BASESeata两种柔性事务方案但分布式事务的性能和复杂度都会上升所以设计上尽量让核心操作落在单分片内完成比如一个用户的订单操作全部按 user_id 路由到同一个库。还有几个隐性限制自增主键不能再当全局主键用分片之间会产生冲突必须换分布式 ID唯一约束只在分片内有效跨分片唯一性要靠业务保证深分页 LIMIT offset 需要把每个分片的结果都拉到内存做归并offset 越大开销越大必须从业务上限制DDL 变更要注意同步到所有分片执行。这些不是 ShardingSphere 的缺陷而是分布式数据库方案的通用边界提前知道边界就不会在线上去「发明」边界。2.5 表的分类处理分片表、广播表、绑定表分库分表方案里不是所有表都要做同样的待遇我一般把业务表分成三类来设计。分片表是核心业务表带分片键按算法路由到不同库表比如订单表、订单明细表。广播表是那些数据量小、变更少、几乎每个查询都要关联的字典类表比如商品类目、地区表、配置表这类表在每个分片里都放一份完整副本查询时直接本地关联。ShardingSphere 对广播表的写操作会同步到所有分片所以这类表千万不能是大表或者高频写表。绑定表是分片键和分片数完全一致的一组表比如订单表和订单明细表都按 user_id 分库这样两表 JOIN 时 ShardingSphere 能把 JOIN 下推到同一个库内执行避免跨分片笛卡尔积。这三类表的划分要在配置里显式声明在上线前把每个表的归属过一遍。我见过最惨的案例是把一张高频写的大字典表配成广播表结果每次更新都往所有分片写直接拖垮整个库。表分类这件事做得越细后面的 SQL 兼容性越好。3. 分库分表 读写分离联动实战3.1 联动配置的核心思路逻辑数据源怎么套分库分表和读写分离不是两条平行线而是嵌套关系。正确的做法是先对每个物理主库配上它的从库组成一个读写分离的逻辑数据源然后分片规则里 actual-data-nodes 不再指向物理库而是指向这些逻辑数据源。这句话展开说假设你有两个物理库 order_db_0、order_db_1每个库又各有一个从库 order_db_0_replica、order_db_1_replica。第一步用 readwrite-splitting 规则把 write_ds_0 和 read_ds_0 组合成 rw_ds_0把 write_ds_1 和 read_ds_1 组合成 rw_ds_1。第二步在分片规则的 actual-data-nodes 里写 rw_ds_${0..1}.t_order_${0..1}表示分片是「逻辑数据源 表」的二维组合。这样一条查询 SQL 经过分片引擎路由到某个逻辑数据源后再经过读写分离引擎决定走主库还是从库两层路由串起来就是联动。这里最容易配错的地方是顺序。有同事第一次配置时把主从库写进了分片的>dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.4.1/version /dependency数据源配置这块把四个物理数据源都声明出来写库 0、写库 1、读库 0、读库 1。生产环境强烈建议把 HikariCP 连接池参数显式写出来而不是用默认值。spring: shardingsphere: datasource: names: write_ds_0, write_ds_1, read_ds_0, read_ds_1 write_ds_0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://10.0.0.1:3306/order_db_0?useSSLfalseserverTimezoneAsia/Shanghai username: order_rw password: xxxxx maximum-pool-size: 30 write_ds_1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://10.0.0.2:3306/order_db_1?useSSLfalseserverTimezoneAsia/Shanghai username: order_rw password: xxxxx maximum-pool-size: 30 read_ds_0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://10.0.1.1:3306/order_db_0?useSSLfalseserverTimezoneAsia/Shanghai username: order_ro password: xxxxx maximum-pool-size: 50 read_ds_1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://10.0.1.2:3306/order_db_1?useSSLfalseserverTimezoneAsia/Shanghai username: order_ro password: xxxxx maximum-pool-size: 50注意从库尽量用只读账号权限上就拦住意外写操作这是多一道保险。接下来是两条规则先把主从组成逻辑数据源再让分片指向逻辑数据源。rules: readwrite-splitting: >schemaName: sharding_order dataSources: write_ds_0: url: jdbc:mysql://10.0.0.1:3306/order_db_0?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_rw password: xxxxx connectionTimeoutMilliseconds: 30000 write_ds_1: url: jdbc:mysql://10.0.0.2:3306/order_db_1?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_rw password: xxxxx read_ds_0: url: jdbc:mysql://10.0.1.1:3306/order_db_0?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_ro password: xxxxx read_ds_1: url: jdbc:mysql://10.0.1.2:3306/order_db_1?serverTimezoneAsia/ShanghaiuseSSLfalse username: order_ro password: xxxxx rules: - !READWRITE_SPLITTING dataSources: rw_ds_0: writeDataSourceName: write_ds_0 readDataSourceNames: - read_ds_0 loadBalancerName: round_robin rw_ds_1: writeDataSourceName: write_ds_1 readDataSourceNames: - read_ds_1 loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBIN - !SHARDING tables: t_order: actualDataNodes: rw_ds_${0..1}.t_order_${0..1} databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: db_inline tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: tbl_inline shardingAlgorithms: db_inline: type: INLINE props: algorithm-expression: rw_ds_${user_id % 2} tbl_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 2}启动 Proxy 后客户端连接方式和连 MySQL 一样mysql -h127.0.0.1 -P3307 -uroot -p。应用里 JDBC URL 直接指向 Proxy 的 3307 端口Flink、Python、BI 这些非 Java 组件只要配置一个标准 MySQL 数据源就能读分片数据这是 Proxy 模式最核心的价值。有一个细节要提醒Proxy 模式下应用端拿到的是逻辑库 sharding_order原来 SQL 里写的表名 t_order 不用变但库名要写成逻辑库名很多人在这一步连错。另外 Proxy 的字符集、时区、事务隔离级别要和后端库对齐否则中文乱码或者时间偏移的排查会让你怀疑人生。3.4 事务、主从延迟与强制主库路由读写分离联动之后事务和延迟是最容易出问题的两个点。先说事务Spring 的 Transactional 方法里如果既写又读默认会走主库这一点 ShardingSphere 处理得比较好事务内的查询不会路由到从库。但要注意如果你在事务外先写了数据紧接着在另一个事务里查刚才写的数据这时请求可能走到从库而从库的 binlog 同步还没追上就会读到旧数据。这就是经典的 read-your-writes 问题。解决办法有两个。一个是业务容忍短暂延迟大部分订单列表、消息列表都能接受几百毫秒的延迟那就什么也不用做另一个是必须立即读到就需要强制路由到主库。ShardingSphere 提供了 HintManager 可以做强制主库路由HintManager hintManager HintManager.getInstance(); hintManager.setWriteRouteOnly(); try { // 这里执行的查询强制走主库 ListOrder orders orderMapper.selectLatestOrder(userId); } finally { hintManager.close(); }我再强调一个细节HintManager 用完必须 close最好用 try-finally 包住否则线程池复用时路由状态泄漏到下一次执行线上会出现莫名其妙的请求全打到主库主库压力突然飙高。主从延迟本身要从两个层面治理。第一层是配置层面负载均衡算法可以选 ROUND_ROBIN、RANDOM、WEIGHT从库性能不均匀就用 WEIGHT 调权重从库延迟超过阈值时要及时摘除这块一般要配合监控平台做ShardingSphere 本身不负责健康检查。第二层是业务层面延迟敏感的场景就强制主库读延迟不敏感的场景走从库不要试图靠调整同步参数来解决业务问题。3.5 分布式 ID 与幂等处理分库分表后不能再依赖数据库自增主键ShardingSphere 内置了雪花算法SNOWFLAKE主键生成器配置里声明即可主键字段会自动填充。tables: t_order: actual-data-nodes: rw_ds_$-{0..1}.t_order_$-{0..1} key-generate-strategy: column: id key-generator-name: order_snowflake key-generators: order_snowflake: type: SNOWFLAKE这些配置第一次跑通之后建议立刻做一轮「路由正确性」测试用一组已知的 user_id 和 order_id 组合分别验证增删改查的路由结果是否落在预期库表而不是只看功能正常。路由这一步错了数据分布就是乱的后面所有问题都会被放大。4. 常见问题与排查技巧实录4.1 高频问题排查速查表把我在联调、压测、上线初期遇到的典型问题整理成一张表基本覆盖了联动配置的大部分坑。现象可能原因处理思路查询不带分片键所有分片都被扫到SQL 没有带 user_id 等分片键检查业务 SQL必要时强制带分片键或走搜索引擎写操作执行成功但数据不在预期分片分片算法表达式写错或字段没被识别打开 sql-show 看路由结果核对算法表达式从库读到的数据是旧的主从复制延迟根据业务容忍度决定是否使用 HintManager 强制主库路由事务方法里读到了不一致数据事务外读走了从库检查事务边界必要时强制主库读JOIN 查询结果异常关联表未配置为绑定表或广播表确认表分类把相同分片键的表配成绑定表分页深翻很慢深分页需要全分片归并业务限制 offset或改基于游标/时间翻页主键冲突仍在使用数据库自增主键切换到 SNOWFLAKE 或业务主键生成方案Proxy 客户端连接失败逻辑库名或端口配置错误检查 schemaName确认连接串使用的是逻辑库这张表越到后期越值钱。我每次上线新分片规则都会把上一轮的坑列出来让团队成员照着自检一遍比自己瞎猜快得多。4.2 我踩过的几个实战坑第一个坑是连接池参数没配。第一次压测时所有请求都慢查了半天发现 HikariCP 的默认最大连接数只有 10业务线程一涨全部在等连接。后来把生产环境的 maximum-pool-size 按「并发数 × 平均执行时间」估算后才稳定写库和读库还要区分读库连接数可以给大一些。第二个坑是把读写分离的负载均衡权重忽略了。当时两个从库配置不一样一个 16 核一个 4 核我按默认 ROUND_ROBIN 轮询结果 4 核的从库被打满慢查询拖垮了整个 SLA。改成 WEIGHT 负载均衡权重按核数配成 4:1 之后才恢复正常。第三个坑是扩容时忘了同步广播表。当时新增了一个分片库分片表的数据迁移都做好了但广播表只在旧库里更新新库里的字典数据一直是空的导致关联查询在新库上是 NULL。后来我把广播表刷新做成部署流程里的一步每次扩容和发版都强制检查。第四个坑是 HintManager 泄漏。这个前面提过线程池线程复用导致强制主库路由状态泄漏高峰期主库连接数直接被拉满。排查手段是在压测监控里看到主库连接数异常上涨顺藤摸瓜才发现是 Hint 没关闭。代码评审时我强制要求所有 Hint 必须 try-finally 包住。4.3 联动方案上线前的检查清单最后分享我每次切分片 读写分离前都会过的检查清单不是那种走过场的勾选而是每一条都踩过坑之后的总结。SQL 审计把所有核心 SQL 过一遍确认查询都带分片键不带分片键的低频查询是否可控、有没有兜底方案。表分类复核确认哪些是分片表、广播表、绑定表广播表数据量是否足够小、写入频率是否足够低。主从同步检查确认所有从库 binlog 同步正常延迟基线是多少从库账号是否有只读权限。数据校验在灰度数据上跑一轮分片路由结果校验核对每个分片的数据条数和抽样数据内容。压测验证混合读写压测确认主库写压力、从库读压力、连接数、SQL 路由是否符合预期。回滚预案明确如果上线后出现严重问题如何快速回滚到单库模式数据补偿脚本是否就绪。监控覆盖主从延迟、分片热点、路由错误、Proxy 进程状态、连接池水位都要有监控告警。我自己在实际操作中还有一个体会分库分表方案上线后不要急着把优化工具都扔掉。很多团队拆完库就觉得万事大吉结果半年后慢查询又回来了一查是某个新功能没带分片键。拆分的本质是给未来增长留空间但配套的 SQL 规范、代码评审、监控告警必须长期跟住否则边界会一点点被侵蚀。最后再分享一个小技巧如果你不确定某个查询在 ShardingSphere 里的路由行为不要靠猜直接把 sql-show 打开跑一次它会在日志里打印完整的逻辑 SQL、真实 SQL 和路由结果。这个能力在联调阶段几乎是免费的排障神器等全部验证完再关掉就行。这套「先 JDBC 后 Proxy、按业务定边界、分片与读写分离嵌套配置」的组合是我目前觉得最不容易翻车的落地方案。