行业资讯
📅 2026/7/23 9:09:58
Day30-数据层 × 中间件AI化篇:MySQL分库分表:ShardingSphere
你是否遇到过这样的场景单表数据涨到 2亿行以上单表索引文件超过 60GBMySQL 主库 CPU 常年 80% 以上。CTO 的解决方案是升级 MySQL 8.0、上 SSD、扩容内存。但三个月后问题原样回来了。一、什么时候才需要分库分表很多团队因为焦虑而分片结果分布式事务、跨片查询、运维复杂度一起爆炸。先明确三个信号三缺二就别动信号典型阈值说明数据量单表 5000 万行或物理大小 50GBB 树深度增加随机 IO 性能恶化并发量单库 TPS 2000或活跃连接 300连接池、锁竞争、CPU 成为瓶颈业务边界能找到稳定、高频的查询维度通常是user_id、order_id、tenant_id如果你只是 2000 万行但并发低、查询简单先用分区表 归档就能解决别急着分片。分片是把双刃剑分错片键比不分片更痛苦。二、分片策略取模、范围、复合怎么选ShardingSphere 支持四种分片策略但生产用得最多的其实是三种取模Hash按分片键取模。数据最均匀点查最快但范围查询会触发全路由。范围Range按时间或 ID 范围。适合归档、冷热分离但新数据容易集中在尾部热点。复合先分库再分表比如user_id % 2决定库order_id % 8决定表。这是最常见的订单场景。代码图表选分片键的黄金法则高频查询条件里必须出现。避免倾斜比如按tenant_id分但有个大客户占 80% 数据必须做二次分片。** join 查询尽量落在同一分片**订单表和订单项表用相同的order_id分片避免跨片 join。三、Spring Boot 3 ShardingSphere 5.5.0 实战配置ShardingSphere 5.5.0 提供了ShardingSphereDriverSpring Boot 3 只需要把它配置成普通数据源即可。下面是经过生产验证的最简依赖和配置。1. Maven 依赖dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc/artifactId version5.5.0/version /dependency !-- Spring Boot 3 必须额外引入 Java EE 8 的 JAXB 实现否则启动会报 JAXB 类缺失 -- dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version2.3.8/version /dependency dependency groupIdorg.yaml/groupId artifactIdsnakeyaml/artifactId version1.33/version /dependency版本依赖Spring Boot 3.2.x JDK 17 ShardingSphere 5.5.0 MySQL 8.0。如果 SnakeYAML 版本和 Spring Boot 默认不一致需要显式指定 1.33。2. application.ymlspring: main: allow-bean-definition-overriding: true # 必须让 ShardingSphere 覆盖默认 DataSource datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:sharding.yaml sql: init: mode: never # 禁用 Spring Boot 自动初始化 SQL防止误建表3. sharding.yaml核心配置要点actualDataNodes用 Inline 语法ds_${0..1}.t_order_${0..7}表示 2 库 × 8 表共 16 张物理表。keyGenerateStrategy让 ShardingSphere 自动生成雪花 ID 作为order_id。bindingTables把t_order和t_order_item绑定保证同order_id的明细落在同一分片join 不会跨库。四、自定义分片算法从「表达式」到「Java 类」Inline 表达式适合简单取模但生产里经常遇到复杂规则比如按用户注册时间分库、按订单尾号分表或者大客户单独走一个分片。这时需要自定义算法。ShardingSphere 5.5.0 支持CLASS_BASED类型只要实现StandardShardingAlgorithm接口即可。package com.example.sharding; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.util.Collection; /** * 按用户 ID 取模分库支持精确路由不支持范围查询。 * 适用于Spring Boot 3 ShardingSphere 5.5.0 */ public class UserDbShardingAlgorithm implements StandardShardingAlgorithmComparable? { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueComparable? shardingValue) { long userId ((Number) shardingValue.getValue()).longValue(); String target ds_ (userId % 2); return availableTargetNames.stream() .filter(name - name.equals(target)) .findFirst() .orElseThrow(() - new IllegalStateException(No available target: target)); } Override public CollectionString doSharding(CollectionString availableTargetNames, RangeShardingValueComparable? shardingValue) { // 生产环境里范围查询建议走 Elasticsearch 或 OLAP不要强制全路由 throw new UnsupportedOperationException(范围查询不支持自定义路由请使用业务索引或离线数仓); } Override public String getType() { return CLASS_BASED; } }对应的 YAML 注册shardingAlgorithms: db_mod: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.example.sharding.UserDbShardingAlgorithm自定义算法的好处是业务规则写在代码里能写单测、能版本控制比把复杂 Groovy 表达式塞在 YAML 里安全得多。五、数据迁移如何做到用户无感知分库分表最大的风险不是技术是迁移过程。我见过的翻车案例 90% 是因为直接INSERT INTO ... SELECT到新库结果业务还在写旧库两边不一致。推荐方案双写 增量校验 灰度切流。上线前三个步骤缺一不可全量迁移把旧表数据按分片键重新导入新分片。增量双写业务代码同时写旧库和新库保证实时一致。一致性校验按用户 ID 段分批对比记录数与关键字段哈希。灰度切流先切 1% 流量观察 24 小时再全切。下面是一个可运行的迁移脚本骨架用JdbcTemplate直接操作package com.example.migration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; import java.util.List; import java.util.Map; Service public class OrderMigrationService { private final JdbcTemplate oldTemplate; private final JdbcTemplate newTemplate; private static final int BATCH_SIZE 500; public OrderMigrationService(DataSource oldDataSource, DataSource shardingDataSource) { this.oldTemplate new JdbcTemplate(oldDataSource); this.newTemplate new JdbcTemplate(shardingDataSource); } /** * 按 id 段分批迁移避免一次性加载全表。 * 实际生产请用 Canal / Flink CDC 做增量同步这里展示核心逻辑。 */ Transactional(rollbackFor Exception.class) public int migrateBatch(long startId, long endId) { String query SELECT order_id, user_id, amount, status, create_time FROM t_order_old WHERE order_id ? AND order_id ? ORDER BY order_id LIMIT ?; ListMapString, Object rows oldTemplate.queryForList(query, startId, endId, BATCH_SIZE); String insert INSERT INTO t_order (order_id, user_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE amountVALUES(amount), statusVALUES(status); newTemplate.batchUpdate(insert, rows, BATCH_SIZE, (ps, row) - { ps.setLong(1, (Long) row.get(order_id)); ps.setLong(2, (Long) row.get(user_id)); ps.setBigDecimal(3, (java.math.BigDecimal) row.get(amount)); ps.setInt(4, (Integer) row.get(status)); ps.setTimestamp(5, (java.sql.Timestamp) row.get(create_time)); }); return rows.size(); } /** * 一致性校验按 user_id 段统计两边记录数。 */ public boolean verifyCount(long userIdStart, long userIdEnd) { String sql SELECT COUNT(*) FROM t_order WHERE user_id ? AND user_id ?; Integer oldCount oldTemplate.queryForObject(sql, Integer.class, userIdStart, userIdEnd); Integer newCount newTemplate.queryForObject(sql, Integer.class, userIdStart, userIdEnd); return oldCount ! null oldCount.equals(newCount); } }宁可慢不可错。上线前至少要跑三轮全量校验指标差异为零才能切流。六、跨分片查询、排序、分页不要硬刚分库分表后不带分片键的查询会触发「全路由」比如SELECT * FROM t_order ORDER BY create_time LIMIT 1000000, 10。这条 SQL 在 16 个分片上各跑一遍再在内存里排序合并结果不是慢是直接 OOM。解决思路有两个避免跨片让分片键天然出现在所有查询路径里。最经典的办法是把user_id的后几位嵌入order_id。离线聚合报表类查询走同步到 Elasticsearch、ClickHouse 或 Doris 的宽表。下面是一个把user_id低 3 位嵌入order_id的生成器这样订单号本身就携带了分片信息package com.example.sharding; import java.util.concurrent.atomic.AtomicLong; /** * 生成携带分片信息的订单号。 * 低 3 位 user_id % 8用于保证同一用户的订单落入同一表分片。 * 高位使用递增序列保证趋势递增且唯一。 */ public class ShardingOrderIdGenerator { private static final AtomicLong SEQUENCE new AtomicLong(System.currentTimeMillis()); public static long generate(long userId) { long seq SEQUENCE.incrementAndGet(); // 低位保留 user_id 后 3 位使 order_id % 8 user_id % 8 return (seq 3) | (userId 0x7L); } public static long extractUserIdHint(long orderId) { return orderId 0x7L; } }配合前面的tableStrategyorder_id % 8同一用户的所有订单会稳定落到同一张表。查询时只要拿到order_idShardingSphere 就能直接路由到目标分片不会全库扫描。如果你确实要做跨分片分页ShardingSphere 5.5.0 提供了联邦查询Federation但生产上我不建议对大数据量使用。更稳妥的做法是在 ES 里建立一份订单宽表分页走 ES。或者把分页条件限制在「用户维度 时间」让 SQL 带上分片键。七、建议先分表再分库。同机房内分表带来的复杂度比分库小得多很多单库压力在分表 8~16 片后就能缓解。别一上来就把数据拆到多个 MySQL 实例给自己挖坑。禁止在业务代码里硬编码分片逻辑。所有路由交给 ShardingSphere业务只写INSERT INTO t_order。否则三年后你的分片逻辑会散落在 17 个项目的 300 个文件里想迁移都迁移不了。上线前必须做影子流量对比。把线上流量复制一份到新分片集群跑 24 小时对比旧库和新库的 SQL 结果、延迟、错误率。这是我发现分片 bug 最有效的办法没有之一。分库分表不是架构的终点而是业务规模倒逼下的妥协。它解决的是「扛不住」的问题不是「写得爽」的问题。你分得越优雅后面的运维就越轻松。下篇预告Day 31《MySQL主从复制与读写分离原理、延迟排查与故障切换》。分库之后主从同步和读写分离就是下一道必须跨过的坎我们明天见。