KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 迁移与结构版本 — keel 龙骨
类型:主题概念 · 应用课对应:06 章 · 升级与回滚:要么升上去,要么原样退回来
类型:主题概念 · 应用课对应:06 章 · 升级与回滚:要么升上去,要么原样退回来
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲怎么设计表结构。
一、它为什么会出现
一次回滚:命令跑完,退出码是 0,屏幕上没有多说什么。看起来退回去了。
然后起栈,worker 的探针当场喊出真相:
[FAIL] 库结构版本:3,镜像要求 2 —— 先跑 `board migrate`
紧跟着一句 container board2-worker-1 is unhealthy。那个退出码为 0 的命令,其实什么都没做。
原因是执行它的那个镜像不认识第 3 版结构——在它看来,库本来就在它认识的那一版上,所以目标版本与当前版本一比,它认为"已经在位"。
需求一句话:库结构会随版本往前变,所以它必须被版本化、能被读出来,也必须能退回去。
二、它是什么
一句话:迁移是"升级 = 跑到目标版本 / 回滚 = 退到目标版本"的同一段代码;结构版本,就是 schema_migrations 里那个最大的版本号。
它不是下面这三样东西:
- 不是"一段按顺序执行的 SQL"。 它是若干条
(版本号, 上行语句, 下行语句)三元组——下行语句的存在,才是"可回滚"的地基。 - 不是"要么全成、要么全不成"。 迁移是逐条提交的,所以一次失败的迁移往往停在中间,而不是回到原样。
- 不是"退出码 0 就等于改了结构"。 上面那次回滚就是反例:命令成功,结构没动。
三、它长什么样
它首先是一张表,加一组三元组。库这一侧的读数只有一行:
SELECT MAX(version) FROM schema_migrations;
那个值就是现场自检里报出来的「结构版本」:
结构版本 2
代码这一侧,每一版都是一对上、下行语句:
MIGRATIONS = (
(1, "CREATE TABLE IF NOT EXISTS tasks ...", "DROP TABLE IF EXISTS tasks;"),
(2, "ALTER TABLE tasks ADD COLUMN priority ...", "ALTER TABLE tasks DROP COLUMN priority;"),
(3, "ALTER TABLE tasks ADD COLUMN claimed_at ...", "ALTER TABLE tasks DROP COLUMN claimed_at;"),
)
跑一次"往下走",日志会说清它做了什么:
{"event": "migrate.step", "direction": "down", "version": 3}
{"event": "migrate.done", "from": 3, "to": 2}
direction 有两种取值(up / down);而 from 与 to 之间差了几版,才是这次动作真正覆盖的范围。
四、它怎么工作
从"我要把库挪到第 N 版"到"结构真的停在第 N 版",四步:
① 读现状 查 schema_migrations 的最大版本号,这就是"现在在哪一版"
↓
② 定方向 目标比现在大就往上升、小就往下降;步集合按方向排序(往下走要倒着来)
↓
③ 逐条执行 一版一版地跑:上行就记一条版本,下行就删一条版本,每版各自提交
↓
④ 报终点 打印 migrate.done 的 from / to —— 这两个数才是"它到底做了什么"
第 ③ 步是理解一切的关键:每一版各自提交,所以"迁移失败"很少是"什么都没发生",而是"停在中间"。 这也解释了为什么升级失败之后不能只是"切回旧镜像重来"——结构不会自己退回去。
五、它会带出哪些概念
- 交付包:推进结构的那段代码住在包里的镜像里,对面没有别的东西可用。→ 00 页
- 停机窗口:迁移是"手上的活",它跑起来的时候,停栈这件事就带上了一段不能忽略的耗时。→ 04 页
- 实例:一套编排、一份数据卷对应一个结构版本;两套实例的结构各自独立。→ 02 页
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
"迁移不是一段 SQL,而是一组 (版本号, 上行, 下行) 三元组" |
因为回滚走的就是同一组语句的另一个方向,缺了下行就没有回滚 |
| "回滚时执行迁移的镜像,必须是认识新结构的那一版" | 不认识第 3 版的镜像看库本来就"已经在位",于是 --to 变成一次静默的空操作 |
| "一个退出码为 0 的动作,不等于它做了你以为的那件事" | 命令成功只说明它没报错;它做没做,要看 migrate.done 的 from 与 to |
| "结构不会自己退回去" | 结构是被一版一版提交出来的,往回走也必须一版一版地执行下行语句 |
这一页改变了你的哪个判断:看到"迁移成功"时,你不再只看退出码,而是先问**"它从哪一版走到了哪一版"**——from 等于 to,就是"它根本没动"。
→ 这一页的展开在应用课 06 章 · 升级与回滚:要么升上去,要么原样退回来。
自测题
- 为什么"迁移失败"通常意味着"停在中间",而不是"什么都没发生"?
- 一个命令退出码是 0,你凭什么判断它到底有没有改结构?