如何在微服务分布式架构中删除数据? - bennorthrop


尽管微服务具有各种好处,但似乎也有许多新的复杂性和并发症。我最近经常遇到的一种情况(并没有找到很多很好的资源)是删除数据。考虑一个简单的例子:
有三种服务:

  1. Product 服务,管理与所提供的产品,
  2. Order 追踪产品购买服务,
  3. Catalog服务 ,其控制被发布到不同的媒介(比如,印刷,数字等)。

现在如果需要删除给定的产品会发生什么?说“Widget X”不再是要出售的产品。或者“Widget X”被某个昏昏欲睡的用户意外添加,只需要删除。处理这个问题的最佳方法是什么?
 
解决方案 1:只需删除它
在单体世界中,我们也许可以删除表中的“Widget X”行PRODUCT,就是这样。如果在另一个表中的某行有一些对该产品行的外键引用(例如,因为该产品是在先前的订单中销售的,或者该产品包含在现有目录中),则可能会有一个删除级联,它会自动也删除这些行(通常是危险的事情),或者至少会有一些外键约束在尝试删除该产品时会引发错误。不管怎样,因为单体应用中的所有数据都驻留在一个数据库中,所以可以集中管理参照完整性(即不留下孤立数据)的管理。
在微服务架构中,这显然行不通。当然,“Widget X”行可以直接从PRODUCTProduct 服务的表中删除,但其他服务的数据将被有效地孤立。
例如,订单服务可能在其ORDER_DETAILS旧订单的表中引用了“Widget X” ,并且可能想要获取有关此产品的信息以显示完整的订单详细信息。如果从 Product 服务中删除了“Widget X”,则 Order 服务将在其 GET 请求中收到 404 /products/widget-x。换句话说,即使每个微服务都旨在解耦和自治,但这并不意味着它从数据的角度来看是完全独立的。如果我们想保持一定程度的参照完整性,协调和沟通是必要的。
(banq注:这是最初设计问题,订单在其ORDER_DETAILS旧订单不能引用产品“Widget X”,而是复制一份,因为当时下订单的上下文时的产品快照必须被记录到订单上下文中,这样,即使该产品信息在下完订单以后更改,也不会引起下这个订单的用户的异议,技术问题如果不结合业务上下文就很难解决)
 
解决方案2:同步协调

下一个可能性是让产品服务在删除给定产品时首先检查任何依赖服务以查看它是否能够删除该产品,然后在必要时执行该“级联”。例如,产品服务可以首先检查订单服务和目录服务是否可以安全地删除“Widget X”(即没有对它的引用),并且只有在确认它是安全的之后才会从PRODUCT表中删除. 或者,产品服务可以继续进行自己的删除,然后同步调用订单和目录服务,并说“我刚刚删除了我的小部件 X,因此从您的数据库中删除对它的任何引用”。
即使从业务规则的角度删除这种方式是可以的,这也是不明智的。创建从产品服务到目录和订单服务的同步依赖有很大的缺点——复杂性增加、每个请求的额外延迟以及弹性/可用性受损(即,如果订单服务关闭,产品服务无法执行删除)。微服务应该尽可能地独立。将它们与运行时依赖项绑定在一起时应该慎重考虑,以免我们只是创建一个分布式单体
 
解决方案 3:不要删除
在这一点上,我们可能会观察到,无论如何删除产品并没有真正意义!正如Udi Dahan 指出的那样,在现实世界中确实没有“删除”事物的概念。相反,数据只是改变状态。员工不会被删除、被解雇或辞职。订单不会被删除,它们会被取消。并且产品不会被删除,它们不再销售。换句话说,在几乎所有情况下,在给定行上支持某些状态字段比完全删除它要好。
在这里的例子中,这将解决一些问题。产品服务不支持真正的“删除”,而是只公开产品的“退役”(或其他)端点,但将数据保留在其数据库中。这样,目录和订单服务将不再有孤立的数据——它们仍然可以回调产品服务以获取有关“Widget X”的信息,即使它当前不再销售。
但这并不能解决所有问题。如果目录服务需要知道某个产品正在退役,以便不会针对给定目录向客户显示该产品,该怎么办?当然,它可以查询产品服务以获取此信息,但这可能会引入从目录服务到产品服务的同步依赖关系,并引入上面讨论的那些相同的复杂性、延迟和弹性问题。
 
解决方案 4:异步更新
为了支持目录服务的自主性,它可以维护自己的产品数据本地缓存,而不是依赖于产品服务,使其与产品服务的更改保持同步。当产品退役时,产品服务可以发出一个ProductDecommissioned目录服务会监听的事件,然后它可以更新自己的本地产品商店。在“Widget X”的情况下,一旦退役,Catalog 服务就会知道不会在其目录中显示它。这有效。除了...
这个产品数据的本地缓存 究竟什么?是内存吗?还是目录服务数据库中的表?我们如何确保它与事实来源,即产品服务同步?这里有几个选项,每个选项都有自己的优点和缺点。
如果本地缓存在内存中,那么这很容易实现,并且可能很容易同步(例如,可能有一些协调过程来检查产品服务并确保其自己的数据是最新的/最新的)。缺点是它放弃了数据库级别的参照完整性。如果CATALOG在其数据库的表中存在某个 product_id ,则无需(在数据库级别)强制该 product_id 实际上有效(即映射到产品服务数据库中的 product_id)。此外,如果要缓存的数据很大,则内存缓存可能无法成立。
或者,Catalog Service 中的本地缓存也可以是一个数据库表,这将从上面解决数据库级引用完整性和存储问题。一个问题是复杂性。编写 SQL 以播种和更新本地PRODUCT表并非易事 - 编写的代码越多意味着需要维护的代码越多。此外,同步还增加了复杂性——如果给定微服务有多个实例,但只有一个数据库实例,那么哪个微服务或哪个进程负责更新和同步缓存?
可能更好的解决方案是实现一个事件日志(使用 Kafka 等),并让每个微服务利用它来与数据更改保持同步。这个选项有很多需要考虑的地方(超出了这篇博文的范围),一个很好的起点是事件驱动微服务。虽然这种模式优雅地解决了上述所有问题,但在我看来,这是一种需要构建和支持的升级架构,因此只有在没有足够专业知识和资源的情况下才能进入。
 
作者:Ben Northrop
我是一个“老”程序员,已经写了将近 20 年的博客。2017 年,我创办了Highline Solutions,这是一家帮助软件架构和全栈开发的咨询公司。我有卡内基梅隆大学的两个学位,一个是实用的(信息和决策系统),另一个不是(哲学 -论文在这里)。