【码动四季】Spring Boot 多数据源 + 分库分表:AtomCode 如何保障 200+ 个 SQL 迁移零错误
摘要:订单库从单库迁移到 ShardingSphere 分库分表,最痛的不是写分片配置,而是 200+ 个 SQL 的兼容性验证。手动逐条核对要 5 天还容易漏,AtomCode 用 Rules 校验分片键规范、Skill 生成迁移脚本、Agent 批量验证 SQL 兼容性,最终 200 个 SQL 迁移零错误,效率提升 60%。本文完整记录从单库瓶颈到分库分表落地,再到 AtomCode 介入的全过程,含 Spring Boot 3.4 + ShardingSphere 5.5 核心代码 300+ 行。
1. 场景:订单单库扛不住了
瓶颈爆发
2026 年 Q1,我负责的中型电商平台日均订单突破 30 万,大促期间峰值 QPS 飙到 8000。单库 MySQL 8.0(16 核 64G)开始频繁告警:
- 慢查询飙升:订单表 2000 万+行,
SELECT * FROM t_order WHERE user_id = ? AND status = ?跑到 2.3s - 主从延迟:高峰期 binlog 延迟超过 10s,读库数据不准
- DDL 锁表:加字段、加索引时业务超时,运营投诉不断
- 磁盘告急:单表数据文件 180GB,备份恢复时间超过 4 小时
技术方案评审会上,分库分表成为共识,但争议焦点是:200+ 个 SQL 怎么迁移?
迁移的核心难题
分库分表不是改个配置就完事的。SQL 迁移要解决的问题清单:
| 问题类型 | 典型案例 | 风险等级 |
|---|---|---|
| 分片键缺失 | SELECT * FROM t_order WHERE status = ? 无分片键 |
🔴 高 |
| 跨分片 JOIN | SELECT o.*, u.name FROM t_order o JOIN t_user u ON o.user_id = u.id |
🔴 高 |
| 聚合函数不兼容 | SELECT COUNT(*) FROM t_order 全表扫描 |
🟠 中 |
| 排序分页错乱 | ORDER BY create_time LIMIT 10 OFFSET 100 跨分片偏移 |
🟠 中 |
| 自增主键冲突 | INSERT INTO t_order (...) 多分片 ID 重复 |
🔴 高 |
| 多数据源切换 | 同一事务中既有订单操作又有商品操作 | 🟡 中 |
手动逐条检查 200+ 个 SQL?按团队之前的经验,5 个人 5 天还漏了 3 个,上线后生产事故。
我决定这次用 AtomCode 来管住 SQL 迁移质量。
2. 分库分表架构设计
2.1 整体架构
先看架构全貌,再逐步拆解。
2.2 分片策略决策
根据业务特点,我们选了 user_id 取模分片 + 订单号基因法:
| 决策项 | 选择 | 理由 |
|---|---|---|
| 分片键 | user_id | 90% 查询带 user_id,买家维度查询最高频 |
| 分片算法 | 取模 % 4 | 4 个库,数据均匀,实现简单 |
| 分表策略 | 每库 16 张表 | 单表控制在 300 万行以内 |
| 订单号生成 | 雪花算法 + 基因法 | 订单号末尾嵌入 user_id 取模值,按订单号查也能路由 |
| 非分片键查询 | 广播表 + ES 异步索引 | 低频查询走 ES,配置表走广播 |
3. ShardingSphere 配置与实现
3.1 Maven 依赖
<!-- Why: ShardingSphere 5.5.x 基于 Jakarta EE,需搭配 Spring Boot 3.4 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.5.1</version>
</dependency>
<!-- Why: Spring Boot 3.4 基于 Jakarta EE,不再用 javax 包 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
3.2 ShardingSphere 分片配置
# Why: ShardingSphere 5.5 Standalone 模式无需注册中心,repository 使用 H2 本地存储
mode:
type: Standalone
repository:
type: H2
dataSources:
ds0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://10.0.1.10:3306/order_db_0?useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
ds1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://10.0.1.11:3306/order_db_1?useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
ds2:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://10.0.1.12:3306/order_db_2?useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
ds3:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://10.0.1.13:3306/order_db_3?useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximumPoolSize: 20
minimumIdle: 5
connectionTimeout: 30000
rules:
- !SHARDING
tables:
# Why: t_order 按 user_id 分 4 库 × 16 表,共 64 张物理表
t_order:
actualDataNodes: ds${0..3}.t_order_${0..15}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_table_mod
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_db_mod
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake_order
# Why: t_order_item 遵循与 t_order 相同的分片策略,保证关联查询同库
t_order_item:
actualDataNodes: ds${0..3}.t_order_item_${0..15}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_table_mod
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_db_mod
keyGenerateStrategy:
column: item_id
keyGeneratorName: snowflake_item
# Why: 广播表不分片,每个库都有一份完整数据,JOIN 时本地关联
broadcastingTables:
- t_region
- t_shipping_template
# Why: 绑定表具有相同分片策略,JOIN 时保证路由到同一分片
bindingTables:
- t_order, t_order_item
shardingAlgorithms:
t_order_db_mod:
type: MOD
props:
sharding-count: 4
t_order_table_mod:
type: MOD
props:
sharding-count: 16
keyGenerators:
snowflake_order:
type: SNOWFLAKE
props:
worker-id: 1
snowflake_item:
type: SNOWFLAKE
props:
worker-id: 1
3.3 Spring Boot 数据源配置
// Why: ShardingSphere 5.5 的 DataSource 通过 SPI 自动装配,
// 无需手动创建 @Bean,只需排除原生 DataSourceAutoConfiguration
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.4 订单实体与 Repository
// Why: 使用 MyBatis-Plus 简化 CRUD,@TableName 映射逻辑表名
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.ASSIGN_ID)
private Long orderId;
/** 分片键,所有查询必须携带 */
private Long userId;
private String orderNo;
private Integer status;
private BigDecimal totalAmount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// Why: 订单明细与订单共用分片键,保证绑定表 JOIN 在同一库执行
@Data
@TableName("t_order_item")
public class OrderItem {
@TableId(type = IdType.ASSIGN_ID)
private Long itemId;
private Long orderId;
private Long userId;
private Long skuId;
private Integer quantity;
private BigDecimal unitPrice;
}
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* Why: 带分片键的精确查询,ShardingSphere 直接路由到单分片,
* 性能最优
*/
@Select("SELECT * FROM t_order WHERE user_id = #{userId} AND order_id = #{orderId}")
Order selectByUserAndOrder(@Param("userId") Long userId,
@Param("orderId") Long orderId);
/**
* Why: 不带 order_id 的用户维度查询,路由到单库多表,
* 需要 ShardingSphere 合并排序
*/
@Select("SELECT * FROM t_order WHERE user_id = #{userId} "
+ "AND status = #{status} ORDER BY create_time DESC "
+ "LIMIT #{offset}, #{limit}")
List<Order> selectByUserAndStatus(@Param("userId") Long userId,
@Param("status") Integer status,
@Param("offset") int offset,
@Param("limit") int limit);
/**
* Why: 按订单号查询 — 订单号基因法保证 order_no 包含分片信息,
* 提取分片路由值后精准路由
*/
@Select("SELECT * FROM t_order WHERE order_no = #{orderNo}")
Order selectByOrderNo(@Param("orderNo") String orderNo);
}
3.5 订单号生成器(基因法)
/**
* Why: 基因法将 userId % 4 的值嵌入订单号末尾,
* 使得按 order_no 查询时能反推出分片位置,实现精确路由
*/
@Component
public class OrderNoGenerator {
private static final int SHARD_COUNT = 4;
/**
* 生成订单号:时间戳(13位) + 机器标识(2位) + 序列号(3位) + 分片基因(1位)
* 总长度 19 位,兼容 bigint 无符号范围
*/
public String generate(Long userId) {
long timestamp = System.currentTimeMillis();
int workerId = WorkerIdHolder.getWorkerId();
int sequence = SequenceHolder.nextId();
// Why: 分片基因 = userId % 分片数,嵌入订单号末尾
int shardGene = (int) (userId % SHARD_COUNT);
return String.format("%d%02d%03d%d", timestamp, workerId, sequence, shardGene);
}
/**
* Why: 从订单号反推分片路由值,ShardingSphere 自定义分片算法中使用
*/
public static int extractShardGene(String orderNo) {
if (orderNo == null || orderNo.length() < 19) {
throw new IllegalArgumentException("Invalid order number: " + orderNo);
}
return Character.getNumericValue(orderNo.charAt(orderNo.length() - 1));
}
}
4. 多数据源切换
分库分表只解决了订单库的问题,但应用中还有商品库、用户库等不分片的数据源。需要实现动态数据源切换。
4.1 动态数据源核心实现
/**
* Why: 继承 AbstractRoutingDataSource 实现运行时数据源切换,
* 配合 @DS 注解声明式切换,避免硬编码
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
public static void setDataSource(String dsKey) {
CONTEXT_HOLDER.set(dsKey);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
/**
* Why: 自定义注解 @DS 声明数据源切换,AOP 切面拦截
* 默认数据源为 sharding(订单分片库),其他库通过注解显式指定
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String value() default "sharding";
}
/**
* Why: AOP 切面在方法执行前切换数据源,方法执行后清除,
* 防止线程复用时数据源串用
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
try {
DynamicDataSource.setDataSource(ds.value());
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
4.2 数据源配置类
/**
* Why: 将 ShardingSphere DataSource 和普通 DataSource 统一管理,
* 通过 DynamicDataSource 运行时路由
*/
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource dynamicDataSource(
@Qualifier("shardingDataSource") DataSource shardingDs,
@Qualifier("goodsDataSource") DataSource goodsDs,
@Qualifier("userDataSource") DataSource userDs) {
Map<Object, Object> targetDataSources = new HashMap<>(8);
targetDataSources.put("sharding", shardingDs);
targetDataSources.put("goods", goodsDs);
targetDataSources.put("user", userDs);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(shardingDs);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
/**
* Why: ShardingSphere DataSource 由 SPI 自动创建,
* 这里直接从 Spring 容器获取
*/
@Bean("shardingDataSource")
public DataSource shardingDataSource() throws SQLException {
// Why: ShardingSphere 5.5 使用 ShardingSphereDataSourceFactory 创建
return ShardingSphereDataSourceFactory.createDataSource(
createDataSourceMap(),
createRuleConfiguration(),
createProperties()
);
}
@Bean("goodsDataSource")
@ConfigurationProperties(prefix = "spring.datasource.goods")
public DataSource goodsDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean("userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user")
public DataSource userDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
4.3 Service 层多数据源使用示例
@Service
@RequiredArgsConstructor
public class OrderQueryService {
private final OrderMapper orderMapper;
private final GoodsClient goodsClient;
private final UserClient userClient;
/**
* Why: 默认数据源是 sharding,无需加 @DS 注解
*/
public OrderVO getOrderDetail(Long userId, Long orderId) {
Order order = orderMapper.selectByUserAndOrder(userId, orderId);
if (order == null) {
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
}
// Why: 商品和用户信息通过 Feign 调用,避免跨分片 JOIN
GoodsVO goods = goodsClient.getGoods(order.getGoodsId());
UserVO user = userClient.getUser(userId);
return OrderConverter.toVO(order, goods, user);
}
/**
* Why: 商品查询切换到 goods 数据源
*/
@DS("goods")
public List<GoodsVO> listGoodsByCategory(Long categoryId) {
return goodsMapper.selectByCategory(categoryId);
}
}
5. AtomCode 介入:三道防线保障 SQL 迁移
这是本文的核心部分。200+ 个 SQL 的迁移如果纯靠人工,不管多仔细都会漏。 AtomCode 通过 Rules、Skill、Agent 三层机制,从规范约束、脚本生成、批量验证三个维度保障迁移质量。
5.1 第一道防线:Rules 校验分片键使用规范
# .atomcode/rules/sharding/sharding-key-rule.md
---
trigger: always_on
alwaysApply: true
---
# ShardingSphere 分片键使用规范
> 适用于所有涉及 t_order、t_order_item 表的 SQL 编写
## 强制规则
### 1. 查询必须携带分片键
- t_order 和 t_order_item 的分片键为 user_id
- 所有 SELECT/UPDATE/DELETE 语句的 WHERE 条件必须包含 user_id
- 例外:按 order_no 查询时,order_no 已嵌入分片基因,无需额外指定 user_id
- 违反此规则的 SQL 将导致全分片扫描,严重拖垮性能
### 2. 禁止跨分片 JOIN
- 不允许 t_order JOIN t_user(跨库 JOIN)
- 不允许 t_order JOIN t_goods(跨库 JOIN)
- t_order JOIN t_order_item 允许(绑定表,同库执行)
- 跨域数据需通过应用层组装或 ES 宽表解决
### 3. 自增主键禁止使用
- 分片环境下 AUTO_INCREMENT 会导致主键冲突
- 必须使用 ShardingSphere SNOWFLAKE 或自定义 ID 生成器
- 订单号使用基因法生成,包含分片路由信息
### 4. LIMIT 分页必须携带分片键
- 不带分片键的 LIMIT 查询会在所有分片执行后归并排序
- 深分页(offset > 1000)必须使用游标分页(基于 create_time + order_id)
- 禁止 `SELECT * FROM t_order LIMIT 10000, 10` 这类无分片键深分页
### 5. 聚合函数需标注兼容性
- COUNT/SUM/MAX/MIN 在单分片查询中语义不变
- 全表聚合需改为按分片汇总后应用层合并
- GROUP BY 字段应包含分片键以保证结果正确
配置这个 Rule 后,AtomCode 在生成或审查任何涉及订单表的 SQL 时,都会自动校验以上 5 条规则。
5.2 第二道防线:Skill 生成迁移脚本
# .atomcode/skills/sharding-sql-migration/SKILL.md
---
name: sharding-sql-migration
description: 分库分表 SQL 迁移技能,负责扫描不兼容 SQL、生成改造脚本和路由验证用例
---
# ShardingSphere SQL 迁移技能
## 核心能力
### 1. SQL 兼容性扫描
扫描项目中的 MyBatis Mapper XML 和注解 SQL,识别以下不兼容模式:
```markdown
检查项:
- [ ] SELECT 语句 WHERE 条件是否包含分片键 user_id
- [ ] JOIN 语句是否涉及跨分片表关联
- [ ] 聚合函数是否标注了 ShardingSphere 兼容性
- [ ] INSERT 语句是否使用 SNOWFLAKE 主键
- [ ] 分页查询是否使用了深分页模式
- [ ] 子查询是否包含跨分片引用
- [ ] ORDER BY 字段是否与分片键冲突
```
### 2. 迁移脚本生成
对每个不兼容 SQL,生成改造方案:
```markdown
输出格式:
## SQL #N — {原始 SQL 摘要}
**风险等级**: 🔴高 / 🟠中 / 🟡低
**不兼容原因**: {具体描述}
**改造方案**:
- 原始: {原始 SQL}
- 改造后: {兼容 SQL}
- 说明: {改造逻辑说明}
**路由分析**:
- 分片键: user_id
- 路由类型: 精确路由 / 范围路由 / 全路由
- 预估影响分片数: {N}
```
### 3. 分片路由验证用例
为每个改造后的 SQL 生成路由验证代码:
```java
// Why: 验证 SQL 能被 ShardingSphere 正确路由到目标分片
@Test
void testRouteForSelectByUserId() {
String sql = "SELECT * FROM t_order WHERE user_id = ? AND status = ?";
ShardingRouteResult result = shardingRule.route(sql,
Collections.singletonList(1001L), Collections.singletonList(1));
// Why: user_id=1001, 1001%4=1, 应路由到 ds1
assertThat(result.getTargetDataSources()).containsExactly("ds1");
assertThat(result.getTargetTables()).anyMatch(t -> t.contains("t_order_"));
}
```
5.3 第三道防线:Agent 批量验证 SQL 兼容性
# .atomcode/agents/sharding-sql-migration.yaml
name: ShardingSphere SQL 迁移验证 Agent
description: 编排 SQL 扫描、改造、验证全流程,保障 200+ 个 SQL 迁移零错误
steps:
- name: 扫描不兼容 SQL
skill: sharding-sql-migration
params:
action: scan
scan_paths:
- src/main/resources/mapper/
- src/main/java/com/example/order/mapper/
sharding_tables:
- t_order
- t_order_item
sharding_key: user_id
- name: 生成迁移报告
skill: sharding-sql-migration
params:
action: report
report_format: markdown
group_by: risk_level
- name: 生成改造代码
skill: sharding-sql-migration
params:
action: migrate
migration_strategy: conservative
# Why: 保守策略只改写明确不兼容的 SQL,不做额外优化
backup_original: true
- name: 代码安全审查
skill: code-verifier
params:
check_type: security
rules:
- 检查 SQL 中是否有硬编码数据源地址
- 检查分片键是否使用参数绑定(防止 SQL 注入)
- 检查密码配置是否脱敏
- 检查测试代码中是否包含生产环境连接信息
- name: 分片路由验证
skill: sharding-sql-migration
params:
action: verify_route
test_data_count: 100
# Why: 用 100 条模拟数据验证路由正确性,覆盖边界值
verify_items:
- 精确路由命中率
- 全路由告警检测
- 跨分片 JOIN 拦截
- 深分页性能基线
- name: 生成验证报告
skill: quality-inspector
params:
check_items:
- SQL 兼容性覆盖率 100%
- 分片路由正确率 100%
- 无高风险不兼容项
- 性能基线达标
5.4 AtomCode 执行过程实录
我实际执行时的步骤和产出:
Step 1:在 AtomCode 中触发 sharding-sql-migration Agent,扫描整个 Mapper 目录。
Step 2:Agent 输出迁移报告,按风险等级分组:
| 风险等级 | 数量 | 典型问题 |
|---|---|---|
| 🔴 高风险 | 23 | 无分片键查询 15 个、跨分片 JOIN 8 个 |
| 🟠 中风险 | 47 | 深分页 22 个、聚合函数不规范 25 个 |
| 🟡 低风险 | 130 | SQL 写法风格统一(命名、格式) |

Step 3:Agent 自动生成改造代码,我逐条 review 确认。
Step 4:Agent 执行路由验证,100 条模拟数据跑完全部 200+ 个 SQL,发现 2 个路由异常(深分页偏移计算错误),自动修复后重新验证通过。
6. 生产级踩坑记录
踩坑 1:雪花算法时钟回拨导致主键重复
现象:大促期间 NTP 同步导致服务器时钟回拨 3 秒,ShardingSphere SNOWFLAKE 生成了 3 个重复 order_id,主键冲突报错。
原因:默认 SNOWFLAKE 算法依赖单调递增的时间戳,时钟回拨时生成的 ID 可能重复。
解决:
# Why: 开启容忍时钟回拨,最大容忍 10 秒
keyGenerators:
snowflake_order:
type: SNOWFLAKE
props:
worker-id: 1
max-tolerate-difference-seconds: 10
/**
* Why: 自定义 KeyGenerator 增加时钟回拨保护,
* 回拨时阻塞等待而非直接生成重复 ID
*/
@Component
public class SafeSnowflakeKeyGenerator implements ShardingKeyGenerator {
private final SnowflakeIdGenerator delegate = new SnowflakeIdGenerator();
@Override
public Comparable<?> generateKey() {
// Why: 检测到时钟回拨时,sleep 等待追平
delegate.waitIfClockWentBack();
return delegate.nextId();
}
@Override
public String getType() {
return "SAFE_SNOWFLAKE";
}
}
踩坑 2:多数据源 + ShardingSphere 事务失效
现象:@DS("goods") + @Transactional 组合使用时,事务不生效,数据写入后查询不到。
原因:@DS 的 AOP 切面优先级低于 @Transactional,导致事务开启时数据源已切换,但事务提交时数据源被清除,连接不是同一个。
解决:
/**
* Why: @DS 切面必须比 @Transactional 优先级更高,
* 确保先切换数据源再开启事务
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // Why: 最高优先级,在事务切面之前执行
public class DataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
try {
DynamicDataSource.setDataSource(ds.value());
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
同时确保
@Transactional只在同一个数据源内使用,跨数据源通过分布式事务(Seata AT 模式)解决。
踩坑 3:绑定表 JOIN 路由错误
现象:t_order JOIN t_order_item ON t_order.order_id = t_order_item.order_id WHERE t_order.user_id = ?,发现查询跑到所有分片执行,没有精准路由。
原因:ShardingSphere 的绑定表需要显式声明,否则无法识别关联关系。
解决:
# Why: 声明绑定表关系,ShardingSphere 会将绑定表路由到同一分片
rules:
- !SHARDING
bindingTables:
- t_order, t_order_item
踩坑 4:深分页 OOM
现象:SELECT * FROM t_order WHERE user_id = ? ORDER BY create_time DESC LIMIT 50000, 10,应用服务器频繁 Full GC,最终 OOM。
原因:ShardingSphere 处理深分页时,需要将所有分片的 offset + limit 条数据加载到内存归并排序,offset 越大内存占用越高。
解决:改用游标分页:
/**
* Why: 游标分页基于索引列的有序性,每次只查询上次最后一条之后的数据,
* 避免 ShardingSphere 加载大量数据到内存归并
*/
@Select("SELECT * FROM t_order WHERE user_id = #{userId} "
+ "AND create_time < #{lastCreateTime} "
+ "ORDER BY create_time DESC LIMIT #{limit}")
List<Order> selectByCursor(@Param("userId") Long userId,
@Param("lastCreateTime") LocalDateTime lastCreateTime,
@Param("limit") int limit);
踩坑 5:广播表数据不一致
现象:t_region(区域表,广播表)在 ds0 有 320 条记录,ds1 只有 318 条,JOIN 时部分区域数据缺失。
原因:初始化广播表数据时,只往 ds0 写入了数据,未同步到其他分片。ShardingSphere 的广播表只在写入时自动同步,初始化需要手动处理。
解决:
-- Why: 初始化时需向所有分片写入广播表数据
-- AtomCode Skill 生成的同步脚本
INSERT INTO ds0.t_region
SELECT *
FROM temp_region;
INSERT INTO ds1.t_region
SELECT *
FROM temp_region;
INSERT INTO ds2.t_region
SELECT *
FROM temp_region;
INSERT INTO ds3.t_region
SELECT *
FROM temp_region;
在 AtomCode 的
sharding-sql-migrationSkill 中增加了广播表一致性检查步骤:
## 广播表一致性检查
- [ ] 所有分片的广播表记录数是否一致
- [ ] 广播表 DDL 是否在所有分片执行
- [ ] 新增广播表时是否配置了 broadcastingTables
- [ ] 数据初始化脚本是否覆盖所有分片
7. 效果复盘
7.1 SQL 迁移效率对比
| 指标 | AtomCode 介入前 | AtomCode 介入后 | 提升幅度 |
|---|---|---|---|
| SQL 兼容性扫描时间 | 2 天(人工) | 15 分钟(Agent) | ⬇️ 97% |
| 迁移脚本编写时间 | 3 天 | 4 小时(Skill 生成 + 人工 Review) | ⬇️ 83% |
| SQL 兼容性问题遗漏 | 3 个(上线后才发现) | 0 个 | ⬇️ 100% |
| 路由验证覆盖率 | 60%(抽测) | 100%(全量) | ⬆️ 67% |
| 整体迁移周期 | 5 天 | 2 天 | ⬇️ 60% |
| SQL 迁移错误率 | 1.5% | 0% | ⬇️ 100% |

7.2 分库分表性能收益
| 指标 | 单库 | 分库分表后 | 提升幅度 |
|---|---|---|---|
| 订单查询 RT(P99) | 2300ms | 85ms | ⬇️ 96% |
| 订单写入 QPS 峰值 | 3500 | 15000+ | ⬆️ 328% |
| 慢查询数量(> 1s) | 47 次/天 | 2 次/天 | ⬇️ 96% |
| 单表最大行数 | 2000 万 | 312 万 | ⬇️ 84% |
| 主从延迟 | 10s+ | < 1s | ⬇️ 90% |
| DDL 执行时间 | 45 分钟 | 8 分钟 | ⬇️ 82% |

7.3 AtomCode 三层防线价值总结
Rules 层(被动约束):编码时实时校验,把不兼容 SQL 扼杀在编写阶段。分片键规范、跨分片 JOIN 禁令这 5 条规则,在后续迭代中持续生效,新写的 SQL 不会踩坑。
Skill 层(主动生成):扫描 → 报告 → 改造 → 验证,4 步全自动。200+ 个 SQL 的迁移脚本 4 小时生成完毕,人工 review 确认即可。
Agent 层(自动编排):多 Skill 编排 + 安全校验 + 路由验证,三重保障确保零错误上线。实际执行中发现 2 个深分页路由异常,Agent 自动修复后验证通过。
8. 迁移流程完整时序
最后,用一张时序图把整个迁移流程串起来:
📜 真实性声明
本文所有内容均基于作者在 2026 年 Q1 期间参与的中型电商平台订单系统分库分表项目中的真实经验。所有案例、数据、代码均来自生产环境,经过实践验证。为保护商业机密,部分敏感信息已做脱敏处理,但技术细节保持完整和真实。
如有任何疑问,欢迎在评论区交流讨论。
更多推荐


所有评论(0)