第5章 分解单块系统

 
 
论过了。然后,尝试理解这个单块系统能够被映射到哪些限界上下文中。假设一开始我们 识别出这个单块后台系统包含以下四个上下文。
 
•产品目录
 
与正在销售的商品相关的元数据。
 
•财务
 
账户、支付、退款等项目的报告。
 
•    仓库    -分发客户订单、处理退货、管理库存等。
 
•    推荐
 
该系统的算法正在申请专利。它是革命性的推荐系统,代码非常复杂。该团队中博士的 比例,比一般科学实验室的还要高。
 
首先创建包结构来表示这些上下文,然后把已有的代码移动到相应的位置。可以使用现代 IDE的重构功能来自动完成这些代码移动,而且你也不用专门抽时间做这些事情,在做其 他功能时穿插一些这部分工作即可。虽然IDE很智能,但还需要有测试来捕获任何可能的 破坏性修改,尤其是对动态语言来说,因为IDE很难完全精准地对它们进行重构。过一 段时间之后,就可以看到哪些代码很好地找到了自己的位置,而哪些代码找不到合适的位 置。这些剩下的代码很有可能就是我们遗漏掉的限界上下文。
 
在这个过程中,我们还会使用代码来分析这些包之间的依赖。代码应该与组织相匹配,所 以表示限界上下文的这些包之间的交互,也应该与组织中不同部分的实际交互方式一致。 比如像Structure 101这样的工具,就能可视化包之间的依赖。举个例子,如果发现仓库包 依赖于财务包中的代码,而真实的组织中并不存在这样的依赖,那么就需要看看到底是什 么问题,并想办法解决它。    -
 
5.3分解单块系统的原因
 
决定把单块系统变小是一个很好的开始。但我强烈建议你慢慢开凿这些系统。增量的方式 可以让你在进行的过程中学习微服务,同时也可以限制出错所造成的影响(相信我,你一 定会犯错的!)。把单块系统想象成为一块大理石,我们可以把整块石头炸开,但这样做 的结果通常不好。增量开凿的方式更合理。
 
所以如果我准备开始对单块系统做分离,要从哪里下手呢?接缝已经找到了,那么应该先 把哪个拉出来呢?最好考虑一下把哪部分代码抽取出去得到的收益最大,而不是为了抽取 而抽取。接下来考虑一些指导因素(也就是抽取出来的服务有什么特点)。
 
5.3.1 改变的速度
 
接下来,我们可能会对库存管理方面的代码做大量修改。所以如果现在把仓库接缝抽出来 作为一个服务,使其成为一个自治单元,那么后期开发的速度将大大加快。
 
5.3.2团队结构
 
MusicCorp的交付团队事实上分布在两个不同的地区,一个团队在伦敦,另一个在夏威夷 (有些人太舒服了!)。最好能把夏威夷团队维护的大部分代码分离出来,这样它们就能对 此全权负责。第10章会进一步讨论这个想法。
 
5.3.3 安全
 
MusicCorp有安全审计的机制,并且决定对敏感信息做更加严密的保护。目前这部分功能 由财务相关的代码处理。如果把这个服务分出去,可以对这个独立的服务做监控、传输数 据的保护和静态数据的保护等,第9章会对此做进一步阐述。
 
5.3.4技术
 
维护推荐系统的团队研究出了一种新的算法,这种算法使用了 Clojure语言中逻辑式编程 的库,并且认为这能够大大改善我们的服务。如果能把这部分推荐代码分离到一个单独的 服务中,就很容易重新实现一遍,并对其进行测试。
 
5.4杂乱的依赖
 
当你已经识别出了一些备选接缝,另一个需要考虑的点是,这部分代码与系统剩余部分之 间的依赖有多乱。我们想要拉取出来的接缝应该尽量少地被其他组件所依赖。如果你识别 出来的几个接缝之间可以形成一个有向无环图(前面提到的包建模工具可以对此提供帮 助),就能够看出来哪些接缝会比较难处理。
 
通常经过这样的分析就会发现,数据库是所有杂乱依赖的源头。
 
5.5数据库
 
前面详细讨论了使用数据库作为服务之间集成方式的做法。而且我已经非常明确地表示我 不喜欢这么做!这意味着需要找到数据库中的接缝,这样就可以把它们分离干净。然而数 据库是一个棘手的怪物。
 
5.6找到问题的关键
 
第一步是看看代码中对数据库进行读写的部分。通常这部分代码会存在于一个仓储层中, 其中会使用某种框架,比如Hibernate,来把代码和数据库进行绑定,从而简化对象和数据 库之间的读写操作。如果前面讲的东西你都有了,那么你的代码应该已经按照限界上下文 被组织到相应的包中了。对于数据库访问相关的代码来说,也应该做类似的事情,所以需 要把仓储层的代码分成几部分,如图5-1所示。
图5-1 :分离仓储层
 
把数据库映射相关的代码和功能代码放在同一个上下文中,可以帮助我们理解哪些代码用 到了数据库中的哪些部分。如果使用Hibernate的话,可以针对每个限界上下文写一个映射文件。
然而这还远远没有结束。比如我们可能会发现财务代码使用了总账表,产品目录代码 使用了行条目表,但是你可能不清楚的是,数据库中还存在从总账到行条目的外键约 朿。这些数据库级别的约束可能会有问题,所以需要使用其他的工具来可视化这些数据。 SchemaSpy (http://schemaspy.sourceforge.net)就是一个这样的工具,它可以使用图形的方 式展现出表之间的关系。
 
很多表最终会被分离到不同的限界上下文中,而上述的这个工具可以帮助你理解,这些横 跨不同上下文的表之间存在什么样的耦合。那么如何切断这些连接呢?对于同一张表被多 个限界上下文使用的场景又该如何处理呢?这些问题很难回答,但还是存在很多种不同的 处理方式。
 
回到一些比较实际的例子,重新考虑MusicCorp。前面已经找到了四个限界上下文,接下 来可以把它们分解开来,使其成为相互协作的服务。接下来看看,可能会遇到哪些问题以 及如何处理。虽然下面讨论问题的背景是关系型数据库,但其中很多原则也适用于其他的 NoSQL存储。
 
5.7例子:打破外键关系
 
在这个例子中,产品目录部分的代码使用通用的行条目表来存储专辑信息,而财务部分的 代码使用总账表来跟踪财务事务。每个月结束的时候,需要为组织内的一些人生成一份报 告,从而让大家知道我们做得怎么样。我们希望这个报告看起来很漂亮且易于阅读,所以 它的内容不应该像这个样子:“我们卖了 400份SKU 12345的副本,挣了 1300美元”,而 应该包含更多信息说明我们卖的到底是什么(比如“我们卖了 400份Bruce Springsteen的 Greatest Hits,挣了 1300美元”)。为了做到这一点,财务包中生成报告的代码,需要从行 条目表中获取SKU的标题。总账表和行条目表之间可能还存在外键约束,如图5-2所示。
图5-2:外键关系
那应该如何处理这些问题呢?事实上有两处需要改动。首先要去除财务部分的代码对行条 目表的访问,因为这张表属于产品目录相关的代码,所以当产品目录服务分离出去以后, 财务和产品目录两部分代码就会不可避免地使用数据库进行集成。快速的修改方式是,让 财务部分的代码通过产品目录服务暴露的API来访问数据,而不是直接访问数据库。这个 API调用会成为微服务化的第一步,如图5-3所示。
图5-3:外键关系的后去除
 
现在你会发现一个事实:你需要做两次数据库调用来生成报告。没错,做成两个独立的服 务之后也会是这样。这时很多人就会对性能表示担忧。我对这些担忧给出的答案很简单: 你的系统需要多快?系统现在是多快?如果能够对当前性能做一个测试,并且还知道你 的期望是什么,那就可以放心地做这些修改。有时候让系统的一部分变慢会带来更大的好 处,尤其是当这个“慢”事实上还在可接受的范围内时。
 
那外键关联怎么办?我们也只能放弃它了。所以你可能需要把这个约束从数据库移到代码 中来实现。这也就意味着,我们可能需要实现跨服务的一致性检查,或者周期性触发清理 数据的任务。这样做与否通常不由技术专家决定。举个例子,如果订单服务包含产品目 录项的ID列表,那么当产品目录项被删除,而一个汀单项却指向该不合法的产品目录ID 时,该如何处理呢?应该允许这样的事情发生吗?如果允许了,订单又应该显示成什么样 子呢?如果不允许,那么如何检查这个约束是否被破坏了呢?事实上,首先应该知道系统 的期望行为是什么,然后再根据期望行为做决定。
5.8例子:共享静态数据
 
我见过的把国家代码放在数据库中(如图5-4所示)的次数,大约和我在内部Jjva项目中 编写StringUtils类的次数一样多。这似乎暗示着,系统中所支持国家的改变频率比部署新 代码的频率还要高,但不管真正的原因是什么,这些将共享静态数据存在数据库中的例子 非常多。所以在我们的音乐商店中,如果所有的服务都要从同一张像国家这样的表中读取 数据,该怎么办呢?
 
5.8例子:共享静态数据
 
我见过的把国家代码放在数据库中(如图5-4所示)的次数,大约和我在内部Jjva项目中 编写StringUtils类的次数一样多。这似乎暗示着,系统中所支持国家的改变频率比部署新 代码的频率还要高,但不管真正的原因是什么,这些将共享静态数据存在数据库中的例子 非常多。所以在我们的音乐商店中,如果所有的服务都要从同一张像国家这样的表中读取 数据,该怎么办呢?
图5-4:将国家代码存入数据库
有这么几个解决方案可供选择。第一个是为每个包复制一份该表的内容,也就是说,未来 每个服务也都会保存这样一份副本。当然这会导致一个潜在的一致性问题。比如说,当澳 大利亚东海岸新成立了一个国家叫作Newmamopia,你有可能会漏修改掉一些服务中的表。
 
第二个方法是,把这些共享的静态数据放人代码,比如放在属性文件中,或者简单地放在 一个枚举中。数据一致性的问题仍然存在,虽然从经验上看,修改配置文件比修改在线数 据库要简单得多。通常这是比较合理的办法。
 
第三个方法有些极端,即把这些静态数据放入一个单独的服务中。在我以前遇到过的一些 场景中,数据量和复杂性及相关的规则值得我们这样做,但如果仅仅是国家代码的话就不 必了。
 
从个人经验来看,大部分场景下,都可以通过把这些数据放入配置文件或者代码中来解决 问题,而且它对于大部分场景来说都很容易实现。
 
5.9例子:共享数据(两个服务需要访问同一个服务的数据的情况)
 
现在来考虑一个更为复杂的例子,共享的可变数据对于分离系统来说通常是一个大麻烦。 财务代码会追踪客户产生的订单信息,同时也会追踪退货和退款。仓库代码也会在客户订 单被分发或者接受之后更新汀单信息。所有的这些数据都会在网站某个地方统一显示出 来,这样客户就可以看到他们的账户活动记录。为简单起见,我们把所有这些信息都放在 了通用的客户表中,如图5-5所示。
图5-5:访问客户数据:我们漏掉什么了吗?
 
所以,无论是财务相关的代码还是仓库相关的代码,都会向同一个表写入数据,有时还会 从中读取数据。在这种情况下应如何做分离?其实这种情况很常见:领域概念不是在代码 中进行建模,相反是在数据库中隐式地进行建模。这里缺失的领域概念是客户。
 
需要把抽象的客户概念具象化。作为一个中间步骤,我们可以创建一个新的包,叫作 Customer。然后让财务和仓库这些包,通过API来访问此新创建的包。按照这个思路做下 去,最终可以得到一个清晰的客户服务(图5-6)。
 
 
图5-6:识别出客户的限界上下文
 
5.10例子:共享表
 
图5-7展示的是最后一个示例。产品目录需要存储记录的名字和价格,而仓库需要保存仓 储的电子记录。最初我们把这两个东西放在了同一个地方,即比较通用的行条目表。当把 代码全都放在一起时,事实上很难意识到我们把不同的关注点放在了一起,但现在问题就 很明显了。接下来,就可以采取行动把它们存储在不同的表中。
图5-7:在不同上下文中共享的表
 
这里的答案是分成两个表,如图5-8所示。可以对仓库创建库存项表,对产品目录详情创建产品目录项表。
图5-8:分离共享表
 
 
5.11重构数据库
 
在上一个示例中,我们进行了数据库的重构操作,这种操作"j*以帮助我们分离数据库结 构。如果你想更多地r解这个I舌题,可以看看Scott J. Ambler和Pramod J. Sadalage编写的 Refactoring Databases。
 
实施分离
 
我们已经找到了应用程序中的接缝,按照限界t下文对它们进行分组,幷•且也找到了数据 库中的接缝,尽量对其进行了分离。然后呢?你想要在一次发布中把单块服务直接变成两 个服务,并且每个服务有各自的数据库结构吗?事实上,我会推荐你先分离数据库结构,暂时不对服务进行分离,如图5-9所示。
 
 
图5-9:逐步对服务进行分离
 
表结构分离之后,对干原先的某个动作而言,对数椐库的访问次数可能会变多。因为以前 简单地用一个SELECT语句就能得到所有的数据,现在则需要分别从不同的地方拿到数据, 然后在内存中进行连接。还有,分成两个表结构会破坏事务完整性,这会对应用程序造成 很大的影响,后面会对此进行详细讨论。先分离数据库结构但不分离服务的好处在于,可 以随时选择回退这些修改或是继续做,而不影响服务的任何消费者。我们对数据库分离感 到满意之后,就可以考虑对整个应用程序的分离了。
 
5.12事务边界
 
事务是很有用的东西,它可以保证一些事件要么都发生,要么都不发生。在插入数据库时这 一点非常有用,因为它允许我们对多个表同时进行修改,而且一旦发生任何错误,所有的操 作都会被回退,从而保证数据库不会处于一个不一致的状态。简单地说,一个事务可以帮助 我们的系统从一个一致的状态迁移到另一个一致的状态:要么全部做完,要么什么都不变。
 
事务不仅仅存在干数据库中,尽管这个词大多数是在这个上文中使用。举个例子,消息 代理就允许你在一个事务中提交和接收数据。
 
使用单块表结构时,所有的创建或者更新操作都可以在一个事务边界内完成。分离数据库 之后,这种好处就没有了。下面考虑一个MusicCorp上下文中可能存在的例子。在创建订 单这个场景中,我在更新i丁单表的同时,也应该在仓库团队的一张表中插人一条记录来通 知他们去派发该订单。至此,作为分离应用程序代码之前的准备工作,代码的分包和数据 库表结构的分离都已经完成了。
 
使用图5-10:在同一个事务中更新两个表
 
但是,如果我们已经把表结构分成了两部分,其中一个与客户相关,其余的与仓库相关, 那么就无法获得事务所能提供的安全性。下订单操作现在跨越了两个事务边界,如图5-11 所示。如果这个插入订单表的操作失败,我们可以显式地清除所有的状态,从而保证系统 状态的一致性。可如果插人订单表成功,但插入提取表失败了呢?
 
现存的单块表结构,可以在同一个事务中进行汀单的插入和仓库记录的插入操作,如 图5-10所示。
图5-10:在同一个事务中更新两个表
 
但是,如果我们已经把表结构分成了两部分,其中一个与客户相关,其余的与仓库相关, 那么就无法获得事务所能提供的安全性。下订单操作现在跨越了两个事务边界,如图5-11 所示。如果这个插入订单表的操作失败,我们可以显式地清除所有的状态,从而保证系统 状态的一致性。可如果插人订单表成功,但插入提取表失败了呢?
图5-11:跨越事务边界的单一操作
 
 
posted @ 2019-12-05 21:32  mongotea  阅读(163)  评论(0编辑  收藏  举报