【问题标题】:How does data denormalization work with the Microservice Pattern?数据非规范化如何与微服务模式一起工作?
【发布时间】:2015-01-16 09:51:48
【问题描述】:

我刚刚在Microservices and PaaS Architecture 上阅读了一篇文章。在那篇文章中,大约下降了三分之一,作者指出(在 Denormalize like Crazy 下):

重构数据库模式,并对所有内容进行反规范化,以实现数据的完全分离和分区。也就是说,不要使用服务于多个微服务的底层表。不应该共享跨多个微服务的基础表,也不应该共享数据。相反,如果多个服务需要访问相同的数据,则应通过服务 API(例如已发布的 REST 或消息服务接口)共享这些数据。

虽然这在理论上听起来很棒,但在实践中它有一些严重的障碍需要克服。其中最大的原因是,数据库通常是紧密耦合的,每个表都与至少一个其他表有一些外键关系。因此,不可能将数据库划分为 n 个由 n 个微服务控制的子数据库。

所以我问:给定一个完全由相关表组成的数据库,如何将其非规范化为更小的片段(表组),以便这些片段可以由单独的微服务控制?

例如,给定以下(相当小但示例性的)数据库:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime
user_id

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
product_id
order_id
quantity_ordered

不要花太多时间来批评我的设计,我是即时完成的。关键是,对我来说,将这个数据库分成 3 个微服务是合乎逻辑的:

  1. UserService - 用于系统中的 CRUDding 用户;应该最终管理[users] 表;和
  2. ProductService - 用于系统中的 CRUDding 产品;应该最终管理[products] 表;和
  3. OrderService - 用于系统中的 CRUDding 订单;最终应该管理[orders][products_x_orders]

但是,所有这些表之间都有外键关系。如果我们对它们进行非规范化并将它们视为单体,它们就会失去所有的语义意义:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
quantity_ordered

现在无法知道谁订购了什么、数量多少或何时订购。

那么这篇文章是典型的学术喧嚣,还是这种非规范化方法在现实世界中有实用性,如果有,它是什么样子(在答案中使用我的示例的奖励积分)?

【问题讨论】:

  • WRT "疯狂地去规范化" 。 . .为什么?我在文章中没有看到任何具体的理由。
  • 您是否正在解决这个问题?似乎是任何推动微服务的人最容易避免的问题之一。
  • 你好@ccit-spence - 请看我的回答,让我知道你的想法。我必须自己设计这个解决方案,它已经运行了好几个月,但对其他开发人员的想法很感兴趣。
  • 也许值得注意的是,这篇文章引用了一个甚至不支持外键约束的数据库(所以对我来说,这表明作者没有重视外键约束——也许没有'甚至不知道丢失了什么?)。

标签: database denormalization microservices


【解决方案1】:

这确实是微服务中的关键问题之一,在大多数文章中都非常方便地省略了。幸运的是,有解决方案。作为讨论的基础,让我们有您在问题中提供的表格。 上图显示了表在单体应用中的外观。只有几个带有连接的表。


要将其重构为微服务,我们可以使用一些策略:

API 加入

在这个策略中,微服务之间的外键被破坏,微服务暴露了一个模仿这个键的端点。例如:产品微服务将暴露findProductById端点。订单微服务可以使用这个端点代替join。

它有一个明显的缺点。比较慢。

只读视图

在第二个解决方案中,您可以在第二个数据库中创建表的副本。副本是只读的。每个微服务都可以在其读/写表上使用可变操作。当涉及从其他数据库复制的只读表时,他们可以(显然)使用只读表

高性能读取

read only view解决方案之上引入redis/memcached等解决方案,可以实现高性能读取。应将连接的两侧复制到优化阅读的平面结构。您可以引入全新的无状态微服务,可用于从该存储中读取数据。虽然看起来很麻烦,但值得注意的是,它比基于关系数据库的单体解决方案具有更高的性能。


可能的解决方案很少。实现最简单的那些性能最低。高性能解决方案需要几周时间才能实施。

【讨论】:

  • 这不是将读者与他们正在阅读的视图的模式联系起来吗?每一篇关于微服务的文章都说他们应该有自己的数据存储,保持数据的私密性......
  • 是的,这在某种程度上将读者与制片人结合在一起,从好的方面来说,读者只能阅读部分事件,而不关心整个信息。实际上,在几乎每个大型应用程序中,您都需要在微服务之间共享一些状态。就像在示例中一样。订单有产品和用户。如果没有共享信息,很难重新设计这个案例
【解决方案2】:

我意识到这可能不是一个好的答案,但到底是什么。你的问题是:

给定一个完全由相关表组成的数据库,如何 将其非规范化为更小的片段(表组)

WRT 数据库设计我会说“你不能不删除外键”

也就是说,使用严格的不共享数据库规则推动微服务的人们要求数据库设计者放弃外键(他们隐式或显式地这样做)。当他们没有明确说明 FK 的丢失时,您会怀疑他们是否真的知道并识别外键的值(因为它经常根本没有被提及)。

我已经看到大型系统被分成几组表。在这些情况下,可能存在 A) 组之间不允许 FK 或 B) 一个特殊组包含“核心”表,这些表可以被 FK 引用到其他组中的表。

...但在这些系统中,“表组”通常是 50 多个表,因此不足以严格遵守微服务。

对我来说,使用微服务方法拆分数据库的另一个相关问题是它对报告的影响,即如何将所有数据汇总在一起以进行报告和/或加载到数据仓库中的问题。

有些相关的还有忽略内置 DB 复制功能而倾向于消息传递(以及核心表/DDD 共享内核的基于 DB 的复制如何)影响设计的趋势。

编辑:(通过 REST 调用加入的成本)

当我们按照微服务的建议拆分数据库并删除 FK 时,我们不仅失去了(FK 的)强制声明性业务规则,而且我们也失去了数据库跨这些边界执行连接的能力。

在 OLTP 中,FK 值通常不是“UX 友好”的,我们经常希望加入它们。

在示例中,如果我们获取最后 100 个订单,我们可能不想在 UX 中显示客户 ID 值。相反,我们需要再次致电客户以获取他们的姓名。但是,如果我们还想要订单行,我们还需要再次调用产品服务以显示产品名称、sku 等而不是产品 ID。

通常我们会发现,当我们以这种方式分解数据库设计时,我们需要执行大量“通过 REST 连接”调用。那么这样做的相对成本是多少?

实际案例:“通过 REST 连接”与数据库连接的示例成本

有 4 个微服务,它们涉及很多“通过 REST 连接”。这 4 项服务的基准负载为 ~15 分钟。这 4 个微服务转换为具有 4 个模块的 1 个服务,针对共享数据库(允许连接)在 ~20 秒内执行相同的负载。

不幸的是,这并不是数据库连接与“通过 REST 连接”的直接比较,因为在这种情况下,我们也从 NoSQL DB 更改为 Postgres。

与具有基于成本的优化器等的数据库相比,“通过 REST 加入”的性能相对较差,这是否令人惊讶?

在某种程度上,当我们像这样分解数据库时,我们也在远离“基于成本的优化器”以及与查询执行计划相关的所有事情,转而编写我们自己的连接逻辑(我们有点写我们自己相对简单的查询执行计划)。

【讨论】:

    【解决方案3】:

    这是主观的,但以下解决方案对我、我的团队和我们的数据库团队都有效。

    • 在应用层,微服务被分解为语义功能。
      • 例如Contact 服务可能会 CRUD 联系人(有关联系人的元数据:姓名、电话号码、联系信息等)
      • 例如User 服务可能会使用登录凭据、授权角色等对用户进行 CRUD。
      • 例如Payment 服务可能会使用 CRUD 付款并在后台与第三方 PCI 兼容服务(如 Stripe 等)一起工作。
    • 在 DB 层,可以按照 devs/DBs/devops 人员的要求组织表格

    问题在于级联和服务边界:付款可能需要用户知道谁在付款。而不是像这样对您的服务进行建模:

    interface PaymentService {
        PaymentInfo makePayment(User user, Payment payment);
    }
    

    像这样建模:

    interface PaymentService {
        PaymentInfo makePayment(Long userId, Payment payment);
    }
    

    这样,属于其他微服务的实体仅通过 ID 在特定服务内引用,而不是通过对象引用。这允许数据库表在所有地方都有外键,但在应用层,“外来”实体(即生活在其他服务中的实体)可通过 ID 获得。这可以防止对象级联失控并清晰地描绘服务边界。

    它确实引起的问题是它需要更多的网络调用。例如,如果我给每个Payment 实体一个User 引用,我可以通过一次调用让用户获得特定的付款:

    User user = paymentService.getUserForPayment(payment);
    

    但是使用我在这里的建议,您需要两个调用:

    Long userId = paymentService.getPayment(payment).getUserId();
    User user = userService.getUserById(userId);
    

    这可能会破坏交易。但是,如果您很聪明并实施了缓存,并实施了精心设计的微服务,每次调用的响应时间为 50 到 100 毫秒,那么我毫不怀疑这些额外的网络调用可以精心设计,以不会对应用。

    【讨论】:

    • 所有服务是否都绑定到同一个数据库?在我们的例子中,每个服务都是其自己的服务器实例上的独立服务。每个服务都有该服务的专用数据库。
    • 外键不会增加性能。索引是增加性能的东西。但是类 FK 列上的索引可以在任何模式中创建,不一定相同。例如:Orders 表可以存在于他们自己的模式中,并且已经索引了user_id 列,这不是“真正的”FK,而只是从Users 微服务获得的用户 ID,而users 表存在于它的自己的架构。几乎没有性能损失,但我仍然无法理解如何实现一些过滤/批处理。例如:查找所有有订单且产品价格> 100的用户。
    • 但我真正想说的是:如果您已经在使用这样的微服务,则不需要将表放在具有“真实”FK 的单个数据库中。他们每个人都可以住在自己的数据库中。他们只应该在“假”FK 列上有索引。由于微服务,您已经不能使用 JOIN,因此如果将 DB 拆分为更小的 DB,您不会丢失任何东西。
    • 但是如果我创建一个具有不存在的 FK 的实体,例如参考不存在的客户的订单。如果我想要一些一致性,我将不得不通过引用其他微服务来执行一些检查,不是吗?
    • “你已经不能使用 JOIN,因为微服务......”......我认为这类似于说我们正在远离数据库查询计划器(基于成本的优化器)。也就是说,闯入大量小型数据库意味着我们失去了基于成本的优化器的好处,现在通过 rest / rpc 等实现“JOINS”。
    【解决方案4】:

    我会将每个微服务视为一个对象,并且与任何 ORM 一样,您使用这些对象来提取数据,然后在您的代码和查询集合中创建连接,微服务应该以类似的方式处理。唯一的区别在于每个微服务一次代表一个对象,而不是一个完整的对象树。 API 层应该使用这些服务并以必须呈现或存储的方式对数据进行建模。

    为每个事务多次调用服务不会产生影响,因为每个服务都在单独的容器中运行,并且所有这些调用都可以并行执行。

    @ccit-spence,我喜欢交集服务的方法,但是如何设计和使用其他服务呢?我相信它会为其他服务创建一种依赖关系。

    请问有cmets吗?

    【讨论】:

    • @user1294787 你说得对,存在耦合的可能性。一个完全解耦的系统最终将无能为力。正在聚合的服务实际上不知道正在聚合它们的服务。事实上,您可以有许多服务为不同的目的提供聚合。如果不再需要聚合的服务,那么聚合服务本身也将不再需要。
    猜你喜欢
    • 1970-01-01
    • 2016-05-27
    • 2013-12-11
    • 2016-12-14
    • 2022-07-06
    • 2016-07-23
    • 2018-06-18
    • 2019-08-29
    • 2020-07-09
    相关资源
    最近更新 更多