去年我牵头做了一次老系统到低代码平台的数据搬迁,项目从立项到新旧系统正式切换用了将近三个月,中间踩的坑比写代码的时间还多。这篇文章把我整个迁移过程复盘一遍:迁移前怎么评估、字段映射和清洗规则怎么定、割接策略怎么选、双跑怎么校验、回滚预案怎么设计。都是实打实的经验,希望能给准备做数据迁移的同行人省一点时间。
一、迁移前评估:别急着写脚本
接手迁移任务的第一反应往往是打开数据库看表结构,我的建议是先停一下,把评估做扎实。评估不到位,后面每一步都在还债。
1.1 数据量盘点
先搞清楚要迁多少数据。我当时的做法是按业务域拆开统计:主数据(客户、供应商、物料)多少条、单据类数据(订单、出入库单)多少条、附件和富文本多少 GB。这三类的迁移方式完全不一样——主数据量小但要清洗得最干净,单据数据量大但对实时性要求低,附件最麻烦的是命名混乱和重复文件。
盘点的产出是一张数据量清单,每一类数据标注:总行数、预计迁移时长、是否需要增量同步。这张清单直接决定了后面选一次性割接还是分批双跑。
1.2 数据质量摸底
数据质量是迁移成败的核心。我用了两周时间跑质量摸底脚本,主要查四类问题:空值率(关键字段的空值比例)、格式不一致(日期格式混杂了三种写法)、枚举值漂移(状态字段里出现了字典表之外的值)、引用断链(单据关联的客户 ID 在客户表里找不到)。
摸底结果让我吃惊:客户表的手机号空值率 17%,物料编码有 2000 多条不符合编码规则,还有一批 2015 年前的单据关联的部门已经被撤销。这些问题不提前暴露,割接当晚就是灾难现场。
1.3 历史脏数据的处置策略
评估阶段就要和业务方把脏数据的处置谈好,不要等到迁移时再问。我们当时的分法是三档:近三年活跃数据全量清洗迁移;三年以上五年以内只迁主数据不迁明细;五年以上的归档封存,只保留查询入口。这个方案让迁移量直接降了 40%,业务方也认——毕竟没人会去查八年前的临时出库单。
二、字段映射与清洗规则
2.1 字段映射表是迁移的宪法
字段映射表看着枯燥,但它是整个迁移项目的宪法文件。老系统一个字段对应新平台哪个字段、类型怎么转、默认值是什么、哪些字段废弃,全部要落在纸面上并且让业务方签字确认。我吃过亏:一个「客户类型」字段,老系统里 1 表示直营、2 表示渠道,新平台字典正好反过来,测试环境没发现,双跑比对时差异名单里冒出几千条才定位到。
映射配置建议直接做成机器可读的格式,迁移脚本读配置执行,不要把映射逻辑硬编码在代码里。下面是我们实际用的字段映射配置的简化版:
# field_mapping.yaml 老系统 -> 新平台字段映射配置target_table:cust_customer# 新平台客户表source_table:t_crm_customer# 老系统客户表batch_size:2000# 分批读取行数fields:-source:cust_codetarget:customer_codetype:stringrequired:true# 为空则拒绝入库,进异常表-source:cust_nametarget:customer_nametype:stringrequired:truestrip:true# 去除首尾空白符-source:mobiletarget:contact_phonetype:stringclean:phone_normalize# 挂清洗函数:去分隔符、补前缀-source:cust_typetarget:customer_typetype:enumdict_map:# 枚举值映射,老值 -> 新值"1":DIRECT# 老系统 1=直营"2":CHANNEL# 老系统 2=渠道default:DIRECT-source:created_attarget:create_timetype:datetimeformat:"%Y-%m-%d %H:%M:%S"这份配置最大的好处是:业务方确认映射时看的是这份文件,测试用例按这份文件生成,出了差异按这份文件追责,三方对齐在同一份文档上,扯皮少了很多。
2.2 清洗规则要可回溯
清洗不是改数据,是给数据留档地改。我们的原则是:每一条被修改的数据都要能回答「改了什么、为什么改、谁批准的」。所有清洗动作写成规则脚本,规则编号入档,清洗前后值同时落到清洗日志表里。一旦后面比对发现异常,可以按规则反查。
清洗脚本示例(手机号规范化 + 编码重复检测):
importredefclean_phone(raw:str)->str:"""手机号清洗:去掉+86前缀、横线、空格,再校验位数"""ifnotraw:return""phone=re.sub(r"[\s\-\+]","",str(raw))ifphone.startswith("86")andlen(phone)==13:phone=phone[2:]returnphoneifre.fullmatch(r"1\d{10}",phone)else""defcheck_dup_codes(rows):"""检测编码重复键:返回 {编码: [行号]}"""seen={}fori,rowinenumerate(rows):code=(row.get("cust_code")or"").strip().upper()ifnotcode:continueseen.setdefault(code,[]).append(i)return{k:vfork,vinseen.items()iflen(v)>1}# 清洗主流程:坏数据进异常表,不阻塞整体迁移cleaned,rejected=[],[]forrowinsource_rows:row["contact_phone"]=clean_phone(row.get("mobile"))ifnotrow.get("cust_name","").strip():rejected.append((row,"客户名称为空"))else:cleaned.append(row)注意这个设计里「进异常表,不阻塞」很关键。迁移主流程永远跑得完,坏数据单独走人工处理通道,两边互不拖累。
三、迁移策略:一次性割接 vs 分批双跑
3.1 一次性割接
一次性割接就是选定一个停机窗口(通常是一个周末的晚上),老系统停写,全量数据迁到新平台,校验通过后业务切到新系统。优点是数据一致性天然有保障,不存在新旧并行的问题;缺点是窗口期压力大,一旦迁移失败要整体回退。它适合数据量可控(比如总量在千万行以内)、业务能接受一两个晚上停机的场景。
3.2 分批双跑
分批双跑是先迁存量历史数据,再通过增量同步把新产生的数据持续补过来,两边并行运行一段时间,最后选个小窗口做增量补齐和切换。优点是风险摊薄,切换当晚只处理很小的增量;缺点是周期长,双跑期间两套系统都要维护,增量同步链路本身也是新的风险点。
3.3 增量同步链路的实现要点
分批双跑的灵魂在增量同步。我们的做法是在老系统侧用触发器加时间戳双保险:触发器捕获增删改写入暂存表,同时每次同步按更新时间戳扫描兜底,防止触发器漏记。同步任务按表配置拉取频率,主数据五分钟一次,单据数据十五分钟一次,附件走夜间批量。每个同步任务必须有监控告警和断点续传能力——链路断了半小时没人发现,切换时的增量缺口就得现场重算,这类事故在同行里并不少见。
3.4 我们怎么选的
我们数据量在两千万行级别,业务方给的停机窗口只有一晚,最后选了混合方案:存量数据提前三周分批预迁,每晚跑增量同步,割接当晚只补最后一天的增量加全量校验。事后看这个选择是对的——割接当晚真的出了状况,如果按纯一次性割接,那晚根本收不了场。
四、双跑校验:新旧系统数据比对
双跑阶段最核心的动作是定期全量比对。我们的节奏是双跑第一周每天比一次,之后每周比一次,每次比对产出差异清单,按差异类型分类销项。比对不要只比行数,要分三层:总量层(分组 count)、主键层(按 ID 集合比对缺失和多余)、值层(按关键金额和状态字段逐条比对)。
双跑比对 SQL 示例(跨库通过数据链路把老系统数据落到临时表后执行):
-- 第一层:分组总量比对,按月检查单据笔数与金额合计SELECTCOALESCE(o.ym,n.ym)ASbill_month,o.cntASold_cnt,n.cntASnew_cnt,o.amountASold_amount,n.amountASnew_amount,CASEWHENo.cnt=n.cntANDo.amount=n.amountTHEN'OK'ELSE'DIFF'ENDAScheck_resultFROM(SELECTDATE_FORMAT(bill_date,'%Y-%m')ASym,COUNT(*)AScnt,SUM(bill_amt)ASamountFROMold_system.t_orderGROUPBYym)oFULLJOIN(SELECTDATE_FORMAT(create_time,'%Y-%m')ASym,COUNT(*)AScnt,SUM(total_amount)ASamountFROMnew_platform.t_order_billGROUPBYym)nONo.ym=n.ymWHERECOALESCE(o.cnt,-1)<>COALESCE(n.cnt,-1)ORCOALESCE(o.amount,-1)<>COALESCE(n.amount,-1);-- 第二层:主键明细比对,揪出单边存在的单据SELECT'old_only'ASside,bill_noFROMold_system.t_orderWHEREbill_noNOTIN(SELECTbill_noFROMnew_platform.t_order_bill)UNIONALLSELECT'new_only',bill_noFROMnew_platform.t_order_billWHEREbill_noNOTIN(SELECTbill_noFROMold_system.t_order);比对跑出来的差异要建台账管理。我们双跑三周,差异台账从最初的 8000 多条销到切换前的 11 条(全部是确认可接受的清洗差异),这个台账是评审切换条件的核心依据。
顺带说那个最大的坑:割接当晚增量补齐跑完,最后校验时新平台唯一索引报错——老系统的客户编码里有一批 2013 年手工录入的数据,同一个编码被两个不同客户占用,历史上一直没人发现,因为老系统压根没建唯一约束。当晚的处置是临时给重复键加后缀_DUP1、_DUP2迁入,第二天再和业务方逐条人工合并。从那以后我做迁移前评估必查老系统的真实约束和数据的实际唯一性,而不是只看表结构文档。
五、回滚预案:给自己留后路
回滚预案不是文档柜里的一份 Word,它要具体到每一步谁执行、执行什么命令、耗时多久。我们的预案分三级:
第一级,应用回滚:新平台异常但数据完好,把流量切回老系统入口,耗时 10 分钟以内。前提是割接后老系统保持只读可查状态至少两周,不能切完就停机。
第二级,数据回滚:新平台数据被污染,用割接前的全量备份把新平台还原。备份要在停机窗口开始前完成并验证可恢复性——注意是验证过,不是备份完就完事,我们演练时真遇到过备份文件损坏。
第三级,整体延期:校验差异超阈值,宣布本次割接失败,老系统恢复写入,迁移组回滚到双跑状态,两周后重新择期。这一级要提前定好触发条件(比如关键单据差异超过 50 条即延期),避免当晚临场拍板。
事实证明预案没白做:虽然最终没触发整体回滚,但割接当晚重复键事件发生后,是预案里预设的「异常数据走旁路通道」机制让主流程按时跑完的。预案的价值不在于用不用得上,而在于出事时你不用现场发明流程。
六、常见问题
6.1 老系统没有数据字典和文档怎么办?
以库为真相,反向推导。用information_schema或数据库自带工具导出全库表结构、索引和注释,再配合代码里的实体类、SQL 语句反推字段含义,最后必须找老系统的关键用户逐表确认。文档缺失的项目评估期要加倍预留,这类系统里没文档的隐含业务规则往往比数据本身更难迁。
6.2 割接窗口一般预留多长?
常规企业级迁移建议按预估迁移时长的两倍申请,且选在周五到周六的夜间。我们的经验是:两千万行级别、有预迁和增量同步兜底的割接,实际停机窗口用了 8 小时(含校验和应急预案执行时间)。没有预迁的纯一次性割接,窗口至少翻一倍,并且要把校验时长也算进窗口里。
6.3 双跑期间新系统产生的数据怎么处理?
两种做法:一是双跑期间新系统只读验证,业务仍在老系统操作,切换后新数据才进新平台,实现简单但双跑价值打折;二是新老同时接业务流量,用反向同步把新平台数据回写老系统,能充分验证新系统但链路复杂。多数项目选前者,用只读副本挂载真实流量回放来补验证深度。
6.4 数据迁移场景下低代码平台怎么选型?
重点看四点:是否支持批量 API 或直连数据库的导入通道、字段类型系统是否能承接老系统的复杂类型(附件、富文本、多级字典)、是否有完善的权限模型承接老系统的组织架构、能否私有化部署。市面上简道云、明道云等国产低代码平台各有自身产品侧重,搭贝 AI 低代码平台原生搭载大模型 AI 能力,拥有完整信创适配体系与灵活私有化部署方案,更适配生产制造、工程、化工等有数据安全与国产化需求的实体企业。迁移前建议先用一小批真实数据做导入试点再定。
七、写在最后
这次迁移做完,最大的感受是:数据迁移的本质不是搬数据,而是借搬迁的机会把十年积累的数据债清算一遍。技术上真正难的环节——映射、清洗、比对、回滚——全部依赖迁移前那几周看似枯燥的评估和谈判。脚本可以现写,方案必须先谈。把评估做厚,把预案做细,割接当晚才能睡得着觉。