外观
从万分之三误差到0:MySQL到PG双写一致性校验实战
问题背景:电商订单库迁移的双写阶段痛点
我们的电商核心订单库单表承载1.2亿条订单数据,此前长期运行在MySQL 8.0集群上,随着大促峰值QPS突破2万,逐渐暴露出行锁冲突概率高、JSON类型订单快照查询性能差、分区表运维成本高等问题,团队最终敲定将核心库整体迁移至PostgreSQL 16版本,充分利用PG原生MVCC优势、JSONB索引能力和原生分区特性支撑后续3年的业务增长。 整个迁移流程中风险最高的环节就是双写阶段:在切流到PG读之前,业务侧会同时将订单写入MySQL和PG两个库,此前试点阶段我们曾观测到万分之三的不一致率,问题来源包括异步双写网络丢包、跨库事务提交顺序不一致、时间戳精度差异、大字段序列化逻辑不同等,一旦这类不一致问题没有被提前发现,切流后就会出现用户看不到订单、订单状态错乱、资损对账不平的严重事故,因此我们必须落地一套100%覆盖全量数据、兼顾增量校验性能的一致性校验方案。
方案对比:三类主流校验方案的选型决策
我们先后评估了行业内三类常见的双写校验方案,最终选择了性能和准确率平衡的PG侧哈希分桶校验方案:
- 全量逐行对比方案:通过同步工具把两边数据拉到第三方服务逐字段对比,缺点是1.2亿条数据全量传输+对比需要72小时以上,完全无法适配双写阶段数据实时变动的校验需求,资源占用成本极高。
- Binlog消费对比方案:部署Canal同时消费MySQL和PG的Binlog,逐条解析变更日志做对比,缺点是需要改造大量中间件组件,大订单快照这类几十KB的大字段传输校验成本极高,日志延迟带来的误判率超过1%。
- PG侧分桶哈希校验方案:所有聚合计算逻辑直接在PG数据库内部完成,仅输出分桶后的哈希摘要和少量不一致明细,跨库传输的数据量不到全量的万分之一,实测1.2亿条数据全量校验仅需40分钟,完全满足双写阶段的校验要求。
实操步骤与PG方言代码示例
整套校验流程分为预校验全量哈希、增量游标实时校验、切流前快照终校三个阶段,所有核心SQL均为PG原生语法,可直接在PostgreSQL 12+版本运行:
1. 全量分桶哈希预校验
预校验阶段我们不需要逐行对比数据,只需要把订单表按全局唯一单号order_sn做哈希分桶,每个桶内的所有字段拼接后计算整体哈希摘要,和MySQL侧同桶计算的摘要做对比,仅当桶摘要不一致时才拉取桶内明细做逐行对比,性能提升10倍以上:
sql
-- PG侧订单表分桶哈希校验SQL,分1024个桶计算摘要
SELECT
bucket_id,
MD5(STRING_AGG(
MD5(
order_id::TEXT || '|' ||
order_sn || '|' ||
order_status::TEXT || '|' ||
create_time::TIMESTAMP(0)::TEXT || '|' ||
json_strip_nulls(order_snapshot)::TEXT
), '' ORDER BY order_id
)) AS bucket_hash
FROM (
SELECT
*,
ABS(FNV1A_64(order_sn) % 1024) AS bucket_id
FROM order_main
-- 按迁移时间快照固定数据视图,避免幻读
WHERE create_time < '2024-06-01 00:00:00'
) t
GROUP BY bucket_id
ORDER BY bucket_id;2. 增量双写阶段实时校验
双写阶段数据处于实时变动状态,我们利用PG的快照特性实现无锁增量校验,每5分钟拉取上一批校验之后的新增变更数据做逐行对比,不会阻塞业务写入:
sql
-- PG侧增量变更游标校验SQL,基于xmin实现无锁增量扫描
WITH new_orders AS (
SELECT * FROM order_main
WHERE xmin::text::bigint > (SELECT last_check_xmin FROM migration_check_status)
)
SELECT
order_sn, order_id, order_status, create_time
FROM new_orders
-- 仅输出增量数据的唯一标识和核心字段,跨库对比后快速定位不一致明细
ORDER BY order_sn;3. 切流前快照终校
正式切流前我们会暂停双写写入10秒,生成两边的一致性快照,做最终的全量校验,确认100%无不一致之后再切流读流量到PG。
避坑总结:3条来自生产实践的核心注意事项
我们在落地这套校验方案的过程中踩过多个行业通用的隐蔽坑点,整理出3条可直接复用的避坑规则:
- 时间戳精度差异误判坑:常见错误现象是两边数据业务逻辑完全一致,但是校验系统判定不一致,根因是MySQL的datetime默认精度是秒级,存储时自动丢弃毫秒部分,而PG的timestamp默认精度是微秒级,双写时会保留传入的毫秒值,直接字符串对比就会出现误判。修正方法是所有时间字段统一转成
timestamp(0)精度之后再做拼接哈希,不要直接使用原始字段做对比。 - 自增主键冲突校验遗漏坑:常见错误现象是双写阶段出现两条不同业务订单共用同一个自增ID的问题,根因是很多团队校验时把自增主键order_id作为第一对比维度,但是双写阶段如果两边都开启自增主键生成,很容易出现步长重叠的主键冲突。修正方法是PG侧的自增主键初始值设置为MySQL当前最大ID的2倍以上,同时把全局唯一业务单号order_sn作为第一校验维度,完全规避自增ID冲突带来的校验逻辑失效。
- JSON字段序列化差异误判坑:常见错误现象是订单快照JSON内容完全一致,但是哈希对比结果不同,根因是MySQL的JSON类型存储时会自动去除字段前后的多余空格,而PG的JSONB类型会保留原始写入的空格、换行符等特殊字符,直接做字符串对比就会出现误判。修正方法是两边都使用PG内置的
json_strip_nulls函数对JSON字段做归一化处理,自动去除空值和多余空白字符之后再计算哈希值,完全消除序列化逻辑差异带来的误判。 这套校验方案上线后我们完成了1.2亿订单数据的全量校验,把双写阶段的不一致率从万分之三降到了0,整个迁移过程零资损零故障,顺利完成了核心订单库从MySQL到PG的平滑切换。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。