作者 | Kiran Ra       译者 | 弯月

出品 | CSDN(ID:CSDNnews)

作为移动应用公司的一名后端开发人员,我最常遇到的一项任务就是迁移数据表结构。有时是为了提高查询性能,而有时则是更改列名/类型,或者是让数据适应新的用例。虽然任务本身看似只是一组简单的 SQL 命令,但想实现零停机还是相当不容易的。

具体步骤如下:

1. 创建新的空表;

2. 同时写入新旧表;

3. 将数据(以块的形式)从旧表复制到新表;

4. 验证一致性;

5. 将读取切换到新表;

6. 停止写入旧表;

7. 清理旧表。

在这篇指南中,我们来看看迁移PostgreSQL数据表的全过程。虽然这些示例介绍的是PostgreSQL的表迁移,但文中介绍的步骤同样适用于其他类型的数据迁移。

81fc3afe7b049d3a8ca3fa36050ac678.png

1、背景介绍

现有的表结构 

假设我们有如下表结构式,其中包含一个名为 old 的表,还有一个包含该表CRUD(Create、Read、Update和Delete,即创建、读取、更新和删除)操作的API。

5c0f200b8174f8342c5afedf6cb613e5.png

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
 
CREATE TABLE IF NOT EXISTS old (
    old_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
    data TEXT NOT NULL
);
 
-- Create
INSERT INTO old (data)
VALUES (?)
RETURNING *;
 
-- Read
SELECT *
FROM old
WHERE old_id = ?;
 
-- Update
UPDATE old
SET data = ?
WHERE old_id = ?;
 
-- Delete
DELETE
FROM old
WHERE old_id = ?;

新的表结构 

列data的类型原本为TEXT,是为了方便操作,但现在该列只用于保存时间戳。假设,现在我们收到了一个请求,要求统计两个时间戳之间的所有数据项。尽管用旧的表结构也能实现,但我们认为,将data的类型改为TIMESTAMP,可以更好地实现需求。同时,表名old也不是一个准确的名称,我们决定改为new。

我们想要的表结构如下:

4d4da2aa83302c481cf5fbb89b9d3689.png

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
 
CREATE TABLE IF NOT EXISTS new (
    new_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
    created_date TIMESTAMP WITH TIME ZONE NOT NULL
);

迁移要求

此次数据迁移还有更进一步地要求:

● 在整个迁移过程中,系统必须能够正常响应请求。

● 任何操作都不能锁定大量数据(写锁定)。

● 不能进行不安全的操作;

● 如果遇到问题,必须能够撤销更改,回滚到前一个版本。

2、具体步骤

创建一个新的空表

CREATE TABLE IF NOT EXISTS new (
    new_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
    created_date TIMESTAMP WITH TIME ZONE NOT NULL
);

同时写入新旧表

现在,我们有两个表,我们可以同时写入两个表。虽然此时旧表的数据才是真实的来源,但这里我们采用最终一致性。

840bbae89a321e7be490df7c349c2826.png

-- Create
WITH new_rows AS (
    INSERT INTO new (created_date)
    VALUES (?)
    RETURNING *
)
INSERT
INTO old (old_id, data)
SELECT new_id, CAST(created_date AS TEXT)
FROM new_rows
RETURNING *;
 
-- Update
UPDATE old
SET data = ?
WHERE old_id = ?;
 
UPDATE new
SET created_date = ?
WHERE new_id = ?;
 
 
-- Delete
DELETE
FROM old
WHERE old_id = ?;
 
DELETE
FROM new
WHERE new_id = ?;

请注意,我们的操作看起来比之前更为复杂,因为我们需要在新表中创建一行数据,然后再插入到旧表中。为了确保随机生成的 UUID 是一致的,这两个操作在一个事务中完成。

将数据复制到新表 

在处理好新记录后,下一步我们需要复制旧记录。

4073aec8b9caa0de870b5a95f07dadf3.png

INSERT INTO new(new_id, created_date)
SELECT old_id, CAST(data AS TIMESTAMP)
FROM OLD
WHERE NOT EXISTS(SELECT *
                 FROM new
                 WHERE new_id = OLD.old_id)
LIMIT 1000
RETURNING *;

我们将新表中不存在的数据,从旧表复制到新表。为了保持数据库正常响应,我们通过limit 1000,分批完成这项操作。虽然块的大小可以根据表进行调整,但我更喜欢小块,因为可以避免锁定大量数据。

验证一致性

这一步的工作经常被忽视。在切换读取之前,我们必须确保新旧表之间的数据完全同步。我们可以通过一些查询来验证新旧表之间的一致性。

检查项1:是否缺少任何记录?

SELECT *
FROM old
    FULL OUTER JOIN new ON old_id = new_id
WHERE new_id IS NULL
   OR old_id IS NULL

检查项2:数据是否一致?

SELECT *
FROM old
    INNER JOIN new ON old_id = new_id
WHERE CAST(data AS TIMESTAMP) <> created_date

切换读取

这一步的工作量往往最繁重。经常有几十处不同的代码会从表中读取。由于现在两个表之间的数据是同步的,因此我们可以花点时间慢慢地完成这部分迁移工作。

9ce1bb2079aaa7e4f9734da218a4454f.png

SELECT *
FROM new
WHERE new_id = ?;

在这个阶段,我们还需要更新视图、外键、触发器等,让它们都引用新表。

停止写入旧表

到这里,所有读取已切换到新表,我们不再需要更新旧数据库。

2be05d1cbd75aeef515f36a48cdd4088.png

-- Create


INSERT INTO new (created_date)
VALUES (?)
RETURNING *
 
-- Update
UPDATE new
SET created_date = ?
WHERE new_id = ?;
 
-- Delete
DELETE
FROM new
WHERE new_id = ?;

清理旧表

在确认系统不再引用旧表时,旧表就可以删除了。

df29a5f6985e6bd5defd4cf73c3cb6df.png

DROP TABLE IF EXISTS old;

3、完工!

迁移完工!

最困难的工作是向产品小组解释,为什么看似如此小的一个请求,花费的时间却是他们预想的三倍。希望在阅读本文之后,他们能够理解。

原文链接:https://kiranrao.ca/2022/05/04/zero-downtime-migrations.html

声明:本文为 CSDN 翻译,转载请注明来源。

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐