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

时间:2014-11-19 01:25:07

标签: database denormalization microservices

我刚读了一篇关于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]表格
  4. 但是,所有这些表都具有彼此的外键关系。如果我们对它们进行非规范化并将它们视为整体,它们将失去所有语义:

    [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 个答案:

答案 0 :(得分:28)

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

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

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

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

模仿它:

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

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

它产生的问题是它需要更多的网络呼叫。例如,如果我为每个Payment实体提供User引用,我可以通过一次通话让用户获得特定付款:

User user = paymentService.getUserForPayment(payment);

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

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

这可能是一个交易破坏者。但是,如果你聪明并实现缓存,并实现精心设计的微服务,每次调用响应50 - 100毫秒,我毫不怀疑这些额外的网络调用可以精心设计为而不是导致延迟应用

答案 1 :(得分:10)

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

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

Api加入

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

enter image description here 它有一个明显的缺点。它比较慢。

只读视图

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

高性能读取

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

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

答案 2 :(得分:2)

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

  

鉴于一个完全由相关表组成的数据库,该怎么做   一个将其归一化为较小的片段(表格组)

WRT数据库设计我说 "你不能删除外键"

也就是说,推送具有严格无共享数据库规则的微服务的人要求数据库设计者放弃外键(并且他们正在隐式或显式地执行此操作)。当他们没有明确说明FK的丢失时,你会想知道他们是否真的知道并认识到外键的价值(因为它经常没有被提及)。

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

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

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

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

编辑:(通过REST呼叫加入的费用)

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

在OLTP中,FK值通常不是" UX Friendly"我们经常想加入他们。

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

总的来说,我们可以发现,当我们以这种方式分解数据库设计时,我们需要做很多事情,通过REST"调用。那么这样做的相对成本是多少?

实际故事:'通过REST加入' vs DB加入

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

遗憾的是,对于DB连接而言,这并不是直接的苹果对比,而是通过REST"在这种情况下,我们也从NoSQL DB更改为Postgres。

通过REST"加入"是一个意外吗?与具有基于成本的优化器等的DB相比,性能相对较差。

在某种程度上,当我们像这样分解数据库时,我们也会放弃基于成本的优化器'所有这些都与我们的查询执行计划有关,支持编写我们自己的连接逻辑(我们在某种程度上编写了我们自己相对简单的查询执行计划)。

答案 3 :(得分:0)

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

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

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

有任何意见吗?