在单体到微服务迁移中如何重构关系数据库?


本文介绍将现有单体应用程序迁移到微服务中,如何重构数据库?

数据库重构模式
可以通过多种方式重构关系数据库(例如 PostgreSQL)以优化基于微服务的应用程序架构的效率。如前所述,数据库是结构化数据的有组织集合。因此,大多数重构模式重组数据以优化组织的功能需求,例如数据库访问效率、性能、弹性、安全性、合规性和可管理性。

1、单一库表
单表是最基本的数据库表之一。有一个包含单个数据库的数据库实例。该数据库有一个包含所有表和其他数据库对象的单一表。

随着组织开始从单一架构转向微服务架构,他们通常会保留其单一数据库架构一段时间。

使用单一表结构来容纳所有表,尤其是 public 表通常被认为是糟糕的数据库设计。随着数据库变得越来越复杂,在单个表中创建、组织、管理和保护数十个、数百个或数千个数据库对象(包括表)变得不可能。


2、多个库表
将表和其他数据库对象分离到多个库表中是重构数据库以支持微服务的绝佳第一步。随着应用程序的复杂性和数据库随着时间的推移自然增长,按业务子域或团队分离功能的库表将显着受益。

根据 PostgreSQL 文档,人们可能想要使用表结构的原因有多种:

  • 允许多个用户使用一个数据库而不会互相干扰。
  • 将数据库对象组织成逻辑组,使它们更易于管理。
  • 第三方应用程序可以放入单独的schema中,因此它们不会与其他对象的名称冲突。

库表结构类似于操作系统级别的目录,只是表结构不能嵌套。

随着组织继续将其整体应用程序架构分解为基于微服务的应用程序,它可以过渡到每个微服务表或类似级别或组织粒度。

应用领域驱动设计原则
领域驱动设计 (DDD) 是“一种软件设计方法,专注于根据领域专家的输入对软件进行建模以匹配该领域”。

架构师经常应用 DDD 原则将单体应用程序分解为微服务。例如,一个微服务或一组相关的微服务可能代表一个限界上下文。在 DDD 中,限界上下文 是“对边界的描述,通常是子系统或特定团队的工作,其中定义并适用特定模型。” 。

限界上下文的示例可能包括销售、运输和支持。

重构数据库时应用模式的一种技术是镜像限界上下文,它反映了微服务。对于每个微服务或一组密切相关的微服务,都有一个表。不幸的是,没有绝对的方法来定义域的限界上下文,以及以后的数据库表。它取决于许多因素,包括您的应用程序架构、功能、安全要求,通常还包括组织的职能团队结构。

跨多表拆分表数据
当重构一个数据库时,你可能不得不通过在多个表结构中复制表定义来分割数据。

以Pagila的地址表为例,它包含客户、员工和商店的地址。customers.customer, stores.staff, and stores.store都与common.address表有外键关系。地址表与城市和国家表都有外键关系。
因此,为了方便起见,在上面的例子中,地址表、城市表和国家表都被放置在公共表中。

尽管一开始,将所有的地址存储在一个表中可能看起来是合理的数据库规范化,但考虑到地址表的数据暴露的风险。商店地址不被认为是敏感数据。然而,客户和员工的家庭地址可能被认为是敏感的个人身份信息(PII)。

另外,考虑到随着应用程序的发展,你可能会有对一种地址类型独有的字段,不适用于其他类别的地址。一个商店的地址的表定义可能与客户的地址定义不同。例如,我们可能会选择在customers.address表中添加一个县域列,用于电子商务税收,或者在store.address表中添加一个on_site_parking的布尔列。

在下面的例子中,添加了一个新的员工表。地址表的定义被复制到客户、员工和商店表结构中。假设原始表中的混合地址数据被分配到适当的地址表中。注意表帮助我们避免表名冲突的方式。

-----------+----------+-----------+---------------
 Instance  | Database | Schema    | Table
-----------+----------+-----------+---------------
 postgres1 | pagila   | common    | city
 postgres1 | pagila   | common    | country
-----------+----------+-----------+---------------
 postgres1 | pagila   | customers | address
 postgres1 | pagila   | customers | customer
-----------+----------+-----------+---------------
 postgres1 | pagila   | films     | actor
 postgres1 | pagila   | films     | category
 postgres1 | pagila   | films     | film
 postgres1 | pagila   | films     | film_actor
 postgres1 | pagila   | films     | film_category
 postgres1 | pagila   | films     | language
-----------+----------+-----------+---------------
 postgres1 | pagila   | sales     | payment
 postgres1 | pagila   | sales     | rental
-----------+----------+-----------+---------------
 postgres1 | pagila   | staff     | address
 postgres1 | pagila   | staff     | staff
-----------+----------+-----------+---------------
 postgres1 | pagila   | stores    | address
 postgres1 | pagila   | stores    | inventory
 postgres1 | pagila   | stores    | store


要创建新customers.address 表,我们可以使用以下 SQL 语句。创建其他两个 address 表的语句几乎相同。

— wrap in transaction
BEGIN;

— create new customers.address table
CREATE SEQUENCE IF NOT EXISTS customers.address_address_id_seq
    INCREMENT 1
    START 1
    MINVALUE 1
    MAXVALUE 9223372036854775807
    CACHE 1;

ALTER SEQUENCE customers.address_address_id_seq
    OWNER TO pagila_admin;

CREATE TABLE IF NOT EXISTS customers.address (
    address_id integer DEFAULT nextval('address_address_id_seq'::regclass) NOT NULL PRIMARY KEY,
    address text NOT NULL,
    address2 text,
    district text NOT NULL,
    city_id smallint NOT NULL REFERENCES common.city ON UPDATE CASCADE ON DELETE RESTRICT,
    postal_code text,
    phone text NOT NULL,
    last_update timestamp with time zone DEFAULT now() NOT NULL
);

ALTER TABLE customers.address
    OWNER TO pagila_admin;

CREATE INDEX IF NOT EXISTS idx_fk_city_id ON customers.address(city_id);

CREATE TRIGGER last_updated
    BEFORE UPDATE ON customers.address FOR EACH ROW
    EXECUTE PROCEDURE last_updated();

COMMIT;


虽然我们现在有两个具有相同表定义的附加表,但我们不会复制任何数据。我们可以使用以下 SQL 语句将唯一地址数据迁移到适当的表中并确认结果。

— wrap in transaction
BEGIN;

— copy only customer addresses to new customers.address table
INSERT INTO customers.address
SELECT *
FROM common.address
WHERE common.address.address_id IN (
        SELECT DISTINCT address_id
        FROM customers.customer
    );

— copy only staff addresses to new staff.address table
INSERT INTO staff.address
SELECT COUNT(*)
FROM common.address
WHERE common.address.address_id IN (
        SELECT DISTINCT address_id
        FROM staff.staff
    );

— copy only store addresses to new stores.address table
INSERT INTO stores.address
SELECT *
FROM common.address
WHERE common.address.address_id IN (
        SELECT DISTINCT address_id
        FROM stores.store
    );

— check for extraneous data in common.address before deleting
SELECT *
FROM common.address
WHERE common.address.address_id NOT IN
        (SELECT DISTINCT address_id FROM customers.customer)
    AND common.address.address_id NOT IN
        (SELECT DISTINCT address_id FROM staff.staff)
    AND common.address.address_id NOT IN
        (SELECT DISTINCT address_id FROM stores.store);
COMMIT;

最后,更改现有的外键约束以指向新 address 表。其他两个 address 表的 SQL 语句几乎相同。

— wrap in transaction
BEGIN;

— customers.customer
ALTER TABLE IF EXISTS customers.customer 
    DROP CONSTRAINT IF EXISTS customer_address_id_fkey;

ALTER TABLE IF EXISTS customers.customer
    ADD CONSTRAINT customer_address_id_fkey FOREIGN KEY (address_id)
    REFERENCES customers.address (address_id) MATCH SIMPLE
    ON UPDATE CASCADE
    ON DELETE RESTRICT;

COMMIT;

现在,查询商店地址时暴露敏感客户或员工数据的风险降低了,并且这三个 address 实体可以独立发展。分别负责customers、 staff和 的 各个职能团队stores可以仅拥有和管理其领域内的数据。

3、多个数据库
使用 DDD 概念,虽然表结构可能代表限界上下文,但数据库最接近 领域,这是“应用程序逻辑所涉及的知识和活动领域”

随着组织继续完善其基于微服务的应用程序架构,它可能会发现同一数据库实例中的多个数据库有利于进一步分离和组织应用程序数据。

假设 films 架构中的数据由一个完全独立的团队拥有和管理,该团队永远不应访问存储在customers、 stores和 sales 架构中的敏感数据。根据 PostgreSQL 文档,数据库访问权限是使用角色的概念来管理的。根据角色的设置方式,可以将角色视为一个数据库用户或一组用户。

为了提供比表格更大的关注点分离,我们可以在同一个RDS数据库实例中为与电影有关的数据创建第二个完全独立的数据库。

有了两个独立的数据库,就更容易创建和管理不同的角色,并确保对客户、商店或销售数据的访问只被需要访问的团队所访问。

# dump v2 of pagila database
pg_dump -Fc -d pagila_v2 -f pagila_v2.dump

# create 2 new v3 databases
export PGDATABASE="postgres"
psql << EOF
\x
CREATE DATABASE pagila_v3;
CREATE DATABASE products_v3;
EOF

# restore v2 of pagila database
pg_restore -d pagila_v3 pagila_v2.dump
pg_restore -d products_v3 -n films pagila_v2.dump

# connect to new pagila database
export PGDATABASE="pagila_v3"
psql

更改数据捕获和发件箱模式
可以使用多种方法在两个数据库之间复制电影数据的插入、更新和删除,包括使用发件箱 模式的更改数据捕获 (CDC) 。

CDC 是“一种使数据库更改能够被监视并传播到下游系统的模式”(RedHat)。发件箱模式使用 PostgreSQL 数据库的能力,使用事务以原子方式执行对两个表的提交 。事务将多个步骤捆绑到一个单一的、全有或全无的操作中。


在此示例中,数据被写入架构中的现有表products.films (更新的聚合状态)以及新products.films.outbox 表(新域事件),并包装在 事务中。使用 CDC,表中的领域事件products.films.outbox 被复制到pagila.films.film 表中。使用 CDC 在两个数据库之间复制数据也称为 最终一致性。

用于 CDC 的 Debezium 和 Confluent
执行 CDC 有多种技术选择。对于这篇文章,我使用了 RedHat 的 Debezium connector for PostgreSQL 和 Debezium Outbox Event Router,以及 Confluent 的 JDBC Sink Connector。下面,我们看到了一个典型的例子,一个使用 Debezium 连接器的 Kafka Connect  Source Connector for PostgreSQL 和一个使用 Confluent JDBC Sink Connector 的 Sink Connector。源连接器 products 使用 PostgreSQL 的 预写日志记录 (WAL) 功能将更改从日志流式传输到 Apache Kafka 主题。相应的接收器连接器将更改从 Kafka 主题流式传输到数据库pagila 。


4、多个数据库实例
在基于微服务的应用程序发展的某个时刻,使用相同的数据库引擎将数据分离到多个数据库实例中可能会变得更有优势。虽然管理大量数据库实例可能需要更多资源,但也有优势。每个数据库实例都有独立的连接配置、角色和管理员。每个数据库实例都可以运行不同版本的数据库引擎,并且每个实例都可以独立升级和维护。

使用 CDC 进行数据复制


5、多个数据库引擎
AWS 通常使用术语“专用数据库”。AWS 提供超过 15 个专门构建的数据库引擎来支持不同的数据模型,包括关系、键值、文档、内存、图形、时间序列、宽列和分类帐。在某些情况下,使用多个专门构建的数据库可能是有意义的。使用不同的数据库引擎允许架构师利用每种引擎类型的独特特性来支持不同的应用程序需求。


CQRS
命令查询责任分离 (CQRS) 是一种流行的软件架构模式,是多个数据库引擎的另一个用例。顾名思义,CQRS 模式是“一种将命令活动与查询活动分开的软件设计模式。用 CQRS 的说法,命令将数据写入数据源。查询从数据源读取数据。当以 Web 规模运行的应用程序对物理数据库及其所在的网络造成过多负担时,CQRS 解决了数据访问性能下降的问题”(RedHat)。CQRS 通常使用一个针对写入优化的数据库引擎和一个针对读取优化的单独数据库。

结论
采用基于微服务的应用程序架构可能会给组织带来许多业务优势。然而,忽略应用程序的现有数据库可能会抵消微服务的许多好处。这篇文章研究了重构关系数据库以匹配现代基于微服务的应用程序架构的几种常见模式。