外观
金融级账务大表MySQL到PG增量断点续传迁移实录
一、金融级账务大表迁移的问题背景
我们本次迁移的核心对象是某支付平台的账务流水大表,单表数据量达1.2亿行,存储占用超过180GB,原运行在MySQL 5.7主从集群上,为了适配后续的复杂对账查询、向量风控检索需求,计划整体迁移到PostgreSQL 15集群。 金融场景的迁移要求远高于普通业务:第一是绝对不能丢数,数据一致性要求100%,不能出现单边账务流水缺失;第二是业务停机窗口不能超过5分钟,全量迁移阶段不能中断线上服务;第三是迁移过程的异常容错能力要强,不能因为网络抖动、服务器资源抢占就导致整个迁移任务从头重来,否则原本预估的12小时迁移周期会完全失控。 前期我们踩过坑:直接用pgloader跑全量迁移的时候,同步到70%的时候因为数据库连接超时中断,只能全部回滚重跑,浪费了超过8小时的时间,完全不符合业务预期,最终我们决定自研一套双锚点断点续传的增量迁移方案,把整个迁移周期的不可控风险降到最低。
二、主流MySQL到PG迁移方案对比
我们提前测试了市面上3种主流的迁移方案,对比结果如下: 第一种是开源Debezium全量+binlog同步方案,优势是原生支持CDC增量同步,但是默认没有内置断点续传能力,进程异常重启后只能重置全量同步位点,针对亿级大表的重试成本极高,而且没有内置的数据一致性校验能力。 第二种是官方pgloader一键迁移方案,优势是开箱即用,兼容大部分MySQL到PG的类型映射,但是默认的事务机制要求全量同步成功后才会提交数据,只要中间任意一行同步失败,整个任务全部回滚,1亿行以上大表的容错性几乎为零。 第三种是本次落地的自研断点续传增量迁移方案,核心是全量阶段用自增ID作为锚点、增量阶段用MySQL binlog位点作为锚点,每同步1万行数据就持久化一次同步位点,任务中断后重启直接从上次记录的位点继续同步,整体性能损耗不到5%,全量迁移耗时从原本的12小时压缩到4.5小时,中间出现过3次网络抖动中断,全部在10秒内恢复续传,没有一次需要重跑。
三、断点续传增量迁移实操步骤与代码示例
整个方案分为位点元数据初始化、全量断点续传同步、增量位点锚定同步、双写校验切换四个阶段,所有代码都在生产环境经过亿级数据验证可以直接运行。
3.1 迁移位点元数据表初始化
首先在目标PG库创建迁移任务位点表,用来持久化所有同步进度信息,避免任务重启后进度丢失,PG方言的建表SQL如下:
sql
-- 创建迁移任务位点表,所有进度原子持久化
CREATE TABLE IF NOT EXISTS migration_sync_checkpoint (
task_id VARCHAR(64) PRIMARY KEY, -- 唯一迁移任务ID,比如account_bill_202405
source_table VARCHAR(128) NOT NULL, -- 源MySQL表名
last_pk_id BIGINT DEFAULT 0, -- 全量阶段最后同步的自增主键ID
last_binlog_file VARCHAR(128), -- 增量阶段最后同步的binlog文件名
last_binlog_pos BIGINT DEFAULT 0, -- 增量阶段最后同步的binlog位点
sync_status SMALLINT DEFAULT 0, -- 0待同步 1全量同步中 2全量完成 3增量同步中 4已完成
update_time TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
remark TEXT
);
-- 给位点表创建行锁优化,避免并发更新冲突
CREATE INDEX idx_migration_task_status ON migration_sync_checkpoint(task_id, sync_status);3.2 全量阶段断点续传逻辑实现
全量同步阶段我们不做一次性全表拉取,而是每次从位点表读取上次已经同步完成的最大自增ID,每次分页拉取1万行数据插入到PG端,插入成功后在同一个事务内更新位点表的last_pk_id字段,就算进程宕机也不会出现部分同步的问题。 对应的核心查询逻辑SQL示例:
sql
-- 从位点表读取当前任务的最新同步位点
SELECT last_pk_id FROM migration_sync_checkpoint WHERE task_id = 'account_bill_202405';
-- 每次批量拉取源表数据:SELECT * FROM account_bill WHERE id > {last_pk_id} ORDER BY id LIMIT 10000;
-- 批量插入PG端成功后,原子更新位点
UPDATE migration_sync_checkpoint
SET last_pk_id = last_pk_id + 10000, update_time = now()
WHERE task_id = 'account_bill_202405';3.3 增量同步阶段位点锚定逻辑
全量同步完成后,切换到增量同步模式,读取全量完成瞬间的MySQL binlog文件名和位点号,开始消费后续的binlog事件,每消费完1万条变更事件,就把当前的binlog位点更新到位点表中,就算同步进程异常退出,下次启动直接从最新记录的位点继续消费,不会出现重复同步或者漏同步的问题。 双写切换阶段我们把业务写入同时转发到MySQL和PG两端,跑72小时全量哈希分片校验,确认两边数据100%一致后才切走流量。
四、落地避坑总结
我们在整个落地过程中踩了不少实践坑,总结出3条金融级场景必须遵守的注意事项: 第一点,绝对不能用更新时间作为断点续传的锚点。很多开发者图方便用gmt_modified字段作为分页同步的条件,但是MySQL的InnoDB事务提交存在乱序问题,后启动的事务可能先提交,导致部分更新时间更早的数据被漏同步,修正方案是全量阶段必须用全局有序的自增主键作为唯一分页锚点,增量阶段用binlog精确位点作为锚点,双锚定机制完全避免漏数据问题。 第二点,位点更新必须和数据插入放在同一个PG事务中。不少开发者的错误操作是先更新位点表,再插入同步的业务数据,如果进程刚好在两步之间宕机,就会出现位点已经前进但是业务数据没有写入的丢数问题,修正方案是把批量数据插入和位点更新放在同一个事务上下文里,利用PG的事务原子性保证两个操作要么全部成功、要么全部回滚。 第三点,账务类大表迁移不能默认过滤软删除数据。很多迁移工具会默认跳过is_deleted=1的软删除行,导致历史对账数据缺失,不符合金融监管要求,修正方案是同步阶段全字段全行导入所有数据,等迁移完成后再根据业务逻辑单独做过滤处理,和源库的全量数据做逐行哈希校验,确保没有任何行丢失。 本次迁移最终整体耗时不到6小时,业务停机切换时间仅2分40秒,上线后连续运行3个月没有出现任何数据不一致问题,验证了这套断点续传方案的稳定性。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。