
“如果现在停止迁移,数据会不一致,永远回不去了。”
凌晨两点,XX医院数据中心。老周盯着屏幕上的进度条,手在发抖。
迁移进度:87%。
总数据量:2.3 TB。
Tables 数量:2176张。
涉及的核心业务:三百万病人的历史病历、五年门诊记录、三年住院档案。
如果失败,后果不堪设想。
但迁移已经开始,没有”撤销”按钮。
1. 为什么这个迁移这么难?
这次迁移,不是简单的”升版本”,而是从旧架构V3.0,迁移到新架构V4.0。
两个架构的区别:
– V3.0是单体数据库,所有业务数据在一张库
– V4.0是微服务架构,业务数据分库分表:门诊库、住院库、药房库、财务库、病历库…
以前的迁移,只需要在同一个数据库里改表结构,数据不动——这次,要把数据从”一张大饼”拆成”五块小饼”,还要保证每块小饼都能重新拼回原来的样子(如果失败回滚)。
难点:
1. 数据拆分逻辑复杂:比如门诊缴费记录,原来在payment表里,现在要拆成paymentheader(支付头)和paymentitems(支付明细);还要关联到outpatient_visit(门诊就诊)表。拆分规则涉及六张表。
2. 历史数据质量堪忧:三年积累的数据,有很多”脏数据”——重复记录、缺失字段、编码错误(比如性别填了”未知”),这些在V3.0时代都容忍了,但V4.0的schema有严格约束,脏数据会导入失败。
3. 没有”试错”机会:迁移窗口只有两天(五一假期门诊量少)。两次迁移机会——第一次失败,第二次必须在12小时内完成,否则影响初二开诊。如果两次都失败,就只好延期,等着杨院长问责。
老周带人准备了三个月:
– 写迁移工具(自己开发的data-migrator)
– 清洗脏数据脚本
– 回滚方案
– 全量演练三次,每次都发现问题,每次都改,第三次演练才成功
但演练再成功,也不是真迁移。
2. 迁移开始后,第一个坑:脏数据
晚上八点,迁移开始。
前两个小时顺利:系统库、用户表、权限表…都是一马平川。
十点,开始迁移核心业务数据。
payment表开始迁移,1%…2%…
突然,报错。
“`
ERROR: Violation of NOT NULL constraint: column ‘patient_id’ cannot be null
“`
日志里指明,有一条记录的patient_id是NULL。
这是脏数据。
老周让小吴排查:SELECT COUNT(*) FROM payment WHERE patient_id IS NULL
结果:73条。
这些记录,都是V3.0时代的老数据,可能是创建记录时系统bug,patient_id没填。
小吴说:”跳过这73条吧,不影响整体。”
“不行。”老周说,”如果跳过,对账的时候会发现门诊对不上。而且,如果这73条都是大额缴费,财务损失谁负责?”
他们做了个决定:现场清洗。
写了一条UPDATE语句,试图从其他表关联补全patientid。但关联发现,这73条记录对应的visitid也缺失,无法追溯到具体是哪次就诊。
死循环。
“只能手工造一个patient_id了。”小吴说,”造一个虚拟患者,把这73条付款挂到他名下。等迁移完成,我们在新系统里加一个’未知患者’账户,把这些数据放进去,后续再处理。”
老周犹豫。虚拟数据虽然能过关,但数据准确性打了折扣。
“有没有其他办法?”
“或者,我们暂停迁移,先回滚,把脏数据彻底清理完再迁?”
回滚意味着放弃这次窗口,五一假期只剩一天了,不够。
时间不等人。
老周咬了咬牙:”现场清洗——把有问题的数据,标上’待处理’标签,迁过去后我们在新系统里专门建一个’脏数据沙箱’,隔离存放。”
这是妥协,但迁移不能停。
3. 第二个坑:数据不一致
凌晨一点,进度到63%。
小吴发现一个问题:visitdate字段,在V3.0里是datetime类型,V4.0里拆分成visitdate(日期)和visit_time(时间)。迁移工具把小吴写得有bug:在拆分日期和时间时,时区处理错了。
V3.0存储的是本地时间(东八区),迁移工具当成UTC时间处理,减了8小时。
结果:所有就诊时间的visit_time,都比实际时间晚8小时。
比如一次早上8点的就诊,迁过去后变成了凌晨0点。
“天呐…”小吴脸白了。
老周也傻了。
这不是小问题。时间错误,会影响排班、统计、甚至医保结算(医保要求精确到小时)。
“修复这个bug,但已经迁过去的数据怎么处理?”
更可怕的是:已经迁了63%的数据,现在发现一个重大bug,是继续迁(错上加错),还是回滚?
继续,所有数据都错,无法挽回。
回滚,63%的数据要清理,重新迁,时间不够。
老周深吸一口气:”调出这个bug的影响范围数据。我们现场修复——迁过去的63%,我们另写一个’修正脚本’,把时间加8小时。”
小吴心算了一下:数据量800万条,修正脚本跑一遍要2小时。
“时间够吗?”
“不够也要够。”老周说。
4. “修正脚本”成为赛跑
老周和团队吃了两片咖啡因,开始写修正脚本。
脚本逻辑很简单:
“`sql
UPDATE outpatient_visits
SET visit_time = DATEADD(hour, 8, visit_time)
WHERE visit_time IS NOT NULL
“`
但要跑800万行,必须在2小时内完成,否则夜深了,医院的业务开始恢复,没机会再改。
他们优化:
1. 分批更新,每次10万行,commit 后继续
2. 加索引:在visit_time上建临时索引,加速 update
3. 关掉binlog,减少IO
4. 调大innodbbufferpool_size,确保数据在内存里
脚本跑起来,每分钟更新12万行。
一小时,600万。
凌晨三点,修正完成。
迁移继续。
5. 最后一个坑:外键约束冲突
早上七点,进度97%。
只剩最后一批数据迁移:prescription(处方)表。
报错:
“`
ERROR: Cannot add or update a child row: a foreign key constraint fails (`prescription` constraint `fk_prescription_visit`)
“`
意思是:有一条prescription记录,引用的visitid,在outpatientvisit表里找不到。
脏数据 again。
但这次很奇怪:前96%的数据都关联成功,为什么最后3%会丢?
小吴排查:最后这批数据,是2024年12月31日跨年的那批。那几天系统做了一次数据归档——把半年前的记录移到历史库。
但归档工具可能有bug,把某些visit_id漏了。
“跳过吧,”小吴说,”就几条处方,影响不大。”
“不行。”老周说,”处方是核心业务,漏一条,病用药记录就不全。而且,这是系统性问题的体现——如果这里漏了,其他地方呢?”
他们决定:现场补数据。
方法:从旧库(V3.0)里,把这批visit_id对应的记录,手动补出来,再导入新库。
旧库还没关,可以查。
但旧库是生产环境,不能直接操作。他们只能查,不能改。
查询:SELECT * FROM outpatientvisit WHERE visitid IN (xxx, yyy, zzz)
发现这三条visitid对应的记录,已经被归档到outpatientvisit_history表了。
迁移工具没考虑到这种情况——只迁了主表,没迁历史表,导致引用断裂。
小吴把这些历史记录也迁过去,但迁到outpatient_visit主表(违反了业务逻辑,历史记录不应该混在主表里)。
“标记为历史记录。”老周说。
6. 100%完成后,还有验证
早上八点,迁移工具显示:100%。
所有人松了一口气。
但老周没放松:”迁移完成,不算完成;数据验证通过,才算完成。”
他们有一套验证流程:
1. 行数对比:每张表的记录数,新库 vs 旧库,差异率<0.1%
2. 总和校验:对金额、数量等关键字段,做SUM对比,应该相等
3. 样本抽查:随机抽取1000条记录,逐字段对比,应该一致
4. 业务逻辑验证:跑一遍核心业务流程(挂号→开处方→缴费),结果应该一致
前三个通过,第四个出问题。
模拟一次门诊全流程:挂一个号,开三个药,缴费。
在V4.0里,挂号的visitid,和处方的visitid,对不上。
又一轮排查发现:visit表的id字段是自增的,迁移过程中,新库的自增起点没设置对,导致新生成的ID和旧的不一样。但prescription表里的visit_id是直接迁过来的(旧的ID值),而新挂号的ID是新产生的(新的自增值),两者当然对不上。
“这是一个’活数据’问题,不是迁移问题。”小吴说。
老周明白了:迁移只迁了历史数据,但迁移完成后,新产生的数据用的ID和旧数据不连续。这会影响对账、追溯等需要全局ID唯一性的场景。
解决的方案:重置自增ID的起点,让它从旧库的最大ID+1开始。
但问题是:迁移后已经产生了一条新挂号记录(验证用的),ID是1。重置起点后,这条记录的ID会和后面的冲突。
只能删除这条验证数据,重置ID,再重新验证一次。
折腾到中午十二点,全部通过。
7. 事后反思:我们做对了什么?
这次迁移后,老周写了长篇复盘。
他的结论:
1. “现场清洗”是必须的能力
– 不要指望数据100%干净再迁
– 要能在迁移过程中,实时发现脏数据,实时处理(跳过、修正、隔离)
2. 修正脚本应该提前准备好
– 不是所有bug都能在迁移前发现
– 为每一类可能的数据问题,提前写好”修正脚本模板”,迁移时填参数就能跑
3. 验证必须自动化
– 人工抽查不够,要有程序自动跑完整的数据验证流程
– 验证通过率应该>99.99%
4. 要有”回滚点”概念
– 每完成一个业务单元(如门诊库),就做一个”回滚点”
– 后面的阶段失败,可以回滚到这个点,而不是全部重来
5. “迁移”不只是”搬数据”
– 还包括:ID生成策略、自增主键连续性、时间戳时区、字符集转换…
– 任何细节出错,都会导致业务逻辑错误
互动话题
你经历过最复杂的数据迁移是什么?有什么经验教训?
> 基于真实医院场景改编,人物均为化名
立即免费试用门诊系统:https://app.kmhis.com/
International Version:https://app.kmhis.com/multi/
了解软佳门诊管理系统详情:https://www.kmhis.com/outpatient-management-system.html
支持8种语言:简体中文、繁体中文、香港中文、English、藏文、泰文、老挝语、越南语
说真的。这类问题我见过太多了。每次看到医院同事为选型头疼。我就想,要是早点有人把这些经验分享出来就好了。毕竟。选择不对。后面全是麻烦。选择对了。省心省力。还能提升整个机构的运行效率。希望这篇能帮到正在纠结的你。
你如果有具体需求。也可以去 www.kmhis.com 看看。那里有更详细的技术方案和案例。
