数据非规范化如何与微服务模式一起工作?

sme*_*eeb 71 database denormalization microservices

我刚读了一篇关于微服务和PaaS架构的文章.在那篇文章中,大约三分之一的时间,作者说(在像Denzy的Denormalize下):

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

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

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

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

[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
Run Code Online (Sandbox Code Playgroud)

不要花太多时间批评我的设计,我在飞行中做了这个.关键是,对我来说,将这个数据库分成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
Run Code Online (Sandbox Code Playgroud)

现在没有办法知道是谁订购了什么,数量或时间.

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

sme*_*eeb 29

这是主观的,但以下解决方案适用于我,我的团队和我们的数据库团队.

  • 在应用层,微服务被分解为语义功能.
    • 例如,Contact服务可能是CRUD联系人(有关联系人的元数据:姓名,电话号码,联系信息等)
    • 例如,User服务可能是具有登录凭据,授权角色等的CRUD用户.
    • 例如,Payment服务可能是CRUD付款,并在第三方PCI兼容服务(如Stripe等)下工作.
  • 在DB层,可以组织表,但是devs/DBs/devops人们希望组织表

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

interface PaymentService {
    PaymentInfo makePayment(User user, Payment payment);
}
Run Code Online (Sandbox Code Playgroud)

这样建模:

interface PaymentService {
    PaymentInfo makePayment(Long userId, Payment payment);
}
Run Code Online (Sandbox Code Playgroud)

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

它产生的问题是它需要更多的网络呼叫.例如,如果我给每个Payment实体一个User参考,我可以通过一次调用让用户获得特定付款:

User user = paymentService.getUserForPayment(payment);
Run Code Online (Sandbox Code Playgroud)

但是使用我在这里建议的内容,你需要两个电话:

Long userId = paymentService.getPayment(payment).getUserId();
User user = userService.getUserById(userId);
Run Code Online (Sandbox Code Playgroud)

这可能是一个交易破坏者.但是如果你聪明并实现缓存,并实现精心设计的微服务,每次调用响应50-100毫秒,我毫不怀疑这些额外的网络调用可以制作成不会给应用程序带来延迟.

  • 外键不会增加性能.索引是增加性能的原因.但是,类似FK的列的索引可以在任何模式中创建,不一定相同.例如:`Orders`表可以存在于他们自己的模式中,并且索引了`user_id`列,这不是"真正的"FK,而只是从'Users`微服务获得的用户的ID,而`users`表存在于其中自己的架构.几乎没有性能损失,但我仍然无法理解如何实现某些过滤/批处理.例如:查找订单中包含价格> 100的产品的所有用户. (6认同)
  • 但是,如果我使用不存在的 FK 创建一个实体,例如一个引用不存在的客户的订单,该怎么办。如果我想要一些一致性,我将不得不通过引用其他微服务来执行一些检查,不是吗? (2认同)
  • “由于微服务,您已经不能使用JOIN。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。也就是说,进入许多小型数据库意味着我们失去了基于成本的优化器的优势,现在通过rest / rpc等实现“ JOINS”。 (2认同)

Mar*_*zak 14

这确实是微服务中的关键问题之一,在大多数文章中都很容易省略.幸运的是有解决方案.作为讨论的基础,我们提供了您在问题中提供的表格. 在此输入图像描述 上图显示了表格在整体中的外观.只有几个表连接.


要将其重构为微服务,我们可以使用很少的策略:

Api加入

在这种策略中,微服务之间的外键被破坏,微服务暴露出模仿这个密钥的端点.例如:产品微服务将公开findProductById端点.订单微服务可以使用此端点而不是连接.

在此输入图像描述 它有一个明显的缺点.它比较慢.

只读视图

在第二个解决方案中,您可以在第二个数据库中创建表的副本.复制是只读的.每个微服务都可以在其读/写表上使用可变操作.当只读取从其他数据库复制的表时,它们(显然)只能使用读取 在此输入图像描述

高性能读取

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


几乎没有可能的解决方案.最简单的实现性能最低.高性能解决方案需要几周时间才能实施.


Rob*_*ave 5

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

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

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

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

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

...但在这些系统中,“表组”通常是 50 多个表,因此对于严格遵守微服务来说还不够小。

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

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

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

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

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

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

总的来说我们可以发现,当我们以这种方式分解DB设计时,我们需要做很多“JOIN via REST”调用。那么这样做的相对成本是多少?

实际故事:“通过 REST 加入”与数据库加入的示例成本

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

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

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

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