84

我刚刚阅读了一篇关于微服务和 PaaS 架构的文章。在那篇文章中,大约下降了三分之一,作者指出(在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

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

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

4

4 回答 4

36

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

  • 在应用层,微服务被分解为语义功能。
    • 例如,Contact服务可能会 CRUD 联系人(关于联系人的元数据:姓名、电话号码、联系信息等)
    • 例如,User服务可能会使用登录凭据、授权角色等对用户进行 CRUD。
    • 例如,一项Payment服务可能会使用 CRUD 付款并与第三方 PCI 兼容服务(如 Stripe 等)一起工作。
  • 在 DB 层,可以组织表格,但是开发人员/数据库/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 毫秒,那么我毫不怀疑这些额外的网络调用可以精心设计,不会对应用程序造成延迟。

于 2015-03-11T00:45:57.393 回答
21

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


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

接口加入

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

在此处输入图像描述 它有一个明显的缺点。它更慢。

只读视图

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

高性能读取

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


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

于 2017-11-14T14:12:03.710 回答
5

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

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

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

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

我见过大系统被分成几组表。在这些情况下,可能存在 A)组之间不允许 FK 或 B)一个特殊组,该组包含 FK 可以将 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 加入”的性能相对较差,这是否令人惊讶?

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

于 2017-02-26T10:16:52.940 回答
0

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

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

@ccit-spence,我喜欢交叉点服务的方法,但是其他服务如何设计和使用它呢?我相信它会对其他服务产生一种依赖。

请问有什么意见吗?

于 2016-03-27T22:40:33.637 回答