数据库迁移是任何web应用程序的常见部分。它们用于更新数据库模式以匹配应用程序的代码。在传统的web应用程序中,数据库迁移是同步运行的,这意味着应用程序将被阻塞,直到迁移完成。这并不理想,因为这意味着用户在迁移期间无法使用该应用程序。很久以前,停止维修服务是可以接受的;但现在我们需要能够在不阻塞应用程序的情况下运行迁移。
在小型数据库中或没有负载的情况下,可以轻松执行数据库迁移。但是,如果您有一个大型数据库和大量用户,该怎么办?
初始数据库结构
假设我们有一个用于存储客户的数据的简单表:
DROP TABLE IF EXISTS customer;
CREATE TABLE
customer (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(256),
balance FLOAT NOT NULL DEFAULT 0,
UNIQUE INDEX email_idx (email)
);
INSERT
INTO
customer (email, balance)
VALUES
('alice@example.com', 42),
('bob@example.com', -42);
我主要关注的是“平衡”领域。
它是一个浮点数,这意味着它可以有一个小数部分。我们希望将此字段的类型更改为整数,以便以美分为单位存储余额。这是金融应用中的常见做法。
但也许您已经有了一个拥有数百万用户的活动应用程序。作为开发人员,您的责任是确保应用程序在迁移期间对用户可用。此外,您还需要确保应用程序在迁移后能够正常工作,如果它不能正常工作,您必须在没有很多客户注意到的情况下回滚更改。
因此,计划是:
-
1.创建一个整数类型的新字段“balance_cents”。
-
2.使用新字段部署应用程序。应为有限数量的用户提供新的应用程序版本。
-
3.确保应用程序在新字段中正常工作。
-
4.为所有用户部署新的应用程序版本。
-
5.删除旧字段“balance”。
棘手的部分是使新旧字段之间的数据保持一致。由于这些字段相互关联,我们需要确保旧字段中的更改反映在新字段中,反之亦然。
让我们一步一步来看看如何实施这个计划。
创建新字段
创建整数类型的新字段“balance_cents”是一项简单的任务:
ALTER TABLE
customer
ADD COLUMN
balance_cents INT NOT NULL DEFAULT 0;
设置新旧字段之间的同步
此时,应用程序对新字段一无所知,现在是在新旧字段之间建立同步的最佳时机。为了实现这一点,我们需要创建触发器
首先,让我们在insert上创建一个触发器。
DROP TRIGGER IF EXISTS insert_balances;
CREATE TRIGGER insert_balances
BEFORE INSERT
ON customer
FOR EACH ROW IF new.balance <> 0 THEN -- insert from the old code
SET
new.balance_cents = CEIL(new.balance * 100);
ELSEIF new.balance_cents <> 0 THEN -- insert from the new code
SET
new.balance = new.balance_cents / 100;
END IF;
$$
DELIMITER ;
让我们做一些实验,看看这个触发器是如何工作的。
从旧代码插入:
INTO
customer (email, balance)
VALUES
('account_from_old_code@example.com', -1.23);
SELECT
email,
balance,
customer.balance_cents
FROM
customer
WHERE
email = 'account_from_old_code@example.com';
+-----------------------------------+---------+---------------+
| email | balance | balance_cents |
+-----------------------------------+---------+---------------+
| account_from_old_code@example.com | -1.23 | -123 |
+-----------------------------------+---------+---------------+
从新代码插入:
INSERT
INTO
customer (email, customer.balance_cents)
VALUES
('account_from_new_code@example.com', 345);
SELECT
email,
balance,
customer.balance_cents
FROM
customer
WHERE
email = 'account_from_new_code@example.com';
+-----------------------------------+---------+---------------+
| email | balance | balance_cents |
+-----------------------------------+---------+---------------+
| account_from_new_code@example.com | 3.45 | 345 |
+-----------------------------------+---------+---------------+
因此触发器按预期工作。
现在,让我们为更新创建一个触发器。
DROP TRIGGER IF EXISTS update_balances;
DELIMITER $$
CREATE TRIGGER update_balances
BEFORE UPDATE
ON customer
FOR EACH ROW IF new.balance <> old.balance THEN -- update from the old code
SET
new.balance_cents = CEIL(new.balance * 100);
ELSEIF new.balance_cents <> old.balance_cents THEN -- update from new code
SET
new.balance = new.balance_cents / 100;
END IF;
$$
DELIMITER ;
现在让我们来测试一下。
根据旧代码进行更新:
UPDATE customer
SET
balance = -1.45
WHERE
email = 'account_from_new_code@example.com';
SELECT
email,
balance,
balance_cents
FROM
customer
WHERE
email = 'account_from_new_code@example.com';
+-----------------------------------+---------+---------------+
| email | balance | balance_cents |
+-----------------------------------+---------+---------------+
| account_from_new_code@example.com | -1.45 | -145 |
+-----------------------------------+---------+---------------+
根据新代码进行更新:
UPDATE customer
SET
balance_cents = 567
WHERE
email = 'account_from_new_code@example.com';
SELECT
email,
balance,
balance_cents
FROM
customer
WHERE
email = 'account_from_new_code@example.com';
+-----------------------------------+---------+---------------+
| email | balance | balance_cents |
+-----------------------------------+---------+---------------+
| account_from_new_code@example.com | 5.67 | 567 |
+-----------------------------------+---------+---------------+
我们的触发器按预期工作。现在,我们需要用“balance”字段中的数据填充空的“balance_cents”字段。
填充空的“balance_cents”字段
填充空“balance_cents”字段的最简单方法是使用UPDATE语句:
UPDATE customer
SET
balance_cents = CEIL(balance * 100);
但更新查询会给数据库带来很大压力。由于我们的主要目标是避免停机,因此更新过程应该小批量执行。
可以在应用程序内创建迁移脚本,但在我们使用SQL时,让我们创建一个存储过程。
DROP PROCEDURE IF EXISTS batch_update_balance_cents;
DELIMITER $$
CREATE PROCEDURE batch_update_balance_cents(
start_id INT,
end_id INT,
batch_size INT)
BEGIN
DECLARE batch_start INT DEFAULT start_id;
DECLARE batch_end INT DEFAULT start_id + batch_size;
IF end_id < start_id + batch_size THEN
SET end_id = start_id + batch_size;
END IF;
WHILE batch_end <= end_id
DO
UPDATE customer
SET
balance_cents = CEIL(balance * 100)
WHERE
id BETWEEN batch_start AND batch_end;
SET batch_start = batch_start + batch_size;
SET batch_end = batch_end + batch_size;
END WHILE;
END$$
DELIMITER ;
CALL batch_update_balance_cents(1, (SELECT
MAX(id)
FROM
customer), 1000);
现在让我们检查一下结果:
SELECT *
FROM
customer;
+----+-----------------------------------+---------+---------------+
| id | email | balance | balance_cents |
+----+-----------------------------------+---------+---------------+
| 1 | alice@example.com | 42 | 4200 |
| 2 | bob@example.com | -42 | -4200 |
| 3 | account_from_old_code@example.com | -1.23 | -123 |
| 4 | account_from_new_code@example.com | 5.68 | 568 |
+----+-----------------------------------+---------+---------------+
所有旧条目都已更新。
删除触发器和存储过程
迁移已完成。每个人都对我们的最新变化感到高兴。将向所有客户部署新的应用程序代码。
DROP PROCEDURE IF EXISTS batch_update_balance_cents;
DROP TRIGGER IF EXISTS update_balances;
DROP TRIGGER IF EXISTS insert_balances;
删除旧字段
现在,没有人使用旧字段,删除它。
ALTER TABLE customer
DROP COLUMN balance;
结论
在本文中,我们展示了如何在不停机的情况下从一个字段迁移到另一个字段。
我们使用触发器和存储过程来保持数据同步。
我在balance字段中使用了一个特定的示例,但也可以对任何其他字段或字段集使用相同的方法。
原文标题:Non-blocking Database Migrations
原文作者:Aleksei Kankov
原文链接:https://dzone.com/articles/non-blocking-database-migrations




