[微服务设计][5][分解单块系统]

第 5 章 分解单块系统

5.1 关键是接缝

服务应该是高内聚、低耦合的。

在《修改代码的艺术》这本书中定义了接缝的概念,从接缝处可以抽取出相对独立的一部分代码,对这部分代码进行修改不会影响系统的其他部分。识别出接缝不仅仅能够清理代码库,更重要的是,这些被识别出的接缝可以成为服务的边界。限界上下文就是一个非常好的接缝。

5.2 分解MusicCorp

首先,我们应该识别出组织中的高层限界上下文。然后,尝试将这个单块系统的各部分代码映射到这些限界上下文中。

举例说明,想象一下,现在有一个巨大的后台单块服务,其中包含了 MusicCorp在线音乐系统所需要的所有行为。假设一开始我们识别出这个单块后台系统包含以下四个上下文。

  • 产品目录:与正在销售的商品相关的元数据。
  • 财务:账户、支付、退款等项目的报告。
  • 仓库:分发客户订单、处理退货、管理库存等。
  • 推荐:该系统的算法正在申请专利。它是革命性的推荐系统,代码非常复杂。该团队中博士的比例,比一般科学实验室的还要高。

首先创建包结构来表示这些上下文,然后把已有的代码移动到相应的位置。当移动之后,就可以看到哪些代码很好地找到了自己的位置,而哪些代码找不到合适的位置。这些剩下的代码很有可能就是我们遗漏掉的限界上下文。

接着我们分析包之间的交互。代码应该与组织相匹配,所以表示限界上下文的这些包之间的交互,也应该与组织中不同部分的实际交互方式一致。举个例子,如果发现仓库包依赖于财务包中的代码,而真实的组织中并不存在这样的依赖,那么就需要看看到底是什么问题,并想办法解决它。

5.3 分解单块系统的原因

分解单块系统最好遵循增量模式,一步一步分解。把单块系统想象成为一块大理石,我们可以把整块石头炸开,但这样做的结果通常不好。增量开凿的方式更合理。增量的方式可以让你在进行的过程中学习微服务,同时也可以限制出错所造成的影响。

5.4 杂乱的依赖

5.5 数据库

前面详细讨论了使用数据库作为服务之间集成方式的做法。而且我已经非常明确地表示我不喜欢这么做!这意味着需要找到数据库中的接缝,这样就可以把它们分离干净。然而数据库是一个棘手的怪物。

5.6 找到问题的关键

第一步是看看代码中对数据库进行读写的部分,通常这部分代码会存在于一个仓储层中。其中会使用某种框架,比如 Hibernate,来把代码和数据库进行绑定。对于数据库访问相关的代码来说,也应该做类似的事情,所以需要把仓储层的代码分成几部分,如下图所示。

把数据库映射相关的代码和功能代码放在同一个上下文中,可以帮助我们理解哪些代码用到了数据库中的哪些部分。

当将持久层分包完毕后,你会发现,有的数据库表横跨不同的限界上下文;而不同上下文之间的表还保持着外键关系;甚至数据库事务也被分离到了不同的包中完成。下面我们来探讨如何解决这些问题

5.7 例子:打破外键关系

假设出于某些业务,财务的总账表有指向产品目录的外键。

我们现在要拆分数据库,怎么处理这个外键呢?事实上修改分为两步。

  1. 首先要去除财务部分的代码对行条目表的访问。快速的修改方式是,让财务部分的代码通过产品目录服务暴露的API来访问数据,而不是直接访问数据库。这个API调用会成为微服务化的第一步,如下图所示。但是,这样做会导致一件事:原本只需要访问一次数据库就可以完成的业务,现在可能要访问多次了。好在Mysql数据库目前建立和断开连接的速度很快,所以也许多次数据库访问不是问题。

  1. 那外键关联怎么办?我们也只能放弃它了。所以你可能需要把这个约束从数据库移到代码中来实现。这也就意味着,我们可能需要实现跨服务的一致性检査,或者周期性触发清理数据的任务。

5.8 例子:共享静态数据

静态数据指的是:代码中很少更改的数据。我们可以理解为在业务中它们是只读的。例如:我们有一个表记录了本软件支持的国家。这个表通常很少通过代码进行写操作,更多是后台人员对它进行写维护。但是会有很多服务都用到国家表这种静态数据,如下图

有这么几个解决方案可供选择。第一个是为每个包复制一份该表的内容,也就是说,未来每个服务也都会保存这样一份副本。当然这会导致一个潜在的一致性问题。比如说,当澳大利亚东海岸新成立了一个国家叫作 Newmantopia,你有可能会漏修改掉一些服务中的表。

第二个方法是,把这些共享的静态数据放入代码,比如放在属性文件中,或者简单地放在个枚举中。数据一致性的问题仍然存在,虽然从经验上看,修改配置文件比修改在线数据库要简单得多。通常这是比较合理的办法。

第三个方法有些极端,即把这些静态数据放入一个单独的服务中。

5.9 例子:共享数据

前面我们讨论的是静态数据,那如果数据不是静态的呢?现在来考虑一个更为复杂的例子,共享的可变数据对于分离系统来说通常是一个大麻烦。
财务代码会追踪客户产生的订单信息,同时也会追踪退货和退款。仓库代码也会在客户订单被分发或者接受之后更新订单信息。它们都会依赖于客户信息表。如下所示

所以,无论是财务相关的代码还是仓库相关的代码,都会向同一个表写入数据,有时还会从中读取数据。在这种情况下应如何做分离?这种情况,可能提醒我们:缺少客户这个限界上下文。如下图所示。我们可以创建一个新的包,叫作Customer。然后让财务和仓库这些包,通过API来访问此新创建的包。

5.10 例子:共享表

下图展示了一个示例。产品目录需要存储记录的名字和价格,而仓库需要保存存储的电子记录。最初我们把这两部分数据全都存储在产品条目表中。但是现在仓储服务和产品服务都要访问这张表

这里的答案是分成两个表。可以对仓库创建库存项表,对产品目录详情创建产品目录项表。

5.11 重构数据库

我们已经找到了应用程序中的接缝,按照限界上下文对它们进行分组,并且也找到了数据库中的接缝,尽量对其进行了分离。然后呢?你想要在一次发布中把单块服务直接变成两个服务,并且每个服务有各自的数据库结构吗?事实上,我会推荐你先分离数据库结构,暂时不对服务进行分离,如下图所示

表结构分离后

  1. 对于原先的某个动作而言,对数据库访问的次数可能会变多。以前简单地用一个 SELECT语句就能得到所有的数据,现在则需要分别从不同的地方拿到数据,然后在内存中进行连接。
  2. 分成两个表结构会破坏事务完整性,这会对应用程序造成很大的影响

5.12 事务边界

事务是很有用的东西,它可以保证一些事件要么都发生,要么都不发生。在插入数据库时这点非常有用,因为它允许我们对多个表同时进行修改,而且一旦发生任何错误,所有的操作都会被回退,从而保证数据库不会处于一个不一致的状态。

使用单块表结构时,所有的创建或者更新操作都可以在一个事务边界内完成。例如,可以在同一个事务中进行订单的插入和仓库记录的插入操作

但是,分离数据库之后,我们就很难以如此简单的方式实现事务了。我们已经把表结构分成了两部分,其中一个与客户相关,其余的与仓库相关,那么就无法获得事务所能提供的安全性。下订单操作现在跨越了两个事务边界,如下图

5.12.1 再试一次

其实,对我们来说知道订单被捕获并被处理就足够了。我们可以把对仓库的提取表的操作放在一个队列或者日志文件中,之后再尝试对其进行触发。我们把这种形式叫作最终一致性。相对于使用事务来保证系统处于一致的状态,最终一致性可以接受系统在未来的某个时间达到一致。

5.12.2 终止整个操作

另一个选择是拒绝整个操作。我们需要通过多个补偿事务,把系统重置到某种一致的状态。

5.12.3 分布式事务

我们还使用分布式事务。分布式事务会横跨多个事务,然后使用一个叫作事务管理器的工具来统一编配其他底层系统中运行的事务。就像普通的事务一样,一个分布式的事务会保证整个系统处于一致的状态。唯一不同的是,这里的事务会运行在不同系统的不同进程中,通常它们之间使用网络进行通信。

处理分布式事务常用的算法是两阶段提交。

  1. 首先是投票阶段。在这个阶段,每个参与者会告诉事务管理器它是否应该继续。
  2. 如果事务管理器收到的所有投票都是成功,则会告知它们进行提交操作。
  3. 只要收到一个否定的投票,事务管理器就会让所有的参与者回退。