尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot 集成 ShardingSphere 实战:分片策略、全局 ID 与跨库查询避坑指南

Spring Boot 集成 ShardingSphere 实战:分片策略、全局 ID 与跨库查询避坑指南 1. 单库撑不住时再动分库分表业务跑顺了数据量上来MySQL 迟早会给你脸色看。单表破两千万或者容量逼近 500G 的时候B 树层级变深、Buffer Pool 命中率往下掉、主从同步延迟肉眼可见这时候靠堆硬件已经填不平查询延迟的坑。索引失效、锁竞争、慢查询日志刷屏都是常态。分库分表不是银弹但确实是把写入瓶颈打散、用空间换时间的实在办法。Apache ShardingSphere-JDBC 走的是轻量级胖客户端路线JAR 包一引配置一写业务代码基本不用改就能把 SQL 路由、改写、结果归并全包了。这篇笔记基于 Spring Boot 3.x ShardingSphere 5.5.0 整理不扯虚的直接上生产环境能落地的配置和踩过的坑。2. 核心配置与逻辑映射ShardingSphere-JDBC 的链路就四步解析 SQL → 算路由 → 改写 SQL → 拿结果合并。对业务来说你只管操作逻辑表底层物理节点怎么分框架自己算。依赖引入dependencygroupIdorg.apache.shardingsphere/groupIdartifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactIdversion5.5.0/version/dependency!-- 持久层按需选JPA 或 MyBatis 均可 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-jpa/artifactId/dependencyYAML 配置要点5.x 语法5.x 的配置比 4.x 严谨不少缩进错一点直接启动报错。下面是一份跑通读写分离分表的基准配置spring:shardingsphere:datasource:names:ds-write,ds-read-01,ds-read-02ds-write:jdbc-url:jdbc:mysql://primary-host:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghaidriver-class-name:com.mysql.cj.jdbc.Driverusername:rootpassword:${DB_WRITE_PWD}ds-read-01:jdbc-url:jdbc:mysql://replica01-host:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghaidriver-class-name:com.mysql.cj.jdbc.Driverusername:readerpassword:${DB_READ_PWD}ds-read-02:jdbc-url:jdbc:mysql://replica02-host:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghaidriver-class-name:com.mysql.cj.jdbc.Driverusername:readerpassword:${DB_READ_PWD}rules:sharding:tables:t_order:actual-data-nodes:ds$-{0..1}.t_order_$-{0..1}table-strategy:standard:sharding-column:user_idsharding-algorithm-name:t_order_inline# 读写分离下的分片必须显式声明库路由否则全路由database-strategy:standard:sharding-column:user_idsharding-algorithm-name:db_inlinesharding-algorithms:t_order_inline:type:INLINEprops:algorithm-expression:t_order_$-{user_id % 2}db_inline:type:INLINEprops:algorithm-expression:ds$-{user_id % 2}binding-tables:-t_order,t_order_itemreadwrite-splitting:data-sources:readwrite_ds:write-data-source-name:ds-writeread-data-source-names:ds-read-01,ds-read-02load-balancer-name:round_robinload-balancers:round_robin:type:ROUND_ROBIN几个概念不用死记上线跑两遍就明白了逻辑表代码里写的t_order对 ORM 框架透明。真实数据节点ds0.t_order_0这种物理表名框架按算法动态拼出来的。绑定表主从表比如订单和订单项如果分片键一致JOIN 时框架只会把 SQL 打到同一个物理库避免笛卡尔积爆炸。这招在生产环境能省掉一半的跨库查询开销。广播表像字典、地区码这种变动少、数据量小的表配成广播后每个库都会存一份JOIN 时直接本地查不用跨分片拉数据。3. 分片算法怎么选选错算法后期扩容就是灾难。别一开始就追求“完美均匀”先看查询场景。Hash 取模最常见user_id % N写起来简单数据分布也均匀。但硬伤是扩容加节点后余数全变了老数据得搬否则旧路由查不到新节点的数据。另外按 Hash 分的表完全没法走范围扫描WHERE create_time BETWEEN这种查询只能全分片广播。线上建议初始分片数留点余量比如按 32 或 64 分后期通过逻辑路由合并或双写过渡。范围分片适合流水号、账单周期这类天然递增的字段。代码实现 SPI 接口即可比如每 100 万切一张表。范围查询性能极好索引命中率高。但写入热点很要命最新数据全砸在同一张表上MySQL 的页分裂和锁竞争会拉高 RT。折中方案是“范围哈希”复合路由或者把热点表单独拆出来。// 5.x 范围分片 SPI 实现示例publicclassRangeShardingAlgorithmimplementsStandardShardingAlgorithmLong{OverridepublicStringdoSharding(CollectionStringavailableTargetNames,PreciseShardingValueLongpreciseShardingValue){longidpreciseShardingValue.getValue();longindex(id/1000000L)%availableTargetNames.size();returnavailableTargetNames.stream().filter(n-n.endsWith(String.valueOf(index))).findFirst().orElseThrow(()-newIllegalArgumentException(No valid target));}}时间维度分片按年月或天做日志、流水归档很顺手历史数据直接DROP TABLE或者迁到冷存储生命周期管理特别干净。缺点是跨年跨月查询必须拼多表得配合二级索引或者 ES 做兜底。扩容别硬来线上标准动作是四步走应用层开双写 → 存量数据全量迁移增量 Binlog 追平 → 数据抽样校验MD5/Checksum 比对 → 灰度切读稳了再切写。切流期间旧拓扑别急着删留两周回滚窗口真出问题能一键切回去。4. 全局 ID 生成别依赖数据库自增分片后AUTO_INCREMENT直接废了必须上分布式 ID 生成器。雪花算法64 位结构大家熟时间戳机器位序列号QPS 轻松破百万。但线上最怕时钟回拨哪怕回拨 1 毫秒ID 就可能重复。ShardingSphere 内置实现加了max-vibration-offset和回拨容忍窗口配置500ms左右基本够用。如果机房 NTP 漂移严重建议自己封装一层时钟回拨时直接抛异常阻塞或切备用算法。号段模式是从数据库里批量拿区间比如[1001, 2000]在应用内存里自增。美团 Leaf 的 DB 模式就是这个路子。好处是 ID 严格单调递增对 MySQL 页分裂友好而且彻底不怕时钟问题。代价是得维护一张号段表不过 QPS 打过来时DB 压力其实极小因为一次拿一万次用。生产集成 Leaf 现在直接用官方 Starter 就行手动Bean硬塞配置容易漏掉初始化钩子。高可用靠两点多节点部署本地号段提前刷新。号段快用完时比如剩 20%主动去 DB 拉新段断网或 DB 挂了就降级到雪花算法虽然可能不单调但保证业务不中断。5. 读写分离与一致性陷阱读多写少的场景读写分离是标配。ShardingSphere 按 SQL 类型自动路由INSERT/UPDATE/DELETE走主库SELECT走从库。负载均衡支持轮询、随机、权重事务内可以用TRANSACTION_RANDOM保持会话粘性。但主从延迟是绕不开的坎50ms 到几秒都有。“写完马上读读不到最新数据”是线上最常见的客诉。解决思路很直接关键链路强制主库用HintManager强路由。注意一定要在finally里close()ThreadLocal 没清干净会导致后续请求全走主库主库直接被打挂。HintManagerhintManagerHintManager.getInstance();try{hintManager.setWriteRouteOnly();orderService.create(dto);}finally{hintManager.close();}延迟容忍分级支付回调、订单状态变更这种强一致场景直接FORCE_MASTER商品列表、评价列表读从库没问题延迟个几百毫秒用户无感知。会话粘滞如果业务允许把同一个用户的请求路由到固定从库结合 Nginx Cookie 或网关 Session能大幅降低读己之写不一致的概率。6. 跨片查询与慢 SQL 治理分片后原来一条 SQL 搞定的事现在可能要框架帮你拼结果。GROUP BY、ORDER BY、LIMIT一旦带错条件就会触发全路由内存直接 OOM。框架底层用的是流式归并和内存归并。聚合函数像COUNT/SUM/MAX能下推先在各分片算完再汇总AVG会被改写成SUM() / COUNT()二次计算。最头疼的是ORDER BY ... LIMIT offset, size如果 offset 很大每个分片都得扫全量再排序性能断崖式下跌。实战优化手段绑定表必须配主从表分片键一致JOIN 时只路由到一个库物理连接数直接减半。分片键选择要狠查询条件里没带分片键的 SQL一律打回重做。SaaS 多租户场景常用tenant_id user_id做复合路由或者对大商户单独拆库避免数据倾斜。深度分页换思路别死磕LIMIT。改用游标分页WHERE id last_max_id LIMIT 20或者先拿 ID 列表再IN查询详情。列表页直接对接 Elasticsearch主库只做权威写入和事务保障查询压力卸干净后DB 负载能降 60% 以上。慢 SQL 拦截开sql-show: true看路由日志全分片扫描的 SQL 直接标红告警。配置max-connections-size-per-query: 16限制单查询并发线程防止一条烂 SQL 拖垮整个连接池。7. 连接池与分布式事务分片架构最容易踩的坑是连接数膨胀。每个分片独立维护连接池10 个分片 × 20 连接 200 个连接。MySQL 默认max_connections通常也就几百稍有不慎就Too many connections。HikariCP 调参别套公式按实际压测来。单库连接池maximum-pool-size控制在 15~25 之间足够配合minimum-idle: 5和合理的idle-timeout自动回收。启动期连接风暴很常见把initialization-fail-timeout设长点或者加个spring.datasource.hikari.initializationFailTimeout: -1避免健康检查把池子撑爆。分布式事务能不用就不用。XA 模式强一致但性能损耗三成起步两阶段提交卡住是常事。生产更倾向本地事务MQ 最终一致性订单落库成功发个可靠消息库存、积分异步消费补偿。实在绕不开跨分片强一致上 Seata-AT记得配好undo_log表超时阈值调到业务可接受的上限别被默认值坑了。8. 上线 checklist分片键是否覆盖 90% 以上查询条件绑定表配置是否与物理分片规则严格一致HintManager 是否全部闭环释放ThreadLocal 泄漏是隐形炸弹。从库延迟监控是否接入读己之写不一致有无降级预案连接池总数 × 分片数 是否小于 DB 最大连接数 80%全局 ID 生成器有无时钟回拨/号段耗尽的降级逻辑分库分表不是技术炫技是数据规模倒逼出来的妥协。早期业务别急着拆单表索引优化、缓存分层、归档策略足够应付 80% 的场景。等写入 TPS 真正撞墙、I/O 瓶颈肉眼可见时再按这套规范平滑演进。架构没有最优解只有当前业务规模和团队运维能力下的最优平衡。
返回列表