Sharding-JDBC实战:5大模块攻克分库分表痛点

订单系统的响应突然变慢了:本来200毫秒能查出的订单,目前要等到1.5秒,有时候数据库连接池被耗尽,部分交易还直接超时失败。用户量冲破500万后,订单表数据以每月80万条的速度增加,半年的时间里从300万条涨到780万条。这是触发这次连锁反应的直接证据。

Sharding-JDBC实战:5大模块攻克分库分表痛点

问题暴露后,排查从表结构和索引入手。MySQL在单表达到必定规模时,B+树的层级会增多,索引命中率下降,查询路径变长,响应自然变慢。实践经验告知人,单表超过500万行就需要警惕,逼近1000万行时,哪怕索引再好,也很难避免大量全表扫描。我们的情况正处在这个危险区间,表现出来就是查询延迟剧增、连接池占满、交易超时。

业务增长速度放在前面再说,技术手段要跟上。经过比选,团队倾向于做分库分表。对于以Java为主的应用,Sharding-JDBC被列为首选。缘由有两点:一是它以Jar包形式嵌入应用,属于客户端直连,不需要额外部署代理服务,二是性能损耗小,实测低于7%,比起走代理层那种20%+的损耗好许多。最重大的一点是避免把代理层当成单点故障。架构上可以理解成三层:应用接入层(内嵌驱动逻辑)、路由决策层(按规则把SQL拆分成多个子查询)、底层连接层(负责和各个物理库通信与读写分离)。举个简单例子:执行 SELECT * FROM t_order WHERE order_id = 12345 AND user_id = 678,这个查询会先被路由器解析成路由条件,确定目标分片,再把SQL下发到对应的物理表,最后把结果聚合返回给应用。

Sharding-JDBC实战:5大模块攻克分库分表痛点

和其他方案对比时,差别很明显。MyCat和Sharding-Proxy都是独立服务,需要单独运维,支持多语言,但在性能损耗和运维复杂度上不占优势。MyCat社区活跃度下降,逐渐被ShardingSphere等替代。结论上,纯Java生态优先用Sharding-JDBC,多语言场景思考Sharding-Proxy。

实战那块,项目基线是Spring Boot 2.7.18 + Sharding-JDBC 5.1.1。目标是把订单表做成2库4表并实现读写分离。工程里主要改动聚焦在三个地方:实体类(要加分片键和路由注解)、Mapper接口(写法上无侵入,但要思考分片规则)、测试验证(包括单库、跨库和读写分离的用例)。实施过程中遇到几个关键问题,需要提前准备对策。

Sharding-JDBC实战:5大模块攻克分库分表痛点

第一个问题是跨库事务。简单事务只能保证单库的原子性,像订单写入和库存扣减分布在不同数据库时,单库事务无法保证全局一致。这种场景有几条路:一,把相关表设计成同分片键(把主从表绑定起来),这样能在同一库内完成事务;二,使用分布式事务框架支持XA或者BASE策略,Sharding-JDBC支持XA/BASE;三,改成最终一致性方案,用可靠消息、补偿流程(SAGA/TCC)来保证业务层面的数据一致。每种方案复杂度和性能影响不同,选的时候要把业务容忍度和实现成本都算清楚。

第二个常见痛点是跨库JOIN。我们的用户表按userId分片,订单表按orderId分片,做“用户订单列表”查询时会触发跨库关联。可选方案有几种:把主子表按一样分片键做绑定(速度快,复杂度低);把小表做广播表(适合字典类小表);在写入时冗余一些字段,读查询时直接用冗余字段定位分片(读多写少场景合适);要是不行,就只能在应用层把多片数据拉回来再聚合。每种方案的实现复杂度和性能差异大,绑定表是首选。

Sharding-JDBC实战:5大模块攻克分库分表痛点

再说分页和大偏移问题。在分片环境里,像 LIMIT 100000,20 这种写法会被改成 LIMIT 0,100020 在每个分片上执行,结果导致大量不必要的数据在网络间传输。规避办法是改用基于游标的分页(keyset pagination),或者先在每个分片做聚合统计后再取页面,尽量避免大偏移。实际优化里,替换为 order by + where (last_id) 的翻页方式,延迟从秒级回到了可接受范围。

分片规则的选取也很关键。常见算法有取模、范围、哈希。取模分片分布均匀但扩容难(需要数据迁移),适合按用户ID、订单ID这类固定键;范围分片扩容容易,按时间分片方便归档,但可能有数据倾斜;哈希分片读写负载均匀,扩容中等,适合高并发场景。实测在1000万数据量下,取模分片查询延迟比范围分片低大致15%到20%,但范围分片在按时间切分和归档时更方便。

Sharding-JDBC实战:5大模块攻克分库分表痛点

运维层面,数据库连接池参数直接决定系统吞吐。生产环境中我们采用的提议值如下:maximumPoolSize 设在20到50之间,一般按 CPU 核心数 * 2 + 有效磁盘数 来估算;minimumIdle 保持在 maximumPoolSize 的30%,大致5到15;connectionTimeout 设为3000毫秒,避免长时间阻塞;maxLifetime 设为1800000毫秒(30分钟),小于数据库的 wait_timeout。配置不当会让连接被频繁回收或长时间占用,都会影响并发处理能力。

数据迁移从单库到分库分表时,按照双写迁移方案来做比较稳妥。具体步骤是:先在应用里做双写(旧表和新分片同时写入),把历史数据按照规则批量切分同步到新分片;在同步期间做数据一致性校验(哈希校验或行级比对);待一致性确认后,把读请求切换到分片库,并把旧表设为只写或最后下线。过程中要准备补偿流程、慢查回放和降级策略,业务不能由于迁移中断。

热点数据可以通过三级缓存协同来缓解。把本地缓存、分布式缓存和数据库配合起来,热点命中率能从大约50%提升到80%,数据库压力明显下降。注意缓存一致性和过期策略,不然会引入新的复杂性。

一些细节优化也要做到位。分片键要和常用查询条件匹配,避免只用 user_id 查询但分片是 order_id 的情况,那样会触发全表扫描。索引要覆盖常见的查询场景,连接池和SQL超时要合理设定,慢查询要定期分析并优化。

测试结果是可以量化的。把订单表拆成2库4表并开启读写分离后,常用查询延迟从1.5秒回落至200~300毫秒区间,连接池占用稳定,交易超时的情况大幅减少。核心实现基于 Spring Boot 2.7.18 + Sharding-JDBC 5.1.1,相关实体、Mapper和测试用例都已在开发环境跑通,订单创建和库存扣减在启用XA的分布式事务下完成了跨库一致性的验证。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容