暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

无阻塞数据库迁移

原创 eternity 2022-09-26
404

数据库迁移是任何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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论