ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ShardingSphere分库分表实战:JDBC/Proxy选型与读写分离联动解析

ShardingSphere分库分表实战:JDBC/Proxy选型与读写分离联动解析 分库分表听起来是个很“大”的架构决策但我在实际接触过的项目里绝大多数团队根本不是被数据量逼到必须分片而是先被一个问题卡死——ShardingSphere 该用 JDBC 形态还是 Proxy 形态这个选择没想清楚后面接口层、数据源管理、运维方式全都会跟着跑偏。更麻烦的是分库分表和读写分离往往不是单独上的一旦要联动路由规则怎么叠加、事务边界怎么控制、扩容时怎么把数据从 2 个库挪到 4 个库每一步都藏着坑。这篇文章就围着这三个问题展开JDBC/Proxy 怎么选、分库分表的边界到底划在哪、读写分离联动时配置和排障的完整思路。适合正在做技术选型、或者已经上了 ShardingSphere 但被路由问题折磨过的同学。1. JDBC与Proxy的架构分水岭接入方式决定一切1.1 两者到底差在哪一次连接的完整旅程先不谈功能列表我们从一次 SQL 请求的路径来看这两种形态的本质区别。JDBC 形态下ShardingSphere 是以一个 Jar 包的形式嵌入到你的应用进程里的。应用启动时读取你的分片规则本地维护路由表业务代码拿到的是一个经过 ShardingSphere 包装的 DataSource。SQL 从 MyBatis 或者 JdbcTemplate 出来之后ShardingSphere 在应用内部完成解析、改写、路由、归并然后把真实 SQL 发往各个分库。整个过程没有额外的网络跳转应用和数据库之间走的还是原来的连接池。Proxy 形态则是一个独立部署的中间件服务。你的应用连接的是 Proxy 暴露的数据库地址比如 3307 端口Proxy 内部再根据规则把 SQL 转发到后面的真实数据库集群。应用看到的始终是一个逻辑库它根本不关心背后有几个物理库、几张物理表。这两种形态最直接的差异就是一个住在你的进程里一个站在你的进程外。住在进程里少了一层网络开销性能损耗低但集群扩容、规则变更都得跟着应用发版走站在进程外应用无感知DBA 可以独立运维但每一条 SQL 都多一次网络往返延迟和吞吐都会受影响。1.2 一张决策表看懂适用场景很多团队在选型时只盯着性能数据其实更应该看的是团队结构和现有技术栈。我用一张表把关键维度列出来你对着自己的情况打勾就行。决策维度选 JDBC 形态选 Proxy 形态应用技术栈Java 应用纯 Spring 系多语言混用或非 Java 应用访问网络延迟敏感度极高接口 RT 要求 P99 低于 50ms可接受微秒级网络开销数据库规模分片数量少几十个以内分片数量多需独立运维规则变更频率低变更随应用发版高DBA 或平台侧独立调整连接数压力应用直连连接数可控大量应用共享 Proxy连接数集中管理团队角色应用开发主导DBA / 中间件团队主导我自己见过最典型的反面案例是一个金融项目组团队里全是 Java 开发数据库只有 4 个分片但因为听说 Proxy “以后好扩容”硬是中间架了一层 Proxy。结果每次规则调整都要找 DBA 配合SQL 性能问题排查也要跨团队拉群。其实这种规模用 JDBC 形态规则打包在应用里一次发版全部搞定简单得多。1.3 团队现状比技术指标更关键如果你去翻官方文档会看到功能对比表但真正影响决策的往往是几个容易被忽略的问题你们有没有专门的中间件运维人力业务方除了 Java 还有没有 Python、Go 的服务要访问同一套分片数据数据库账号权限是收在 DBA 手里还是应用自己管理我倾向于这样判断如果公司有 DBA 团队且数据库实例数量会长期增长Proxy 值得优先考虑因为连接管理和规则变更都集中化了。如果是创业团队或者业务在一个业务域内闭环开发自己就能搞定数据源那 JDBC 形态的轻量优势非常明显少一个中间件就少一个故障点。还有一个折中思路JDBC 和 Proxy 可以混用。同一个分片集群内部核心交易系统用 JDBC 形态获得最低延迟外围报表、数据对账服务用 Proxy 形态接入两边共享同一套分片规则。ShardingSphere 的配置模型是兼容的只要规则一致混用不会出问题。唯一要注意的是两边版本要保持一致否则分片算法在细节上的行为差异会让你怀疑人生。2. 分库分表边界怎么划先算账再动刀2.1 容量天花板什么时候该拆“分库分表”这四个字听着高级但拆分的动机必须来自数据事实而不是技术焦虑。我在评估一个表是否需要拆分时只算三笔账单表数据量、单条数据平均大小、业务峰值 QPS。单表数据量超过 2000 万行或者单表物理文件超过 50GB这是大多数 MySQL 实例的性价比拐点。不是说 2000 万行一定卡而是到这个量级DDL 变更开始变得痛苦索引维护成本上升冷热数据混在一起导致缓存命中率下降。但数据量大并不代表一定要分片。如果一张表每天新增 10 万行但业务只查最近 7 天的数据那做分区表或者定期归档就够了根本不需要引入分片。反过来一张表只有 500 万行但 QPS 冲到 1 万以上单库连接数吃紧这时候要考虑的是读写分离而不是分片。分片解决的是单库容量和单库写入瓶颈读写分离解决的是读流量分摊两者要分开判断。2.2 分片键的三条硬约束确定要拆之后选分片键是整个设计里最重要、也最不可逆的一步。我总结了三条约约束踩过任何一条后面都会很难受。第一条分片键必须来自高频查询条件。如果业务 90% 的查询都带 userId那分片键就选 userId。反例是把 orderId 当分片键但业务天天按用户查订单列表每次查询都跨全部分片做归并性能还不如不拆。第二条分片键的值必须稳定。用户 ID、商户 ID、设备 ID 这类天然稳定的字段最适合。千万别选手机号这种可能变更的字段一旦用户换绑手机号数据迁移会让你崩溃。第三条分片键的基数要足够大。如果按地区分片全国只有 30 多个省数据倾斜会非常严重广东一个省的订单量可能比其他省加起来还多。基数不够时数据分布会不均匀部分分片提前达到容量上限。这三条听着简单但我见过太多团队在评审时被业务方一句“我们以后肯定会按这个查”带偏选了一个当时看起来合理、半年后想改但已经改不了的字段。2.3 不是所有表都配分片广播表、单表与绑定表很多新手容易犯一个错为了追求架构的“一致性”把所有核心表都做了分片。实际上一个分片库里往往只有少数几张表真正需要分片其他表要么是广播表要么是单表。广播表是指配置、字典、地区这类数据量小但几乎所有分片都要关联查询的表。ShardingSphere 里可以配置成广播表每个分库都存一份完整数据写操作广播到所有分库读操作就近在本分片内完成。这样一来分片表 join 广播表时不需要跨库性能很好。代价是写放大但这类表本身写入频率极低完全可以接受。单表则是那些没有分片、只存在某一个库里的表。ShardingSphere 默认会对未配置分片规则的表走单表路由但我建议在配置里显式指定单表归属避免 SQL 里没带分片键时路由到错误的数据源。绑定表则是另一组容易漏配的东西。订单表和订单明细表如果都按 order_id 分片那就应该配置成绑定表。这样两张表在关联查询时会路由到同一个分片不会出现跨库 join。如果不配绑定表关联查询会变成全分片广播后归并性能下降一大截而且某些跨库关联场景下结果集还会出错。2.4 不拆也能活的替代方案分片不是银弹它的代价是让 SQL 能力退化。JOIN 受限、事务受限、聚合查询受限这些都是实打实的业务成本。所以在定方案之前我强烈建议先排查一遍是否有更轻量的解。最常见的替代是冷热归档。把 90 天前的订单挪到历史库业务表只保留热数据单表数据量立刻降一个数量级查询性能也上来了。这个方法成本低、见效快很多团队其实根本不需要分片只是没人做归档。其次是分区表。MySQL 的 range 分区按时间维度把数据拆到不同物理分区对业务透明DDL 和查询优化器也会针对分区做裁剪。缺点是分区不解决单库写入瓶颈且分区键选择受限。还有一个经常被忽略的点优化索引和 SQL。我在压测中见过不少“分片需求”最后发现是慢 SQL 没优化。一个走了错误索引的全表扫描分片只会让问题更隐蔽因为每个分片单独看都慢一点合起来就变成整体的性能灾难。3. 读写分离联动分片规则与主从规则的叠加顺序3.1 从一主两从到分片组数据源命名是关键读写分离本身不复杂复杂的是它和分片规则叠加在一起时的路由顺序。ShardingSphere 对这两者的处理顺序是先根据分片规则确定要访问哪个逻辑数据源再在逻辑数据源内部根据读写分离规则选择主库或从库。为了把这两个维度组合起来有一个非常关键的命名设计把“分片逻辑库”和“读写分离组”绑定成同一个数据源名称。比如计划拆成两个分片每个分片都是一主两从那么可以这样设计数据源物理主库ds_0_master、ds_1_master物理从库ds_0_slave_0、ds_0_slave_1、ds_1_slave_0、ds_1_slave_1逻辑分组ds_0对应分片 0 的主从组、ds_1对应分片 1 的主从组分片规则里 actualDataNodes 写的是逻辑分组名 ds_0、ds_1读写分离规则再把 ds_0 映射到它的主库和从库。这样分片层完全不感知主从关系读写分离层也完全不感知分片关系两层规则解耦得非常干净。3.2 配置示例分片读写分离的YAML落法我用 ShardingSphere 5.x 的配置风格给你一个可以直接改的骨架这是 JDBC 形态的 YAML 示例Proxy 的 server.yaml 思路完全一致。dataSources: ds_0_master: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.10:3306/ds_0 username: root password: root ds_0_slave_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.11:3306/ds_0 username: root password: root ds_0_slave_1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.1.12:3306/ds_0 username: root password: root # ds_1_master、ds_1_slave_0、ds_1_slave_1 同理略 rules: - !READWRITE_SPLITTING dataSourceGroups: ds_0: writeDataSourceName: ds_0_master readDataSourceNames: - ds_0_slave_0 - ds_0_slave_1 loadBalancerName: round_robin ds_1: writeDataSourceName: ds_1_master readDataSourceNames: - ds_1_slave_0 - ds_1_slave_1 loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBIN - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order_${0..31} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: user_id_mod keyGenerateStrategy: column: order_id keyGeneratorName: snowflake shardingAlgorithms: user_id_mod: type: MOD props: sharding-count: 32 bindingTables: - t_order,t_order_item注意一个细节分片规则里 dataSource 这一层现在写的是 ds_0、ds_1而不是物理主库或从库的名字。ShardingSphere 会先从 READWRITE_SPLITTING 路由规则拿到 ds_0 真正对应的物理数据源再执行 SQL。3.3 路由优先级先分片还是先走主从这里有一个很多初学者会搞反的优先级问题。当一条带分片键的查询进来比如select * from t_order where user_id 123ShardingSphere 的处理顺序是解析 SQL提取分片键 user_id 的值。根据分片算法计算出该用户的数据落在哪个逻辑数据源比如 ds_1。进入 ds_1 的读写分离逻辑根据负载均衡算法选择一个从库或主库执行。也就是说分片路由发生在读写分离之前。这也意味着你可能在不同的分片上分别走向不同机器分片 0 走了从库 A分片 1 走了从库 B这完全正常。但有一种情况会打破这个常规流程强制主库路由。如果你在事务里执行写操作或者手动指定了 Hint 走主库那么读写分离规则会被绕过整个事务的所有 SQL 都会落到主库执行。这是 ShardingSphere 保证事务一致性的底线任何读写分离框架都不会允许事务中的读请求走从库。3.4 事务、强制路由与延迟问题关于事务和读写分离联动有两条实操经验值得记住。第一条事务内的查询不要指望走从库。哪怕一个事务里只有一条 select 和一条 update只要事务一开启select 也会被固定到主库。这是正确设计但我见过不少团队不理解以为全链路读写分离能把事务内的读也分摊到从库结果压测时主库压力还是很大然后误判是路由配置有问题。第二条从库延迟是读写分离联动时最容易踩的坑。业务刚写完主库立刻查从库可能查不到最新数据。解决方案有几种对强一致场景用 Hint 强制走主库对最终一致场景接受短暂延迟更精细的做法是把读写分离权重做成可配置某些关键查询固定路由到主库。ShardingSphere 里用 Hint 强制走主库的姿势是HintManager hintManager HintManager.getInstance(); hintManager.setWriteRouteOnly(); // 执行需要走主库的查询 hintManager.close();这个 API 在 JDBC 形态下非常方便但要小心 HintManager 是线程绑定的用完一定记得 close否则连接归还到连接池后下一次复用时可能还带着主库路由标记。4. 一次真实事故主库被打满从库在旁看戏4.1 现象与初步判断讲完配置我用一次真实事故来展示分片读写分离联动时怎么排查问题。那是一个用了 ShardingSphere Proxy 的电商订单服务分 4 个库每库 1 主 2 从。上线初期一切正常但大促压测时发现主库 CPU 跑到 90% 以上而从库 CPU 只有 20% 左右。一眼看去读写分离根本没有生效——大量读流量全压在主库上。第一反应是检查配置。我把 server.yaml 翻出来读写分离规则、负载均衡算法都看着没问题从库节点的连接也正常。然后又怀疑是不是 Proxy 版本 bug查了官方 issue版本也没有已知的路由异常。4.2 排查链路从慢SQL到事务传播既然配置没问题那就从 SQL 日志入手。开启 Proxy 的 SQL 日志后我截了一段典型的读请求日志发现所有 select 语句后面跟着的actual data source全是 master 节点。更奇怪的是这些 select 并不是在事务里发出的部分单条 select 也走了主库。接着排查应用侧代码才发现问题出在事务传播机制上。这个订单服务的大部分接口都挂了Transactional有些是类级别或者公共方法上的注解导致事务范围被放大。虽然接口内部大部分是读操作但只要事务一开启ShardingSphere 的读写分离就会把整个事务内的 SQL 固定到主库。结果就是——每个请求进来哪怕只写了一行 update整个请求的所有查询都走了主库。从库自然在旁边看戏。4.3 根因事务边界过大把读流量全绑在主库上这才是根因读写分离的“事务内全部走主库”策略没有错错的是事务边界本身。类上挂Transactional导致本可以走从库的只读查询也被圈进了一个写事务里。随着 QPS 上来主库连接被大量只读事务占满CPU 自然被打爆。这个问题的排查难点在于配置完全正确日志也看不出明显异常唯一线索是“读流量没有分布到从库”。如果你也遇到类似场景建议优先按这个链路排查先确认路由日志中的 actual data source 是不是主库再看调用链中事务注解的粒度最后才回头怀疑配置。4.4 修复验证与监控补充修复方案不是去改 ShardingSphere 的强制路由开关而是收缩事务边界。把Transactional从类级别移到真正需要原子性的方法上只读接口去掉事务注解写操作单独开事务。改完之后重新压测从库的读流量立刻上来了主库 CPU 降到 30% 以下。这次事故之后我给团队立了两条规矩。第一事务注解不允许加到类级别统一由组长 review。第二监控面板必须同时看主库和从库的 QPS 占比如果从库长时间没有读流量路由一定有问题。ShardingSphere 的 SQL 日志里每次路由都会打印 actual data source配合日志采集做路由分布统计比看数据库侧指标更直观。5. 扩容这事别等上线再想动态扩容的三种准备姿势5.1 取模分片为什么扩容痛苦分片方案确定后最不想面对但又必须面对的问题就是扩容。很多团队用的是最简单的取模分片user_id % 2两个库。数据量涨上来需要扩到 4 个库问题就来了——原来user_id % 2落在库 0 的数据用user_id % 4计算后可能落在库 0、库 2、库 3 任意一个位置。这意味着几乎全量数据都要重新分布不是简单的拷贝而是逐条重新计算路由并迁移。ShardingSphere 的扩容有专门的工具设计思路但拿来即用的自动化工具远没有你想的成熟。更常见的方式是双写迁移业务写入时同时写新旧两个集群存量数据做一次全量迁移校验一致后切换读流量最后摘除旧集群。整个过程耗时和风险都不小。5.2 方案一一致性哈希如果你预判到未来还要多次扩容选分片算法时就不要用 MOD改用一致性哈希。一致性哈希的核心思想是把分片键映射到一个 2^32 的哈希环上节点变更时只影响环上相邻节点的数据。ShardingSphere 的 HASH_MOD 算法结合固定分片数量也能起到类似效果但严格的一致性哈希算法在扩容时迁移成本远低于取模。代价是查询时多一次哈希计算且数据分布可能不如取模均匀。实际使用中我建议把一致性哈希和虚拟节点结合起来让数据分布更平滑。ShardingSphere 5.x 中可以通过自定义分片算法来实现代码量不大但收益非常明显。5.3 方案二双写迁移影子表如果已经用了取模分片又想平滑扩容最稳妥的姿势是双写加影子表。大致流程是新建目标分片集群配置好新的分片规则。应用层双写写旧集群的同时按新规则写入新集群。存量数据全量迁移迁移时对每一条数据计算新路由写入新集群。做数据校验对比新旧集群的数据量和关键字段指纹。切换读流量到新集群观察一段时间确认无误后摘除旧集群。这里有个实用技巧影子表。在旧集群里创建一张与新规则对应的“影子表”每次写操作除了写原有表还按新规则把数据写到影子表对应的物理位置。这样可以在不切换流量的情况下提前验证新路由规则的准确性。ShardingSphere 的分片规则本身不限制你表名影子表就是利用了这一点做预演。5.4 方案三按时间维度分片如果你的业务有天然的时间属性比如订单表、流水表、日志表最省心的扩容方案是按时间分片。按月分片每个月自动落入新的物理表业务增长只需要增加新的分片不需要迁移历史数据。缺点是单月数据量超过单表容量时还是会撞墙因此更适合流水类、可归档的数据。时间分片可以跟其他方案组合比如按用户取模分库、按时间分表两层规则一起上。但要注意组合规则会让 SQL 路由判断更复杂——如果查询条件里只有时间没有用户 ID那就只能路由到一个库内的多个表如果都有才能精确落到单库单表。业务方对查询条件的约束越清晰组合分片越安全。我在评估扩容方案时还有个原则宁可提前把分片数量做大。比如现在只需要 4 个库但规则直接配成 32 个分片把 32 个分片映射到 4 个物理库上。这样未来扩容时只需要把分片重新映射到更多物理库数据迁移范围从“全量”变成“部分分片”。这个思路和 Proxy 的逻辑库、物理库解耦是一致的很早就在 ShardingSphere 的设计里被支持了。最后再说几句我做了这么多年的分库分表最大的体会是这个技术坑不在框架本身而在于你把它当成一个纯技术组件来用忽略了它和数据增长规律、业务查询特征、团队运维能力之间的联动。选 JDBC 还是 Proxy本质上是在选你愿意为性能损耗、规则集中管理、多语言接入付出多少代价分库分表的边界本质上是对业务数据的耐心盘点读写分离联动的坑本质上又是事务边界和架构认知的问题。把这些想透了ShardingSphere 对你来说就是一个顺手工具而不是一个需要天天救火的复杂系统。如果你现在正要上这套方案我的建议是先把第 4 节那种“路由看起来不对”的排查手段准备好监控从 SQL 日志直接抓 actual data source别等出事故了再翻。毕竟分库分表最大的特点就是一旦上线数据分布已经落到物理库表里回头的成本远比你想的高。
返回列表