零停机迁移的 Expand and Contract 模式
发布: (2025年12月20日 GMT+8 23:00)
4 min read
原文: Dev.to
Source: Dev.to
抱歉,您只提供了来源链接,未附上需要翻译的正文内容。请把要翻译的文章文本粘贴在这里,我会为您翻译成简体中文并保留原有的 Markdown 格式。
步骤
- 扩展 – 在旧结构旁边添加新结构。
- 迁移 – 在应用程序中引入双写逻辑并回填现有数据。
- 收缩 – 在没有实例依赖旧结构时将其移除。
每个步骤在单独的部署中执行。
示例:重命名列
将 users.name 重命名为 users.full_name。
扩展
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
双写部署
func (r *UserRepo) Create(ctx context.Context, user *User) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO users (name, full_name, email)
VALUES ($1, $1, $2)
`, user.FullName, user.Email)
return err
}
func (r *UserRepo) UpdateName(ctx context.Context, id int, name string) error {
_, err := r.db.ExecContext(ctx, `
UPDATE users SET name = $1, full_name = $1 WHERE id = $2
`, name, id)
return err
}
读取并回退
func (r *UserRepo) GetByID(ctx context.Context, id int) (*User, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id, COALESCE(full_name, name), email FROM users WHERE id = $1
`, id)
var user User
err := row.Scan(&user.ID, &user.FullName, &user.Email)
return &user, err
}
回填已有行
UPDATE users SET full_name = name WHERE full_name IS NULL;
收缩
ALTER TABLE users DROP COLUMN name;
移除双写逻辑
func (r *UserRepo) Create(ctx context.Context, user *User) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO users (full_name, email) VALUES ($1, $2)
`, user.FullName, user.Email)
return err
}
将地址列提取到单独的表
扩展
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) UNIQUE,
street VARCHAR(255),
city VARCHAR(100),
postal_code VARCHAR(20)
);
双写部署
func (r *UserRepo) UpdateAddress(ctx context.Context, userID int, addr Address) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Write to old columns (for v1 instances)
_, err = tx.ExecContext(ctx, `
UPDATE users
SET address_street = $1, address_city = $2, address_postal_code = $3
WHERE id = $4
`, addr.Street, addr.City, addr.PostalCode, userID)
if err != nil {
return err
}
// Write to new table (for v2 instances)
_, err = tx.ExecContext(ctx, `
INSERT INTO addresses (user_id, street, city, postal_code)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id) DO UPDATE SET
street = EXCLUDED.street,
city = EXCLUDED.city,
postal_code = EXCLUDED.postal_code
`, userID, addr.Street, addr.City, addr.PostalCode)
if err != nil {
return err
}
return tx.Commit()
}
带回退的读取
func (r *UserRepo) GetAddress(ctx context.Context, userID int) (*Address, error) {
// Try new table first
row := r.db.QueryRowContext(ctx, `
SELECT street, city, postal_code FROM addresses WHERE user_id = $1
`, userID)
var addr Address
err := row.Scan(&addr.Street, &addr.City, &addr.PostalCode)
if err == nil {
return &addr, nil
}
if err != sql.ErrNoRows {
return nil, err
}
// Fall back to old columns
row = r.db.QueryRowContext(ctx, `
SELECT address_street, address_city, address_postal_code
FROM users WHERE id = $1
`, userID)
err = row.Scan(&addr.Street, &addr.City, &addr.PostalCode)
return &addr, err
}
回填
INSERT INTO addresses (user_id, street, city, postal_code)
SELECT id, address_street, address_city, address_postal_code
FROM users
WHERE address_street IS NOT NULL
ON CONFLICT (user_id) DO NOTHING;
合约
ALTER TABLE users
DROP COLUMN address_street,
DROP COLUMN address_city,
DROP COLUMN address_postal_code;
在所有实例运行新代码后,移除双写和回退逻辑。
将价格从整数(分)改为小数(美元)
扩展
ALTER TABLE products ADD COLUMN price_decimal DECIMAL(10,2);
双写部署
func (r *ProductRepo) UpdatePrice(ctx context.Context, id int, cents int) error {
dollars := float64(cents) / 100.0
_, err := r.db.ExecContext(ctx, `
UPDATE products SET price = $1, price_decimal = $2 WHERE id = $3
`, cents, dollars, id)
return err
}
回填
UPDATE products SET price_decimal = price / 100.0 WHERE price_decimal IS NULL;
收缩
ALTER TABLE products DROP COLUMN price;
ALTER TABLE products RENAME COLUMN price_decimal TO price;
参考文献
- Deployment Strategies Visualized – 滚动、蓝绿、金丝雀和渐进式发布的可视化指南。