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


77

我刚刚读了一篇有关微服务和PaaS体系结构的文章。在那篇文章中,作者大约说了三分之一,(在Denormalize像Crazy一样):

重构数据库架构,并对所有内容进行规范化,以允许数据的完全分离和分区。也就是说,请勿使用为多个微服务服务的基础表。不应共享跨越多个微服务的基础表,也不应共享数据。相反,如果多个服务需要访问相同的数据,则应通过服务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

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

  1. UserService-用于在系统中添加用户;最终应该管理[users]表;和
  2. ProductService-对系统中的产品进行填充;最终应该管理[products]表;和
  3. OrderService-用于在系统中添加订单;最终应该管理[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“像疯了一样变态”。。。为什么?我在文章中没有看到任何具体的理由。
Mike Sherrill'Cat Recall'14

21
您是否已解决此问题?似乎是任何推动微服务的人都可以避免的问题之一。
代码

嗨,@ ccit-spence-请参阅我的回答,让我知道您的想法。我必须亲自设计该解决方案,并且该解决方案已经运行了好几个月,但对其他开发人员的想法很感兴趣。
smeeb 2015年

1
也许值得注意的是,本文所指的是一个甚至不支持外键约束的数据库(因此对我而言,这表明作者没有重视外键约束的价值-甚至不知道丢失了什么? )。
罗布·比格雷夫

Answers:


35

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

  • 在应用程序层,微服务被分解为语义功能。
    • 例如 Contact服务可能会CRUD联系人(有关联系人的元数据:姓名,电话号码,联系信息等)
    • 例如 User服务可能会使用登录凭据,授权角色等来欺骗用户。
    • 例如,一项Payment服务可能会CRUD付款,并且可以与诸如Stripe等第三方PCI兼容服务一起在后台运行。
  • 在数据库层,可以对表进行组织,但是开发人员/数据库/开发人员需要对表进行组织

问题与级联和服务边界有关:付款可能需要用户知道谁在付款。不必像这样对服务建模:

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 ms,那么毫无疑问,可以设计这些额外的网络调用,以免引起应用程序延迟。


1
所有服务都绑定到同一数据库吗?在我们的情况下,每个服务都是其自己的服务器实例上的独立服务。每个服务都有一个专用于该服务的数据库。
代码

7
外键不会增加性能。索引是提高性能的关键。但是,可以在任何模式中创建类似FK的列上的索引,不一定相同。例如:Orders表可以生活在自己的模式中,并且具有索引user_id列,这不是“ true” FK,而只是从Users微服务获得的用户的ID ,而users表生活在其自己的模式中。几乎没有性能损失,但是我仍然不明白如何实现某些过滤/分批处理。例如:找到具有顺序具有产品具有价格> 100的所有用户
鲁斯兰Stelmachenko

1
但是我真正想说的是:如果您已经在使用这样的微服务,则不需要将表放在具有“真实” FK的单个DB中。他们每个人都可以住在自己的数据库中。他们只应该在“假” FK列上有索引。由于微服务,您已经不能使用JOIN,因此,如果将数据库拆分为较小的数据库,则不会丢失任何内容。
Ruslan Stelmachenko

1
但是,如果我创建一个不存在的FK的实体,例如,引用不存在的客户的订单,该怎么办?如果我想要保持一致性,就必须参照其他微服务执行一些检查,是吗?
cecemel '16

2
“由于微服务,您已经不能使用JOIN ...”……我认为这类似于说我们正在离开数据库查询计划器(基于成本的优化器)。也就是说,破入大量的小数据库的方式,我们失去了成本优化器基础的好处,现在实行“连接”通过REST / RPC等
罗布Bygrave

18

实际上,这确实是微服务中的关键问题之一,在大多数文章中都已很容易地将其忽略了。Fortunatelly有解决方案。作为讨论的基础,让我们准备问题中提供的表格。 在此处输入图片说明 上图显示了表格在整体中的外观。仅有几张带有联接的表。


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

Api加入

在此策略中,微服务之间的外键被破坏,微服务公开了模仿该键的端点。例如:产品微服务将公开findProductById端点。订单微服务可以使用此端点而不是联接。

在此处输入图片说明 它有明显的缺点。慢一点

只读视图

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

高性能读取

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


解决方案很少。实施最简单的系统性能最低。高性能解决方案将需要数周的时间来实施。


难道这不使读者与他们正在阅读的视图的架构耦合吗?关于微服务的每一篇文章都说他们应该拥有自己的数据存储区,保持数据的私密性……
Steve Chamaillard

是的,从某种程度上说,这会使读者与制作人联系起来,从好的方面来说,读者只能阅读部分事件,而不关心完整的信息。实际上,在几乎每个大型应用程序中,您都将需要微服务之间的某种共享状态。就像在示例中一样。订单具有产品和用户。没有共享信息很难重新设计此案
Marcin Szymczak

5

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

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

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

也就是说,使用严格的无共享数据库规则推动微服务的人们正在要求数据库设计者放弃外键(他们正在隐式或显式地执行此操作)。当他们没有明确声明FK的丢失时,这使您想知道他们是否真正知道并认识了外键的值(因为常常根本没有提到外键)。

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

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

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

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

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

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

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

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

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

实际案例:“通过REST联接”与DB联接的示例成本

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

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

与具有基于成本的优化器等的DB相比,“通过REST联接”的性能相对较差是否令人惊讶?

在某种程度上,当我们像这样分解数据库时,我们也摆脱了“基于成本的优化器”,而所有与查询执行计划相关的工作都转向了编写我们自己的联接逻辑(我们在编写相对而言是相对的)不复杂的查询执行计划)。


0

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

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

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

有什么意见吗?


1
@ user1294787你是对的,存在耦合的可能性。最终,一个完全解耦的系统将无济于事。实际上,正在聚合的服务不了解正在聚合它们的服务。实际上,您可能有许多服务可出于不同目的提供聚合。如果不再需要正在聚合的服务,那么也将不再需要聚合服务本身。
代码
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.