数据迁移
在系统间移动批量数据(DB→DB、格式→格式)带验证零丢失。
为什么
数据迁移是对你最值钱资产的单向操作 — 若行被丢或损坏,备份只回到迁移前,丢所有迁移后写。有纪律的方式是幂等分批 extract-transform-load,末尾行级校验和验证,加双写窗口使迁移期间新写同时入新旧。这使迁移可续(80% 崩不从头)、可验(校验和 diff 抓静默行丢)、无损(双写捕迁移期间写)。跳任一是团队丢用户数据一周不察的方式 。
何时使用
换新 DB、改数据格式或合并系统时。>1GB 迁移计划双写窗口。
如何做
1. 设双写:新写同时入新旧(使迁移捕复制期间写) 2. 从旧幂等分批提取:SELECT ... WHERE id > :cursor ORDER BY id LIMIT 1000 3. 每批转换(旧 schema 映射 -> 新 schema);插入前每行验证 4. 用 upsert 载新(ON CONFLICT DO UPDATE)使重跑安全(幂等) 5. 跟游标/检查点使崩从最后批续而非从 0 6. 批量复制后验证:COUNT(*) 旧 == 新,且行级校验和样本匹配 7. 读切到新;保双写监控窗口;然后停写旧 8. 旧 DB 保只读 30 天作取证回退再退役
示例
# 幂等分批迁移(Node + Postgres)
async function migrate() {
let cursor = await loadCheckpoint(); // 从最后批续
while (true) {
const rows = await oldDb.query(
'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT 1000', [cursor]);
if (rows.length === 0) break;
const transformed = rows.map(toNewSchema);
await newDb.query(
'INSERT INTO users_new (id, email, name) VALUES ($1,$2,$3) ' +
'ON CONFLICT (id) DO UPDATE SET email=EXCLUDED.email, name=EXCLUDED.name',
transformed);
cursor = rows[rows.length - 1].id;
await saveCheckpoint(cursor);
await sleep(50); // 节流
}
}
# 验证(批量复制后跑)
const oldCount = await oldDb.query('SELECT count(*) FROM users');
const newCount = await newDb.query('SELECT count(*) FROM users_new');
assert(oldCount === newCount, '行数不匹配!');
# 行级校验和样本(抓静默损坏)
const sample = await oldDb.query('SELECT id, md5(row_to_json(t)::text) FROM users TABLESAMPLE SYSTEM(1) t');
for (const { id, md5 } of sample) {
const newRow = await newDb.query('SELECT row_to_json(*) FROM users_new WHERE id=$1', [id]);
assert(md5 === md5(JSON.stringify(newRow)), `行 ${id} 损坏`);
}常见问题
- ⚠无双写 — 4 小时复制期间写旧 DB 的行丢,因迁移始于它们之前。
- ⚠不可续迁移 — 80% 崩从头起,窗口和风险翻倍。
- ⚠无验证 — 迁移"完成"但静默丢 200 行;一周后用户投诉才知。
最佳实践
- ✓迁移窗口双写使新写落两处 — 这是多小时复制不丢写的唯一方式。
- ✓迁移幂等(upsert)可续(检查点)— 崩从最后批续而非零。
- ✓用行数 + 采样行级校验和验证 — 单行数漏损坏;校验和抓静默数据损。