尧图精选

ShardingSphere分库分表与读写分离联动实战指南

🕒 发布时间:2026/9/11 20:24:13 📁 来源:尧图网络
ShardingSphere用了快四年团队的订单库从单库单表一路拆到16库32表JDBC和Proxy两种模式都深度踩过坑。这篇文章不打算重复官方文档而是想把我自己总结的选型思路、分片边界判断方法以及读写分离和分库分表联动配置的完整实践过程梳理一遍给正在纠结这些问题的朋友一个可以直接参考的答案。1. 先搞懂JDBC和Proxy到底差在哪1.1 两种模式的核心差异很多人第一次接触ShardingSphere时最困惑的就是JDBC和Proxy两种模式到底怎么选。其实这两种模式一个本质是应用内嵌一个是独立服务理解这一点就抓住了核心。JDBC模式说白了就是一个增强版的数据库驱动。你的业务应用还是走JDBC接口连数据库只是这个驱动内部帮你做了SQL解析、路由、改写、归并等一系列操作。它是以jar包的形式跑在你的应用进程里的应用连的还是原来的数据库地址。相当于给你配了一个贴身翻译在你的进程内部帮你把对逻辑表的访问翻译成对真实表的访问。Proxy模式则完全不同。它是一个独立部署的中间件服务对外暴露一个MySQL/PostgreSQL协议端口。你的应用连接的不再是真实的MySQL而是这个Proxy服务。Proxy帮你做了协议解析再以MySQL客户端的身份去连接后面的真实数据库集群。这相当于给整个数据库层加了一个前置网关所有客户端都统一从这个网关进出。这两种架构差异直接决定了它们后续所有的选型取舍部署方式JDBC模式和应用同进程随应用一起启动Proxy模式是独立服务需要单独维护。支持的客户端语言JDBC模式只支持Java虽然也有Go版但生态远不如Java成熟Proxy模式对所有支持MySQL/PostgreSQL协议的语言都开放理论上Python、Node.js、Go、C都能连。数据聚合能力JDBC模式在每个应用进程里做聚合适合数据量可控的场景Proxy模式在服务端统一聚合适合数据量大的场景且聚合逻辑只维护一份。1.2 一张表看清选型边界我把四年实战下来的判断标准整理成了一张表按这个逻辑去选基本不会出大问题。对比维度JDBC模式Proxy模式性能损耗低进程内完成路由无额外网络跳数中等多一跳网络转发长SQL场景损耗放大运维成本低应用自带无额外组件高需要独立部署、监控、扩缩容语言生态仅JavaGo版本不成熟多语言通用集中管理差规则分散在各应用配置中好规则集中在服务端一次修改全局生效连接管理每个应用独立建连连接数难统一管控服务端统一管理真实连接复用率高业务侵入低应用内配置即用中需要改造应用的数据源配置适合场景中小规模、单团队、Java技术栈多团队共享数据库、多语言、大规模集群1.3 我建议的选型原则选型这件事没有绝对的哪个更好只有哪个更适合你当前的处境。我自己的经验是三个判断维度第一个维度看团队规模。如果你是一个20人以下的后端团队技术栈以Java为主数据库实例数量也不多那JDBC模式几乎是最优解。它开销小、上手快、出了问题好排查不需要额外养一套中间件的运维能力。我们早期就是这种情况一个核心交易库拆分直接引入JDBC模式两周就完成了上线。第二个维度看数据库的使用方范围。如果你们的数据库只给一个业务系统用那JDBC模式没有任何问题。但如果DB层是多个团队共享的资源大家都需要访问同一套分片集群或者有非Java语言的服务需要接入那Proxy模式的集中管理优势就很明显了。我见过太多案例JDBC模式用着用着业务线多了各自在配置里写了一套分片规则改配置需要所有应用一起发布最后被迫迁移到Proxy。第三个维度看的性能敏感程度。Proxy模式因为多了一层网络转发纯查询场景下的性能损耗大概在5%-10%左右如果SQL特别复杂、返回数据量大损耗还会进一步放大。而JDBC模式因为路由和归并都在进程内完成性能几乎无损。如果你对性能有极致要求且Java技术栈统一JDBC模式会更稳。实际中还有一个很关键的判断点你未来的数据量会涨到多大。如果预计单表很快会突破千万甚至亿级需要持续扩容Proxy模式在扩缩容时的优势就会体现出来。ShardingSphere有专门的弹性迁移组件可以对Proxy底层的真实数据做无损搬迁应用层面甚至感知不到。JDBC模式做扩容则要复杂得多需要协调所有应用重新发布。所以如果你已经能看到三五年的增长曲线建议一开始就上Proxy不要先JDBC后Proxy来一次痛苦的架构迁移。2. 分库分表的边界什么时候该动手2.1 分片阈值怎么判断聊到分库分表最常见的问题就是数据量到多少才需要分这是一个没有标准答案的问题但我可以给你一个判断框架而不是一个拍脑袋的数值。先看容量维度。MySQL单表在数据量和索引大小达到一定规模后B树的层级会增加随机IO的成本会明显上升。以我个人经验单表行数超过2000万左右或者单表数据文件超过50GB就应该认真考虑分片了。当然这个阈值和你的硬件环境、查询模式强相关。SSD环境下3000万甚至5000万行可能还能扛但如果你的查询模式有大量的范围扫描、多索引回表2000万行可能已经非常吃力了。再看性能维度。判断是否分片的关键指标不是数据量而是单库单表的QPS是否已经压榨到极限。你可以通过MySQL的performance_schema或者慢日志去统计当单表每秒承载的读写次数达到8000-10000次而CPU和IO已经出现周期性瓶颈时就是该考虑分片的时候了。还有一个我比较常用的估算方法算热点数据增长率。假设你的业务每天新增100万条订单数据一年就是3.6亿条。如果订单只保留最近三个月的活跃数据单表数据量会稳定在9000万左右如果历史数据也频繁查询那这个数字会持续攀升。算出这个数后心里就要有底了我的表能撑多久什么时候到极限。2.2 分片键选型和分片策略分片键的选型是整个分库分表设计中最关键、也最不可逆的决策。改分片键相当于把已分布的数据全部重洗一遍代价极大所以一开始就要想清楚。分片键的第一原则是高频查询必须带它。如果你的业务是典型的用户维度访问——查我的订单、查我的积分、查我的收藏——那user_id就是不二之选。按user_id做hash取模分片所有单用户维度的查询都能精确定位到唯一分片完全不需要全库扫描。但纯用户维度分片有一个很经典的问题后台运营需要按时间范围统计订单。这时候如果没有一个时间维度的全局索引就需要做跨全分片的聚合查询。我有一次就卡在报表需求上后来加了一张用户-订单-分片的映射表来兜底才把这个问题解决。所以如果你明知道会有后台聚合查询的需求分片键的设计要留好这种后手计划。确定好分片键后就是选分片策略。最常见的三种哈希取模按user_id hash后对分片数取模数据分布均匀但扩缩容需要重新分布。范围分片按时间或者ID区间切片扩容方便但数据可能倾斜在最新分片上。一致性哈希分布在环上按虚拟节点路由扩容影响面小但实现复杂且需要自己处理虚拟节点与真实节点的映射。如果追求简单可靠、业务以点查询为主哈希取模最稳妥。如果业务有明显的强序需求比如按时间归档范围分片更合理。两者也可以结合比如先范围再hash但复杂度会高一个量级新手慎用。2.3 常见的分片边界误区分库分表这件事最怕的不是不分而是乱分、早分、过度分。过早分片是我见过最多的一个坑。很多团队刚把单表数据量做到500万看了几篇分库分表文章就急吼吼地要拆库拆表。结果业务还在快速迭代分片规则改一次就疼一次开发效率被拉低了一大截。我个人的建议是如果数据量还能扛1-2年的增长那就先不分。分库分表是给系统续命的架构手段不是用来炫技的。另一个误区是过度分片。比如单表才2000万行非要用64个分片结果每个分片才30多万行分布式事务的复杂度、跨分片聚合的开销反而拖垮了性能。分片的粒度应该以单分片数据量控制在合理范围为目标比如每个分片500万-1000万行这样单个分片既不会太小导致过度拆分的开销也不会太大导致单片查询压力过大。最容易被忽略的还有分库不分表和分表不分库的区别。分库解决的是并发连接数和整体QPS瓶颈分表解决的是单表数据膨胀和索引效率问题。两者目标不同实践中经常需要联动处理。当整个数据库的活跃连接数逼近上限、CPU和IO成为瓶颈时优先分库当单表数据量持续膨胀、查询越来越慢时优先分表。还有一个边界问题是到底要不要保留全量数据在一个库里做备份。有些团队为了查询方便分片之后保留了一张全量汇总表。这个做法意味着所有写入都要双写事务一致性、存储成本都会翻倍。除非有极强的即时全量查询需求否则不建议这么干用离线数仓做全量分析是更合理的方式。3. 分库分表与读写分离的联动设计3.1 为什么要把两者联动起来在很多人的认知里分库分表和读写分离是两件独立的事。实际上在生产高并发场景下这两者几乎总是同时出现的。原因很简单一旦你的数据量大到需要分片说明你库的主库写入压力已经不小了此时再叠加高并发的读流量主库扛不住就需要把读压力分流到从库。但这里有一个容易被忽略的点读写分离和分库分表联动时规则配置是有顺序依赖的。你要先决定哪些库参与分片再在分片的基础上给每个库配置读从库。实际配置时每一个分片主库都得配置对应的从库集合否则流量路由过去后读压力还是会压在主库上。联动配置的典型架构是一主多从的集群作为底层存储每个分片主库负责写入多个从库分担读流量。通过ShardingSphere逻辑上你把一个逻辑表映射到多个物理分片每个分片又对应一组主从数据源。这样一条查询SQL到达ShardingSphere后先按分片键定位到具体分片再按读写分离规则将读流量转发到对应的从库。3.2 读写分离规则的核心配置项ShardingSphere的读写分离规则核心配置就是数据源分组把一个主库和它的一组从库打包成一个数据源组然后指定负载均衡算法。官方的算法有三种常用的轮询、随机、权重。我实际用的最多的就是轮询和权重。轮询适合从库配置完全一致的场景简单公平每个从库轮着来。随机适合从库连接数不均的场景但其实效果和轮询差别不大。权重适合从库机器性能有差异的场景比如一个从库是32核另一个是16核那权重就配置成2:1。这里有一个我踩过坑的细节权重算法是按访问次数来轮询的不是按实际负载如果SQL复杂度差异很大权重效果会失真需要定期观察从库的实际负载来调整。除了负载均衡算法还有一个让我特别推荐的配置是dynamic-strategy的动态读写分离策略。传统的读写分离是静态的从库挂了之后需要手动摘除。ShardingSphere JDBC 5.1.0以后支持动态读写分离可以定期探测从库的延迟和可用性自动摘除延迟过高或不可用的从库节点数据源的故障转移就稳妥得多。3.3 主从复制延迟怎么处理读写分离的最大痛点永远是主从延迟。MySQL默认的异步复制在某些高并发写入下延迟可能达到几百毫秒甚至几秒。在这个延迟窗口内你写入一条数据后立刻去读很可能读不到——因为读请求已经路由到了还没同步完成的从库。这个问题有几种常见的应对策略第一种是强制路由主库。对一致性要求极高的操作通过Hint注解或者ShardingSphere的hint路由机制把某条SQL强制发到主库执行。代价是这部分读流量还是会压到主库所以只适合刚写完立刻读这类高频短操作。我通常会在用户下单支付成功的回调逻辑里强制走主库读取刚写入的订单状态。第二种是延迟阈值控制。ShardingSphere动态读写分离支持配置延迟阈值当从库的复制延迟超过设定值比如10秒这个从库会临时从读流量中摘除等延迟恢复后再自动加回来。这是一个兜底方案保证读流量不会路由到延迟过大的从库上但瞬间的可用从库数量会减少。第三种是业务兜底。在代码里对一致性要求高的数据做本地缓存并设置极短的过期时间如1-2秒在这个时间窗口内从缓存读过期后再去数据库读。这个方案最简单但对业务有一定的侵入性而且缓存本身就是一套要维护的系统。实际上最靠谱的方案是组合拳核心链路强制主库读次核心链路接受微秒级延迟再配合延迟阈值把所有从库的延迟控制在上限内。过分追求所有读都絕對一致是不现实的关键是识别出哪些读必须强一致哪些读可以接受暂时滞后。3.4 分布式事务的取舍分库之后原来一个本地事务可能变成跨多个库的事务这是分库分表后最棘手的问题之一。订单创建时要同时写订单表和账户流水表如果这两张表被分到了不同的库原来一个事务就能搞定的事现在变成了跨库事务。ShardingSphere提供两种分布式事务方案XA强一致和Seata最终一致。我个人的选型建议很明确短事务、对一致性要求极高的用XA长事务、涉及外部系统调用的用Seata。但是——这里我要强调一点——能避免跨库事务就应该尽量避免跨库事务。避免跨库事务最实用的思路就是绑定表和一致分片。如果你有两张表总是出现在同一条SQL里做关联查询或者总是在同一个事务里被一起修改那它们在分片时就应当使用相同的分片键保证它们总是落在同一个分片上。ShardingSphere的绑定表功能就是做这个的配置成绑定表后关联查询会在各分片内直接完成不需要跨库合并。这样一来原来看起来要引入分布式事务的场景最终可能只需要本地事务就能解决。4. 实操过程从零搭建分库分表读写分离环境4.1 环境准备我先说明一下本次实操的环境版本ShardingSphere JDBC 5.3.2、Spring Boot 2.7.x、MySQL 8.0。这套组合是目前生产环境最常见的搭配之一文档齐全坑也基本被踩平了。硬件准备环节我需要3个MySQL实例1个主库、2个从库你也可以用Docker在本地模拟。主库实例命名master从库命名为slave_a和slave_b分别模拟不同的机器。三个实例之间配置好主从复制这一步很简单在从库上执行CHANGE MASTER TO指向主库的binlog坐标即可我这里就不展开复制配置的细节了。准备两个数据库ds_0和ds_1分别映射到两台MySQL服务器模拟分库后的两个物理库。每个库里建两张分表t_order_0和t_order_1最后构成2库×2表的分片拓扑。建表语句如下CREATE TABLE t_order_0 ( order_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, KEY idx_user_id (user_id) ); CREATE TABLE t_order_1 LIKE t_order_0;4.2 构建数据源拓扑这一步是整个配置的关键。我们需要配置两组数据源每组代表一个分片。每个分片组里包含主库和对应的从库。在ShardingSphere的配置体系里数据源的定义在最外层分片规则和读写分离规则在rules里。先在Spring Boot的配置文件里声明数据源spring: shardingsphere: datasource: names: ds_0_master, ds_0_slave_a, ds_0_slave_b, ds_1_master, ds_1_slave_a, ds_1_slave_b ds_0_master: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/ds_0 username: root password: root ds_0_slave_a: # 同上指向192.168.1.11:3306/ds_0 ds_0_slave_b: # 同上指向192.168.1.12:3306/ds_0 ds_1_master: # 指向192.168.2.10:3306/ds_1 ds_1_slave_a: # 同上指向192.168.2.11:3306/ds_1 ds_1_slave_b: # 同上指向192.168.2.12:3306/ds_1这里要注意一个细节多个从库如果只配置一个读写分离的负载均衡就没有意义。生产环境至少配置两个从库才能体会到负载均衡的效果。4.3 配置分片规则与读写分离规则接下来是核心的规则配置。我们需要配置两个规则块!READWRITE_SPLITTING和!SHARDING。spring: shardingsphere: rules: readwrite-splitting: >[INFO ] ShardingSphere-SQL: Logic SQL: INSERT INTO t_order (...) VALUES (...) [INFO ] ShardingSphere-SQL: Actual SQL: ds_1 INSERT INTO t_order_1 (...) VALUES (...)查询数据时观察日志[INFO ] ShardingSphere-SQL: Logic SQL: SELECT * FROM t_order WHERE user_id ? [INFO ] ShardingSphere-SQL: Actual SQL: ds_0_slave_b SELECT * FROM t_order_0 WHERE user_id ?从日志可以看到写入路由到了分片主库ds_1并写入物理表t_order_1查询路由到了分片从库ds_0_slave_b从物理表t_order_0读取。这说明分片和读写分离已经正确联动。为了进一步确认读写分离的负载均衡是否生效可以用一个简单的统计接口查询多次观察日志中SQL实际执行节点的分布情况。如果轮询策略生效两个从库应该交替出现在日志里。4.5 动态扩容的思路分片配置好了以后迟早要面对扩容的问题。以2库2表扩容到4库4表为例这涉及两类工作一是数据迁移把原有各分片的数据按新的分片规则重新分布二是配置变更让ShardingSphere的路由规则指向新的拓扑。ShardingSphere的弹性迁移组件Scaling可以在线完成数据搬迁它会把旧分片的数据按新规则扫描、清洗、写入新分片数据校验通过后自动切换配置。但我实际用的过程中发现这个过程的坑主要在数据校验阶段数据量大了以后校验耗时很长期间如果有持续写入增量数据同步也容易出现延迟累积。所以我的建议很简单扩容这个操作尽量在业务低峰期做提前评估好存量数据量和迁移耗时不要等活动开始了才临时抱佛脚。配置变更方面把上面的actual-data-nodes从ds_$-{0..1}.t_order_$-{0..1}改成ds_$-{0..3}.t_order_$-{0..3}同时数据源部分补上新增的ds_2、ds_3相关配置即可。但注意分片算法的取模基数也要从2改成4否则路由就会错乱这一点极其容易漏。5. 常见问题与排查技巧实录5.1 问题速查表我把实践中最常遇到的问题整理成了一张速查表方便随时查阅问题现象根因解决办法SQL查询报全路由错误日志提示In order to support SQL, ShardingSphere has to route all shards分片键未出现在SQL的WHERE条件中确保查询条件携带分片键或配置allow-hint-disable并接受全路由的性能损耗读写分离不生效日志显示所有SQL都走主库数据源组配置错误读流量未匹配到从库检查read-data-source-names配置确认负载均衡算法名称正确分布式主键冲突插入数据报主键重复雪花算法的workerId分配冲突在key-generators中给每个应用节点分配不同的worker-id或使用机器ID自动生成主从延迟导致数据不一致刚插入的数据查询不到主库写入后从库还未完成同步根据业务要求使用Hint强制主库读或配置延迟阈值动态摘除延迟过高的从库跨分片聚合查询慢后台报表查询长时间无响应聚合操作需要扫描所有分片再归并优化为离线计算或增加汇总表Proxy连接数不够客户端报Too many connectionsProxy的后端连接池配置过小调大Proxy的max-connections同时优化应用侧的连接池配置5.2 定位问题的方法论排查ShardingSphere的问题我总结出了一个固定套路先看SQL日志再看配置最后才看代码。第一步永远是看SQL日志。ShardingSphere会完整打印逻辑SQL和实际SQL这是判断路由是否正确的最直接证据。比如你想确认一条查询到底有没有走到从库日志里的Actual SQL会明确告诉你执行节点。如果这个节点不是你预期的那个那么十有八九是配置有问题而不是SQL的问题。第二步检查配置。ShardingSphere 5.x版本的配置采用YAML格式一个很常见的坑是缩进不规范导致配置静默失效日志里完全没有报错但行为就是不对。还有数据源名称的拼写错误这种问题最难发现因为配置加载不会立即报错只有运行时才暴露。第三步结合业务场景分析。有些问题排查到最后发现不是配置问题而是业务SQL本身写得不好。比如SQL里有一个JOIN操作关联的另一张表没有配置绑定表关系导致ShardingSphere需要跨分片拉取数据再做内存归并性能自然就差了。这种问题靠配置无法根治得从业务SQL入手优化。5.3 避坑经验与心得分享建了绑定表关系后关联查询仍然执行了跨库合并。这个问题的原因是ShardingSphere只在两张表的关联字段都能路由到相同分片时才走绑定表优化如果关联字段不是分片键或者有一张表的分片键不在JOIN条件里绑定表就不会生效。所以配置绑定表之前要确认关联查询确实带了分片键条件。还有一次在配置读写分离时踩了一个很隐蔽的坑我把写库的数据源名称写成了ds_0_master从库写成ds_0_slave但读写分离规则里配置的负载均衡算法名是round_robin_0而加载器里定义的是roundRobin_0——一个字母大小写不一致。ShardingSphere在加载配置时对这种命名不一致并不报错但运行时会提示找不到负载均衡算法然后回退到默认策略从库流量还是没被分担。这种低级错误排查起来费了不少劲我后来养成了每次配置完都手动跑几条SQL验证路由节点的习惯。还有一个值得说的经验是分库分表不等于性能无限提升。分片后如果业务SQL大量不带分片键全路由查询依然会拖垮整个集群。我参与过的一个业务系统原本单表查询很快分片后反而变慢了。后来排查发现这个系统的查询模式完全不是用户维度的大多数查询都是按时间范围做的。对这种业务正确的分片维度应该是时间而不是用户ID。从这里我深刻体会到分片键的选择必须和业务实际的查询模式深度绑定不能拍脑袋决定。最后分享一个让我受益很多的小技巧上线前在测试环境搞一个只读压测脚本随机生成多种模式的SQL跑一段时间专门检查ShardingSphere日志里有没有全路由的情况。这个脚本帮我提前发现了很多隐藏的全路由SQL避免了上线后主从被拖垮的事故。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →