PolarDB + Spring Boot 实战:自建MySQL到云原生数据库的零停机迁移全攻略
故事要从去年双11那次让人背脊发凉的事故说起。
预热期间,我们负责的电商订单系统突然出现异常。自建MySQL主从集群在流量高峰下,主从延迟直接飙升到了30秒以上。用户刚完成下单,回头查询订单状态,系统却显示“订单不存在”——这种致命问题对核心电商系统而言,简直是灾难性的。更严重的是,慢查询数量在短短1小时内,从日均20条暴涨到800条,订单超时率也从0.5%狂飙至8%。运维团队忙得焦头烂额,加从库、调参数,但效果微乎其微。事故复盘会后,CTO下达了硬性指令:大促前必须完成数据库的云原生化改造,并且要求零停机。
经过两周的方案论证和三周的落地实施,我们成功实现了从自建MySQL到PolarDB的迁移。最终效果令人惊喜:主从延迟归零、慢查询锐减70%、订单超时率降至0.3%。今天这篇复盘,希望能给正在纠结“要不要迁”和“怎么迁”的朋友们提供一些实实在在的参考。
一、为什么非要迁移?自建MySQL的5大核心痛点
先说痛点,这并不是为了贬低MySQL,而是当一套架构老化到一定程度,运维成本高到让人窒息,改变就成为必然。
痛点1:主从延迟——读扩展的天花板
自建MySQL基于Binlog的逻辑复制机制,一旦出现大事务或高并发写入,从库追不上主库是常态。我们线上日常延迟就在1-3秒徘徊,大促期间更是直奔30秒以上。这意味着所有“写后读”操作都面临巨大风险:用户体验极差,投诉量直线上升。
痛点2:运维成本——DBA成了救火队员
自建MySQL的运维是一场无休止的持久战:主从切换需要手动操作或依赖MHA这类半成熟方案,参数调优必须时刻紧盯监控,版本升级需要停机维护,备份策略还得自行设计并反复验证。团队里2个DBA,几乎70%的精力都消耗在日常运维上,根本没时间琢磨架构优化。
痛点3:扩容慢——纵向要停机,横向要复制
自建MySQL想要纵向扩容?必须停机更换规格。想要横向扩容?需要添加从库并执行全量数据复制。以我们500GB的数据库为例,新增一个从库需要4-6小时的数据同步时间。在流量突然暴增的场景下,这种速度远远跟不上业务需求。
痛点4:备份风险——恢复时间完全不可控
虽然我们也配置了Xtrabackup的全量+增量备份,但在真实恢复演练中,500GB数据的完整恢复需要2-3小时。对于核心业务系统来说,这个RTO(恢复时间目标)是无法接受的。
痛点5:高可用脆弱——主从切换并非万无一失
MHA主从切换理想状态下30秒内完成,但我们经历过太多次失败:SSH连接超时、Binlog不完整、从库SQL线程报错……每次切换失败,都是妥妥的P0级故障,让人心跳漏拍。
下面这张对比图,可以更直观地看出两套架构的差异:
| 对比维度 | 自建MySQL | PolarDB | 差异化优势 |
|---|---|---|---|
| 主从延迟 | 1-30秒(Binlog逻辑复制) | 毫秒级(物理日志复制) | 物理复制比逻辑复制快10-100倍 |
| 扩容方式 | 添加从库需4-6小时数据复制 | 只读节点5分钟内就绪 | 存储共享,无需数据复制 |
| 备份恢复 | 全量恢复2-3小时 | 秒级快照+任意时间点恢复 | 快照基于存储层,速度提升100倍 |
| 主从切换 | MHA 30秒-5分钟(可能失败) | 30秒自动切换(内置HA) | 内置Raft协议,切换可靠性99.99% |
| 运维成本 | 需要专职DBA | 全托管,零运维 | 释放DBA精力用于架构优化 |
| 存储成本 | 需要预购磁盘空间 | 按需弹性扩展 | 无需预估容量,按需付费 |
| 读扩展性 | 从库数量受限于复制延迟 | 最多15个只读节点 | 共享存储,读扩展无天花板 |
二、PolarDB架构解析:它凭什么能做到毫秒级延迟?
要理解PolarDB的优势,必须先弄懂它的核心——存储计算分离架构,这与自建MySQL“单机存储+逻辑复制”的思路有本质区别。
3.1 存储计算分离架构
3.2 三个核心机制,让它脱胎换骨
一写多读:主节点负责所有写操作,数据通过Redo物理日志同步到只读节点。与自建MySQL的Binlog逻辑复制不同,物理日志只记录数据页的修改,体积小、解析快,因此延迟可控制在毫秒级。
共享存储:所有计算节点共享同一份存储数据。只读节点无需维护独立的数据副本,直接从共享存储读取数据页。添加只读节点时,不需要数据复制,5分钟内即可就绪。
透明兼容MySQL:PolarDB的SQL语法、协议、驱动100%兼容MySQL 5.6/5.7/8.0。对于Spring Boot应用,只需要修改连接串,代码几乎无需改动。
三、迁移方案设计:三种方案的利弊分析
4.1 三种迁移方案对比
| 对比项 | 方案一:DTS全量+增量 | 方案二:DTS结构迁移+数据集成 | 方案三:备份恢复 |
|---|---|---|---|
| 停机时间 | 零停机 | 分钟级 | 小时级 |
| 迁移速度 | 中等(受DTS限速影响) | 快(数据集成批量导入) | 快(物理备份恢复) |
| 数据一致性 | 增量同步保证 | 需要额外校验 | 备份点一致性 |
| 操作复杂度 | 中等 | 较高 | 低 |
| 回滚难度 | 容易(双向同步) | 中等 | 困难 |
| 适用场景 | 核心业务,要求零停机 | 超大数据量迁移 | 非核心业务,可接受停机 |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
4.2 零停机迁移架构
四、零停机迁移实战:5步走完全流程
步骤1:PolarDB集群创建
这一步很关键:集群规格和参数配置直接影响迁移后的性能。如果选错了,后面需要花更多精力补救。
规格选择
| 参数 | 自建MySQL | PolarDB选择 | 选择依据 |
|---|---|---|---|
| 主节点 | 8C32G | 8C32G(独享规格) | 计算能力对等,独享避免资源争抢 |
| 只读节点 | 2×4C16G | 2×8C32G | 只读节点规格不低于主节点,避免读性能瓶颈 |
| 存储 | 500GB SSD | 弹性存储(无需预购) | 按需付费,无需预估容量 |
| 版本 | MySQL 5.7 | MySQL 5.7兼容 | 100%协议兼容,驱动无需修改 |
兼容性评估
-- 检查源库使用的特性是否被PolarDB兼容
-- PolarDB兼容MySQL 5.7绝大部分特性,但以下需要特别关注
-- 1. 检查存储引擎
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'order_db' AND ENGINE NOT IN ('InnoDB', 'MEMORY');
-- 2. 检查自增列使用方式
SELECT TABLE_NAME, AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'order_db' AND AUTO_INCREMENT IS NOT NULL;
-- 3. 检查外键约束
SELECT TABLE_NAME, CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'order_db' AND REFERENCED_TABLE_NAME IS NOT NULL;
关键参数模板
# 连接数设置(根据业务峰值连接数×1.5倍冗余)
loose_max_connections = 3000
# InnoDB缓冲池(独享规格下设置为内存的70%)
loose_innodb_buffer_pool_size = 22G
# 事务隔离级别(与源端保持一致)
loose_transaction_isolation = READ-COMMITTED
# 并行查询(PolarDB特有,加速大查询)
loose_innodb_polar_parallel_query_threshold = 10000
# 优化器开关(保持与MySQL 5.7一致的行为)
loose_optimizer_switch = 'index_condition_pushdown=on,mrr=on,mrr_cost_based=on'
步骤2:DTS数据迁移
为什么选择DTS?它是阿里云官方的数据迁移服务,支持全量+增量的无缝衔接,增量同步延迟在秒级以内,是零停机迁移的核心保障。
DTS全量+增量迁移时序
全量+增量同步流程
# 全量迁移阶段需要控制速率,避免对源库造成性能影响
# 1. 创建迁移任务(阿里云CLI)
aliyun dts CreateMigrationJob \
--SourceEndpoint.InstanceType MySQL \
--SourceEndpoint.IP 10.0.1.100 \
--SourceEndpoint.Port 3306 \
--SourceEndpoint.UserName dts_user \
--DestinationEndpoint.InstanceType PolarDB \
--DestinationEndpoint.InstanceID pc-xxxxxxxxx \
--MigrationObject '[{"DBName":"order_db"}]' \
--MigrationMode.StructureIntance true \
--MigrationMode.DataIntance true
# 2. 监控迁移进度
aliyun dts DescribeMigrationJobStatus --MigrationJobCode dts-xxxxxxxxx
数据校验
-- 迁移后必须做数据一致性校验,不能只看DTS的校验结果
-- DTS的校验只覆盖表级行数对比,我们需要更细粒度的校验
-- 1. 行数对比(快速验证)
SELECT 'order_db.orders' AS table_name, COUNT(*) AS row_count FROM order_db.orders;
-- 2. 数据抽样对比(深度验证)
-- 在源库和目标库分别执行,对比结果
SELECT MD5(GROUP_CONCAT(id, user_id, order_status, total_amount, create_time ORDER BY id)) AS data_checksum
FROM order_db.orders
WHERE create_time >= '2025-11-01' AND create_time < '2025-11-02';
性能影响评估
| 监控指标 | 迁移前 | 全量迁移期间 | 增量同步期间 |
|---|---|---|---|
| 源库CPU | 45% | 52%(+7%) | 46%(+1%) |
| 源库IOPS | 3000 | 3500(+17%) | 3100(+3%) |
| 源库QPS | 8000 | 7600(-5%) | 7900(-1%) |
| 源库RT | 2ms | 3ms(+50%) | 2ms |
全量迁移对源库有一定影响,但通过DTS的限速控制,QPS下降在5%以内,业务可以正常运转。增量同步阶段影响几乎可以忽略。
步骤3:Spring Boot多数据源配置
为什么要配置多数据源?零停机迁移的核心是“双写+双读+灰度切换”,应用需要同时连接源库和目标库,并通过配置中心来控制流量比例。
# 多数据源配置是灰度切换的基础
# application-polar-migration.yml
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://10.0.1.100:3306/order_db?useSSL=false&characterEncoding=utf8mb4
username: ${MYSQL_PRIMARY_USER}
password: ${MYSQL_PRIMARY_PASS}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30
minimum-idle: 10
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
polar:
jdbc-url: jdbc:mysql://pc-xxxxxxxxx.polardb.rds.aliyuncs.com:3306/order_db?useSSL=false&characterEncoding=utf8mb4
username: ${POLAR_USER}
password: ${POLAR_PASS}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30
minimum-idle: 10
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
# 灰度配置(通过Nacos动态下发)
migration:
read-polar-ratio: 0
dual-write-enabled: false
polar-write-fail-fast: false
// 动态数据源路由是灰度切换的核心实现
// 基于ThreadLocal + AOP实现读写分离和灰度路由
public class MigrationDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineTargetDataSource() {
MigrationContext ctx = MigrationContextHolder.get();
// 写操作:根据双写配置决定路由
if (ctx.isWriteOperation()) {
if (MigrationConfig.isDualWriteEnabled()) {
return primaryDataSource; // 由切面异步写PolarDB
}
return primaryDataSource;
}
// 读操作:根据灰度比例路由
int ratio = MigrationConfig.getReadPolarRatio();
if (ratio > 0 && ThreadLocalRandom.current().nextInt(100) < ratio) {
return polarDataSource;
}
return primaryDataSource;
}
}
步骤4:流量灰度切换
为什么一定要灰度?直接全量切换风险太大。灰度切换能逐步暴露问题,随时可以回滚,将影响范围控制在最小。
灰度切换操作步骤
# 阶段一:10%读流量切PolarDB
# 1. 开启双写
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.dual-write-enabled=true"
# 2. 观察5分钟,确认PolarDB写入无异常
# 3. 切换10%读流量
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.read-polar-ratio=10"
# 4. 监控核心指标2小时(RT变化<10%、错误率变化<0.1%、数据一致性校验通过)
# 阶段二:50%读流量
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.read-polar-ratio=50"
# 阶段三:100%读流量
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.read-polar-ratio=100"
回滚方案
# 一旦发现异常,立即执行回滚
# 紧急回滚:所有读流量切回MySQL
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.read-polar-ratio=0"
# 关闭双写
curl -X POST "https://nacos:8848/nacos/v1/cs/configs" -d "dataId=migration-config&group=DEFAULT_GROUP&content=migration.dual-write-enabled=false"
# 注意:回滚后需要用DTS反向同步PolarDB期间写入的数据到MySQL
▲ 灰度切换期间核心监控看板:实时追踪RT对比、流量比例、数据一致性状态与回滚控制台
步骤5:原集群下线
不能立刻下线。需要一段观察期确保PolarDB稳定运行,并做好数据最终一致性校验,确认没有遗漏后,才能安全下线。
数据一致性终验
-- 终验需要全量对比,不能只抽样
-- 使用DTS的数据校验功能 + 自研脚本的行级校验
-- 1. DTS数据校验(在DTS控制台创建数据校验任务)
-- 2. 业务数据对账
SELECT DATE(create_time) AS dt,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS total_amount
FROM order_db.orders
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(create_time)
ORDER BY dt;
-- 3. 确认DTS增量同步延迟为0,持续观察30分钟
下线清单
| 操作项 | 执行时间 | 负责人 | 验证方式 |
|---|---|---|---|
| 停止DTS同步任务 | 切换100%后72小时 | DBA | DTS控制台确认任务已停止 |
| 关闭MySQL监控告警 | 停止DTS后 | 运维 | 确认告警不再触发 |
| MySQL数据最终备份 | 关闭监控后 | DBA | 验证备份文件完整性 |
| 释放MySQL实例 | 保留7天后 | DBA | 确认PolarDB稳定运行 |
| 清理应用MySQL数据源配置 | 释放实例后 | 开发 | 确认应用配置文件已更新 |
五、Spring Boot适配:从MySQL到PolarDB的4个关键调整
1. 连接池配置优化
为什么要调整?PolarDB的网络模型与MySQL略有差异,HikariCP的默认参数需要针对性优化,特别是连接超时和空闲连接回收。
# PolarDB的网络延迟比自建MySQL略高(跨可用区),需要调整超时参数
spring:
datasource:
hikari:
connection-timeout: 5000 # 适当增加
idle-timeout: 900000 # 适当延长
max-lifetime: 1200000 # 主从切换时旧连接需及时回收
connection-test-query: SELECT 1
leak-detection-threshold: 60000 # 迁移期间开启
maximum-pool-size: 40
minimum-idle: 15
2. 事务隔离级别适配
为什么会关注这一点?PolarDB默认的RR隔离级别与MySQL 5.7行为一致,但我们的业务使用的是RC隔离级别,需要确认PolarDB在RC下的行为也完全一致。
@Transactional(isolation = Isolation.READ_COMMITTED)
public class OrderService {
// RC隔离级别下,PolarDB不会加gap lock
// 这和MySQL 5.7行为一致,不需要修改代码
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order createOrder(CreateOrderRequest request) { ... }
// 对于需要Serializable隔离级别的场景
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deductStock(Long skuId, Integer quantity) { ... }
}
3. 批量操作优化:利用PolarDB并行查询
为什么要优化批量操作?PolarDB的并行查询对大表的全表扫描和批量操作有显著的加速效果,Spring Boot应用需要显式开启才能利用这一特性。
// PolarDB并行查询对大批量INSERT和SELECT有2-5倍加速
@Repository
public class OrderBatchRepository {
private final JdbcTemplate jdbcTemplate;
// 批量插入优化:单次INSERT行数建议500-1000
public void batchInsert(List orders) {
int batchSize = 500;
jdbcTemplate.batchUpdate("INSERT INTO orders (id, user_id, order_status, total_amount, create_time) VALUES (?, ?, ?, ?, ?)",
orders, batchSize, (ps, order) -> { ... });
}
// 并行查询:利用PolarDB的并行查询加速大表扫描
public List queryOrdersByTimeRange(LocalDateTime start, LocalDateTime end) {
String sql = "/* PARALLEL(4) */ SELECT * FROM orders WHERE create_time BETWEEN ? AND ? ORDER BY create_time";
return jdbcTemplate.query(sql, (rs, rowNum) -> mapToOrder(rs), start, end);
}
}
4. 读写分离配置
为什么要使用集群Endpoint?PolarDB的集群Endpoint能自动实现读写分离——写请求路由到主节点,读请求自动分发到只读节点。Spring Boot只需要配置一个连接串,自己甚至不用写读写分离逻辑。
# 集群Endpoint是PolarDB读写分离的最佳实践
spring:
datasource:
write:
jdbc-url: jdbc:mysql://pc-xxxxxxxxx-master.polardb.rds.aliyuncs.com:3306/order_db
username: ${POLAR_WRITE_USER}
password: ${POLAR_WRITE_PASS}
read:
jdbc-url: jdbc:mysql://pc-xxxxxxxxx.polardb.rds.aliyuncs.com:3306/order_db
username: ${POLAR_READ_USER}
password: ${POLAR_READ_PASS}
hikari:
maximum-pool-size: 60
minimum-idle: 20
connection-init-sql: SET NAMES utf8mb4
// 通过自定义注解实现声明式读写分离
@Target({ ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly { }
@Aspect
@Component
public class ReadOnlyDataSourceAspect {
@Around("@annotation(readOnly)")
public Object routeReadDataSource(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
try {
MigrationContextHolder.setReadOperation(true);
return joinPoint.proceed();
} finally {
MigrationContextHolder.clear();
}
}
}
六、性能对比:6维度数据说话
迁移完成后,我们进行了为期2周的全链路压测和线上观察。下面是用数据说话的部分。
核心性能指标对比
| 指标 | 自建MySQL | PolarDB | 提升幅度 | 说明 |
|---|---|---|---|---|
| QPS(读写混合) | 8,000 | 12,500 | ⬆️ 56% | 并行查询 + 更优的执行计划 |
| 平均RT | 2.5ms | 1.2ms | ⬇️ 52% | InnoDB优化 + Buffer Pool命中率高 |
| 主从延迟 | 1-30秒 | <5毫秒 | ⬇️ 99.98% | 物理日志复制 vs 逻辑复制 |
| 慢查询数量(日) | 120条 | 36条 | ⬇️ 70% | 并行查询 + 索引优化建议 |
| 全量备份时间 | 2.5小时 | 30秒 | ⬇️ 99% | 快照备份 vs 物理备份 |
| 只读节点扩容耗时 | 4-6小时 | 5分钟 | ⬇️ 98% | 共享存储,无需数据复制 |
不同并发下的QPS对比
| 并发数 | MySQL QPS | MySQL RT | PolarDB QPS | PolarDB RT | QPS提升 |
|---|---|---|---|---|---|
| 50 | 5,200 | 1.8ms | 7,800 | 1.0ms | 50% |
| 100 | 8,000 | 2.5ms | 12,500 | 1.2ms | 56% |
| 200 | 9,500 | 4.2ms | 15,000 | 2.1ms | 58% |
| 500 | 8,200 | 12ms | 14,800 | 5.3ms | 80% |
| 1000 | 5,500 | 36ms | 13,200 | 11ms | 140% |
高并发场景下PolarDB的优势更加明显,这得益于存储计算分离架构——计算节点可以独立扩容,不受存储IO争抢影响。
▲ 不同并发场景下 PolarDB 与自建MySQL的 QPS 柱状对比(柱状图)与 RT 折线对比(线条)
读写分离效果
| 指标 | 单主节点 | 1主+2只读 | 1主+4只读 | 扩展比 |
|---|---|---|---|---|
| 读QPS | 10,000 | 28,000 | 52,000 | 5.2x |
| 写QPS | 2,500 | 2,500 | 2,500 | 1x |
| 读RT | 1.2ms | 0.8ms | 0.5ms | ⬇️ 58% |
PolarDB的读扩展几乎是线性的,因为只读节点共享存储,不存在传统MySQL的复制延迟问题。
七、踩坑实录:5个真实问题复盘
这些坑,每一个都曾让人心跳加速,堪称“宝贵经验”。
坑1:DTS增量同步延迟突增
现象:灰度切换10%流量到PolarDB后,DTS增量同步延迟从正常的1秒突然飙升到60秒,导致读PolarDB的用户看到的是1分钟前的数据,订单状态查询出现大量不一致。
根因:灰度切换后,应用开启双写,但批量操作使用了INSERT ... ON DUPLICATE KEY UPDATE语句。这类语句产生的Binlog事件量是普通INSERT的3-5倍,DTS的增量解析线程处理不过来,导致积压。
解决:将批量操作拆分为先SELECT判断存在性,再分别执行INSERT或UPDATE。同时调整DTS的增量同步并发度从2提升到4,延迟恢复到1秒以内。
经验:DTS增量同步的性能瓶颈往往出在大事务和批量操作上。迁移期间应避免大批量DML操作,如果不可避免,需要提前评估DTS的同步能力,必要时临时提升DTS规格。
坑2:PolarDB只读节点读一致性异常
现象:用户创建订单后立即查询订单列表,偶尔出现“订单不存在”的情况。排查发现请求被路由到只读节点,而只读节点的数据还没更新过来。
根因:PolarDB的物理日志复制虽然延迟在毫秒级,但并非零延迟。应用层在写操作完成后立即发起读请求,如果被路由到只读节点,就可能读到旧数据。这和自建MySQL的“写后读”问题是同一类,但PolarDB的延迟更小,反而容易让人忽略。
解决:对于“写后读”场景,使用集群Endpoint并开启session_consistency参数,确保同一会话的读请求路由到主节点。同时,在Spring Boot层面对关键查询添加@Transactional注解,保证事务内的读写走同一连接。
经验:PolarDB的读一致性策略需要根据业务场景选择。对于“写后立即读”的场景,必须使用主节点或开启会话一致性,不能盲目依赖只读节点。集群Endpoint的session_consistency参数是最简单的解决方案。
坑3:Spring Boot批量插入性能劣化
现象:迁移到PolarDB后,订单批量导入功能耗时从原来的30秒增加到120秒,性能劣化4倍。DBA排查发现PolarDB的CPU使用率飙升到90%。
根因:原来的批量导入使用了MyBatis的标签拼接多行INSERT,单次拼接5000行。PolarDB的SQL解析器对超长SQL的解析效率不如自建MySQL,5000行的INSERT语句解析时间占了总耗时的60%。
解决:将单次批量插入的行数从5000降低到500,同时开启JDBC的rewriteBatchedStatements参数,让驱动层做批量优化。修改后批量导入耗时降到25秒,比自建MySQL还快。
坑4:存储过程兼容性问题
现象:迁移后,结算模块的存储过程执行报错ERROR 1305 (42000): FUNCTION order_db.generate_settlement_id does not exist。该存储过程在自建MySQL上运行正常。
根因:PolarDB对存储过程内的动态SQL(PREPARE/EXECUTE)有更严格的安全限制。该存储过程使用了PREPARE stmt FROM CONCAT(...)拼接SQL,PolarDB默认禁止存储过程内使用动态SQL,需要通过参数loose_sp_dynamic_sql显式开启。
解决:在PolarDB参数配置中开启loose_sp_dynamic_sql = ON,同时审查所有存储过程,将动态SQL改为静态SQL。对于无法避免动态SQL的场景,通过参数开启。
坑5:大表DDL导致连接超时
现象:迁移后在PolarDB上对订单表执行ALTER TABLE ADD COLUMN操作,操作执行了20分钟后,Spring Boot应用开始报Communications link failure,大量连接断开。
根因:PolarDB的DDL操作使用的是Online DDL,虽然不阻塞DML,但在DDL执行期间会持有元数据锁(MDL)。当DDL执行时间过长,HikariCP的连接验证查询SELECT 1在等待MDL时超时,导致连接池判定连接失效并大量重建连接。
解决:将DDL操作安排在低峰期执行,同时临时调大HikariCP的connection-timeout到30秒。长期方案是使用PolarDB的DDL无锁变更功能(基于DMS的无锁变更),对大表DDL可以做到完全不阻塞。
八、最佳实践:迁移决策树 + 检查清单 + 参数调优
1. 迁移决策树
2. 迁移前检查清单
| 检查项 | 检查内容 | 通过标准 | 负责人 |
|---|---|---|---|
| 存储引擎 | 是否有MyISAM/Archive等非InnoDB表 | 全部为InnoDB | DBA |
| 字符集 | 是否使用utf8mb4 | 全库统一utf8mb4 | DBA |
| 外键约束 | 外键约束是否影响迁移顺序 | 按依赖关系排序列出 | DBA |
| 存储过程 | 动态SQL兼容性 | 确认参数调整方案 | DBA |
| 触发器 | 触发器兼容性 | 逐个验证 | 开发 |
| 自增列 | 自增列步长和偏移 | 与PolarDB对齐 | DBA |
| 时区设置 | 源库和目标库时区一致 | UTC或Asia/Shanghai统一 | DBA |
| SQL_mode | SQL_mode是否一致 | 确认PolarDB参数配置 | DBA |
| 大表评估 | 单表超过1000万行的表 | 评估DTS迁移时间 | DBA |
| 应用兼容性 | 驱动版本兼容性 | MySQL Connector/J 5.1.47+ | 开发 |
| 连接池配置 | HikariCP参数适配 | 超时参数调整 | 开发 |
| 监控告警 | 新增PolarDB监控指标 | 告警规则配置 | 运维 |
3. PolarDB参数调优清单
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| loose_max_connections | 2000 | 3000-5000 | 根据业务峰值连接数×1.5 |
| loose_innodb_buffer_pool_size | 实例内存×50% | 实例内存×70% | 独享规格可加大 |
| loose_innodb_polar_parallel_query_threshold | 0 | 10000 | 开启并行查询,阈值10000行 |
| loose_innodb_lock_wait_timeout | 50 | 10 | 减少锁等待时间,快速失败 |
| loose_sp_dynamic_sql | OFF | ON | 存储过程动态SQL支持 |
| loose_innodb_polar_pack_prefix | OFF | ON | 字符串列压缩,节省存储 |
| loose_block_hash_index | OFF | ON | 自适应哈希索引优化 |
| loose_innodb_flush_log_at_trx_commit | 1 | 1 | 保持双1,确保数据安全 |
| loose_sync_binlog | 1 | 1 | 保持双1,确保数据安全 |
| loose_innodb_io_capacity | 2000 | 10000 | 提升后台刷脏页速度 |
九、总结
从自建MySQL迁移到PolarDB,表面上是换了数据库,本质上是一次架构理念的升级。存储计算分离带来的弹性扩展能力、物理日志复制带来的毫秒级延迟、快照备份带来的秒级恢复——这些都是自建MySQL架构下难以实现的能力。
但迁移并非没有风险。上面5个踩坑案例,每一个都说明细节足以成为生产事故的导火索。记住,零停机迁移的核心不在于技术有多先进,而在于方案有多完善、回滚有多快速。
如果你也在考虑类似的迁移,建议按这个优先级来:
- 先评估:用迁移决策树确认方案,用检查清单确认就绪。
- 再验证:在测试环境完整走一遍迁移流程,模拟灰度切换和回滚。
- 后落地:选择低峰期开始灰度切换,每一步都有监控和回滚预案。
希望这次实战复盘能帮你少走弯路,顺利完成PolarDB的迁移。
