
零停机迁移的挑战
传统迁移会锁定表。在规模较大的场景下,即使高流量表上1秒的锁也会导致级联超时。

扩展/收缩模式
永远不要在一次迁移中执行破坏性变更。使用三个阶段:
阶段1:扩展(向后兼容)
添加新列/表,但不移除旧列/表。
阶段2:迁移
回填数据,更新应用程序代码。

阶段3:收缩(清理)
在所有代码部署完成后移除旧列。
安全重命名列
-- 绝对不要这样做(会破坏正在运行的应用):
-- ALTER TABLE users RENAME COLUMN username TO user_name;
-- 阶段1:扩展——添加新列
ALTER TABLE users ADD COLUMN user_name VARCHAR(255);
-- 复制数据(可在后台运行)
UPDATE users SET user_name = username WHERE user_name IS NULL;
-- 阶段2:让两者在应用程序代码中同时工作
-- 写入两个列,从新列读取并回退到旧列
-- 部署能处理两者的应用
-- 阶段3:收缩——在所有应用实例更新后
ALTER TABLE users DROP COLUMN username;
添加 NOT NULL 列
-- 绝对不要:
-- ALTER TABLE orders ADD COLUMN status VARCHAR(50) NOT NULL DEFAULT 'pending';
-- 这会重写整个表!
-- 阶段1:添加可空列(即时)
ALTER TABLE orders ADD COLUMN status VARCHAR(50);
-- 阶段2:批量回填(无锁)
DO $
DECLARE
batch_size INT := 1000;
last_id BIGINT := 0;
max_id BIGINT;
BEGIN
SELECT MAX(id) INTO max_id FROM orders;
WHILE last_id < max_id LOOP
UPDATE orders
SET status = 'completed'
WHERE id > last_id AND id <= last_id + batch_size
AND status IS NULL;
last_id := last_id + batch_size;
PERFORM pg_sleep(0.01); -- 限速以避免过载
END LOOP;
END $;
-- 阶段3:添加约束(验证,PG中不重写)
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
CHECK (status IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;
-- NOT VALID 即时添加;VALIDATE 在后台运行
-- 阶段4:使其成为真正的 NOT NULL(快速,已验证)
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
ALTER TABLE orders DROP CONSTRAINT orders_status_not_null;

无阻塞创建索引
-- 不好:在索引构建期间阻塞所有写入
CREATE INDEX idx_orders_user ON orders (user_id);
-- 好:CONCURRENTLY 构建而不阻塞
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
-- 耗时更长,但不阻塞读取或写入
-- MySQL 等效
ALTER TABLE orders ADD INDEX idx_user (user_id), ALGORITHM=INPLACE, LOCK=NONE;
大表数据回填
// 无锁批量回填
async function backfillInBatches(db: Pool) {
const BATCH_SIZE = 500
let lastId = 0
let processed = 0
while (true) {
const result = await db.query(
`UPDATE orders
SET normalized_status = LOWER(status)
WHERE id > $1 AND id <= $1 + $2
AND normalized_status IS NULL
RETURNING id`,
[lastId, BATCH_SIZE]
)
if (result.rowCount === 0) break
processed += result.rowCount
lastId += BATCH_SIZE
console.log(`Processed: ${processed}`)
await sleep(10) // 限速——给数据库喘息空间
}
}
架构变更的特性标志
// 使用特性标志逐步推出架构变更
async function getUser(id: string) {
const useNewSchema = await featureFlags.isEnabled('new_user_schema', { userId: id })
if (useNewSchema) {
return db.query('SELECT id, full_name, email FROM users_v2 WHERE id = $1', [id])
}
return db.query('SELECT id, first_name || ' ' || last_name AS full_name, email FROM users WHERE id = $1', [id])
}
迁移检查清单
- 先在生产数据的副本上测试
- 为每次迁移准备回滚计划
- 迁移期间监控数据库指标
- 使用
CONCURRENTLY创建索引 - 批量处理大数据更新
- 永远不要在代码变更的同一部署中删除列
- 设置
lock_timeout以防止长时间等待 - 对于风险变更,在低流量时段执行
锁超时安全
-- 设置锁超时以快速失败而不是排队
SET lock_timeout = '2s';
BEGIN;
ALTER TABLE users ADD COLUMN score INT DEFAULT 0;
-- 如果在2秒内未获得锁,则干净地失败
COMMIT;