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

我刚刚阅读了一篇关于微服务和PaaS架构的文章。 在那篇文章中,大约三分之一的时间,作者指出(在非规范化的疯狂下 ):

重构数据库模式,并将所有事情解除规范化,以便完全分离和分区数据。 也就是说,不要使用服务于多个微服务的基础表。 不应共享跨多个微服务的基础表,也不应共享数据。 相反,如果多个服务需要访问相同的数据,则应该通过服务API(例如发布的REST或消息服务接口)共享。

虽然这在理论上听起来很棒,但实际上却有一些要克服的严重障碍。 其中最大的是,数据库通常紧密耦合,每个表与至less一个其他表有一些外键关系。 因此,将数据库划分成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 

不要花太多的时间来批评我的devise,我是这样做的。 关键是,对我来说,把这个数据库分成3个微服务是合乎逻辑的:

  1. UserService – 用于系统中的用户 应该最终pipe理[users]表; 和
  2. ProductService – 用于系统中的CRUDding产品; 应该最终pipe理[products]表; 和
  3. OrderService – 用于系统中的CRUDING订单; 最终应该pipe理[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 

现在没有办法知道谁订购了什么,在什么数量,什么时候。

那么这篇文章的典型学术范畴,还是这种非规范化方法的真实世界的实用性,如果是这样,它是什么样子(在答案中使用我的例子的奖金点)?

这是主观的,但下面的解决scheme为我,我的团队和我们的数据库团队工作。

  • 在应用层,微服务被分解为语义function。
    • 例如Contact服务可能是CRUD联系人(关于联系人的元数据:姓名,电话号码,联系信息等)
    • 例如, User服务可能会以login凭证,授权angular色等方式来CRUD用户。
    • 例如一个Payment服务可能会CRUD付款,并通过像Stripe等第三方PCI兼容服务进行操作。
  • 在数据库层,表可以组织,但是devs / DBs / devops人们希望组织表

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

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

build模如下:

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

这样,属于其他微服务的实体只能通过ID在特定服务中引用 ,而不能通过对象引用。 这允许数据库表遍布所有地方的外键,但在应用程序层“外部”实体(即居住在其他服务的实体)可通过ID。 这样可以防止对象级联失控并干净地描述服务边界。

它造成的问题是需要更多的networking电话。 例如,如果我给每个Payment实体一个User参考,我可以通过一次调用就可以让用户获得特定的支付:

 User user = paymentService.getUserForPayment(payment); 

但使用我在这里build议的,你需要两个电话:

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

这可能是一个交易断路器。 但是如果你很聪明并且实现了caching,并且实现了在每次通话时间为50-100毫秒时都能够响应的devise良好的微服务,我毫不怀疑这些额外的networking调用可以被制作成不会给应用程序造成延迟。

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

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

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

也就是说,用严格的不共享数据库规则推送微服务的人们正在要求数据库devise者放弃外键(他们正在隐式地或显式地这样做)。 当他们没有明确说明FK的损失时,你会怀疑他们是否真的知道并认识到外键的价值(因为它经常不被提及)。

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

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

对于我来说,用微服务方法来分解数据库的另一个相关问题是这个报告的影响,即所有数据如何汇集到一起用于报告和/或加载到数据仓库中的问题。

与此相关的还有一种趋势,即倾向于忽略内置的数据库复制function以支持消息传递(以及基于数据库的核心表/ DDD共享内核的复制)如何影响devise。

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

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

在OLTP中,FK值通常不是“用户体验友好”的,我们经常希望join。

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

一般来说,我们可以发现,当我们以这种方式分解数据库devise时,我们需要做很多“通过RESTjoin”的调用。 那么这样做的相对成本是多less?

实际情况:“通过RESTjoin”与“DBjoin”的示例成本

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

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

与具有基于成本的优化器的数据库相比,“通过RESTjoin”performance相对较差,这是一个惊喜。

在某种程度上,当我们像这样分解数据库时,我们也离开了“基于成本的优化器”,并且为了支持编写我们自己的连接逻辑而对查询执行计划做了所有的事情(我们有些自己写了自己的连接逻辑简洁的查询执行计划)。

这确实是微服务中的关键问题之一,在大多数文章中都很容易省略。 幸运的是有这个解决scheme。 作为讨论的基础,我们来看一下你在问题中提供的表格。 在这里输入图像说明 上面的图片显示了桌子在巨石中的样子。 只有几个表join。


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

Apijoin

在这个策略中,微服务之间的外键被打破,微服务暴露了模仿这个关键的端点。 例如:产品findProductById将公开findProductById端点。 订购微服务可以使用这个端点来代替连接。

在这里输入图像说明 它有一个明显的缺点。 速度较慢。

只读视图

在第二个解决scheme中,您可以在第二个数据库中创build表的副本。 复制是只读的。 每个微服务可以在其读/写表上使用可变操作。 当只读表从其他数据库复制的表,他们可以(显然)只使用读取 在这里输入图像说明

高性能读取

通过在read only view解决scheme之上引入诸如redis / memcached之类的解决scheme,可以实现高性能读取。 join的双方都应该复制到优化阅读的平面结构。 您可以引入全新的无状态的微服务,可用于从此存储读取。 虽然看起来很麻烦,但值得一提的是,在关系型数据库上它比单一的解决scheme具有更高的性能。


有几个可能的解决scheme。 其中最简单的实现性能最低。 高性能解决scheme将需要数周的时间来实施。

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

由于每个服务在一个单独的容器中运行,并且所有这些调用都可以并行执行,所以将多个调用返回给每个事务的服务将不会产生影响。

@ ccit-spence,我喜欢路口服务的方法,但是如何devise和使用其他服务呢? 我相信这会对其他服务产生一种依赖性。

有什么意见吗?