Day969.如何拆分代码 -遗留系统现代化实战

news2024/12/28 5:54:04

如何拆分代码

Hi,我是阿昌,今天学习记录的是关于如何拆分代码的内容。

当完成了项目的战略设计,大体设计出目标架构,又根据系统的现状,决定采用“战术分叉”的方式进行微服务拆分之后,接下来的难点就变成了“如何拆分”的问题。


一、拆分目标与演进计划

回顾一下之前所制定的架构目标和演进计划。

在这里插入图片描述

目标是将核保模块从遗留的单体应用中剥离出来,形成一个独立的微服务。

这意味着,不仅要将代码拆分出来,放到全新的代码库中,数据库也要从原来的单体数据库中独立出来。

以前可以随意访问的代码和数据库表,都要通过某种方式完成解耦最终核保服务和遗留单体服务之间的关系,可能会像下图这样:

在这里插入图片描述

看看如何演进。


二、基于反向代理的特性开关

先通过战术分叉,将单体中的核保模块(包括数据库)以及它所依赖的其他代码都完全复制出来,放到一个全新的 Maven module 或代码库中,并引入 Spring Boot 或其他 Web 框架。这部分工作可能会比较繁琐,但并不太复杂。

这时候,所有的请求还都是指向遗留系统的,要想验证分叉出来的服务是否正确,需要将请求引流到新服务中。

应该如何引流呢?来看第一个实践:反向代理


三、增量交付的粒度

在微服务拆分的过程中,要贯彻“以增量演进为手段”的原则,因此要确保整个拆分的过程是增量交付的,并且是可回退的。

对于增量交付,粒度上应该怎么灵活控制呢?可以粗一些到功能级别 ,即一次交付一个功能场景的改造,也可以细一些到 API 级别,甚至更细到方法级别API 级别是最合适的。

功能级别粒度较粗,可能很难在一个交付周期内完成,无法增量迭代;
方法级别 又太琐碎,且控制开关的代码对业务代码的侵入较多。

当然,在制定自己的增量交付计划时,还是要根据项目的实际情况去选择。

比如每个功能都很小,只有一两个 API,那也可以按功能级别进行控制。

如果某个 API 十分复杂,包含很多方法,也可以对这些方法添加开关。

API 级别的增量交付要求一次改造一个端到端的请求,并进行独立的测试和上线。

可以在一个交付周期内(如两周或四周)计划好要改造的若干 API,并在上线截止日前完成开发和测试。


四、实现“特性开关”

在回退机制上,需要建立一个开关,当改造后的核保服务中的 API 发生问题的时候,能够回退到老的单体服务中。

这有点类似于在开发功能时使用的“特性开关”。

怎么实现这种开关呢?由于目前所有请求都是指向单体服务的,可以在单体服务中加入一个“反向代理”层,当一个请求进入单体服务中时,会先判断当前的请求是否由开关控制。如果有开关且处于打开状态,就将请求重定向到新的核保服务中,否则就仍然访问单体中的旧代码。

如果你的遗留系统中,在单体服务之外已经有了 API Gateway,那当然就要把这个反向代理放在 API Gateway 中了。

在这里插入图片描述

这种反向代理的实现方式有很多。因为专栏里的案例是一个基于 Servlet 的 JSP 遗留系统,因此可以使用 Filter 的方式实现。

开关可以存储在 DB 中,也可以放在配置文件里。

下面是一个可以参考的代码实现:

public class ReverseProxyFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
    // 根据request获取toggle状态
    if (toggleOn) {
      // 包装request
      // 转发到核保服务
      // 得到核保服务返回的响应
      // 填充到response
      return;
    }
    // 继续执行其他filter
  }
}

在开关打开的时候,需要包装和转换当前的 ServletRequeset,然后转发到核保服务。在得到核保服务的响应后,再次转换数据并填充到 response 中。

在做请求转发的时候,可以使用 OkHttpClient 这样的工具。

为什么要做这样的转换,为什么不能直接转发 ServletRequest 呢?当反向代理功能实现完成之后,这段代码就可以上线做验证了。

可以给任意核保模块的 API 设置开关,并打开开关,验证该请求是否能够正常完成。为了保证增量演进所引入的功能,它本身也是按增量去交付的。其实使用的是“绞杀植物模式”。

通过开关和反向代理,让新服务和单体中的旧模块共存。当新服务中要改造的 API,完成对单体中其他代码和数据库的解耦之后,就可以上线,并打开开关做验证了。

如果新服务中的 API 有问题,产生了 bug,就关闭开关,让请求依然指向单体中的旧模块,确保系统的正常运行。

如果新服务中的 API 稳定运行了一段时间后,没有出现问题,对这次改造有了信心,就可以删除开关,让新服务中的 API 完全取代单体中的旧模块,从而完成这次“绞杀”。

这样逐个 API 地去绞杀,直到单体中的旧模块被新服务彻底代替。

看到这里,相信已经对改造具体的 API 跃跃欲试了吧?

来看看数据怎么处理,也就是实现数据同步。


五、建立数据同步机制

为什么需要做到数据同步呢?

不妨从业务角度做个分析。战术分叉只是将数据库复制了一份出来,但旧的单体服务仍在运行,所以总会有新的核保数据源源不断地写入核保相关的表中。

如果这部分数据没有出现在新的核保库中,那么前面设置的开关就是无法打开的,因为数据不全,无法正常开展业务。

为了真正享有反向代理带来的随时可以回退的好处,必须建立某种数据同步机制,让写入旧核保表中的数据也能同步到新的核保库中,反之亦然。这样才能保证,开关无论打开还是关闭,看到的数据都能保持一致。

回忆一下建设新老城区的隐喻,这就好比是新城区中没有自来水厂,从老城区拉一条管线过去给新城区供水一样。

在这里插入图片描述

如何建立这种同步机制呢?

说到同步,可能最先想到的就是事件拦截,这当然是非常有效且一劳永逸的方式。

每一个数据变化的点,都会触发一个事件,并持久化到消息中间件中,事件的消费方通过消费这个事件来实现数据的同步。

如果系统中已经存在了消息中间件,并且有足够的事件,那么恭喜你,你是幸运的。

然而大多数遗留系统是没有这些事件的,要想补齐所有事件来得到全部数据的修改点,工作量可能会超出服务拆分本身。在这种时候,还是推荐你使用变动数据捕获(CDC)的方式来同步数据。

建议使用事务日志轮询,而不要使用触发器,因为需要捕获的数据很多,触发器多了会变得混乱。

要记住的是,不但要做单体库到新库的同步,也要做新库到单体库的反向同步。

为什么要做这种反向同步呢?

因为当开关打开时,请求重定向给了新的核保服务,数据也写到了新的核保库中,此时的单体库中是没有这些数据的。

这时关闭开关,数据就“丢失”了。


还有一种方法,可以在单体服务和核保服务中对数据库进行“双写”,也就是既写入新的核保库,同时也写入旧的单体库,这样无论开关是否打开,都能保证两边的数据一致。

当然,双写的时候不能让核保服务直接访问单体数据库(否则做微服务拆分以及数据所有权的拆分就都没意义了),而应该在遗留单体中为单体库建立一系列数据库包装服务,来提供对单体数据库的读写访问。把这类 API 称为数据 API。

在这里插入图片描述
尽管某种程度上来说数据 API 只是一个壳子,它还是会调用已有的旧代码去修改数据,不过双向 CDC 或双写的方式显然也增加了不少团队认知负载。

再想想看,还有没有更简单的方法呢?

当然是有的,但这些方案都存在一定的局限性,需要根据项目实际情况来选择,一起来看一下后面这两种方案。

  1. 如果拆分出来的服务是只读的,我们可以在单体库中为相关的表创建视图,让新服务直接访问这些视图。当所有 API 改造完成之后,我们再根据这些视图,在新库中创建数据表,将数据一次性迁移到这些表中。
  2. 如果数据库是像 Oracle 这种包含类似 DBLink 技术的,可以在新库中创建同义词,通过 DBLink 指向旧库中的表,这样无论是读写都可以正常完成。当所有 API 改造完成后,我们根据所创建的同义词,在新库中创建表并迁移数据。

在这里插入图片描述

上述两种方案可能看起来都不是特别完美,相当于一个微服务通过某种方式,访问了它不应该访问的数据库。但不要忘了,这只是改造过程中的中间步骤。

当全部改造完成后,这种临时的基础设施(视图或同义词)就会被删除,所以完全不必纠结。另外,这样简单的方式,能让我们比较容易地获取原单体库中的数据,是符合“以降低认知负载为前提”这个原则的。

在这个案例中,虽然核保服务对于核保数据既包含读取,也包含写入,不能使用视图的方式,但由于数据库是 Oracle,我们可以使用同义词的方式来实现双向同步。

建立好回退和数据同步机制之后,就可以逐个 API 做改造了。虽然已经通过战术分叉,将核保模块从单体中复制了一份到新的核保服务中,但核保服务中的代码还是对原单体有着很强的依赖。

需要将各个 API 逐一改造,解除对于非核保代码的依赖。


六、用 API 调用取代代码依赖

先来看这样的一段位于核保服务中的代码。

这里说明一下,为了聚焦于主要的问题,对代码进行了大量的精简,实际遗留系统中的代码很可能比这要复杂很多,但解决思路是一致的。

后面出现的代码也是如此。

// 核保服务中的代码 - UnderwriteApplicationService.java
public UnderwriteApplicationDto getUnderwriteApplication(long policyId) {
  UnderwriteApplicationDao underwriteApplicationDao = new UnderwriteApplicationDao();
  UnderwriteApplicationModel underwriteApplicationModel = underwriteApplicationDao.getUnderwriteApplication(policyId);
  PolicyDao policyDao = new PolicyDao();
  PolicyModel policyModel = policyDao.getPolicy(policyId);
  return getUnderwriteApplicationDto(underwriteApplicationModel, policyModel);
}

这段代码先通过 UnderwriteApplicationDao 获取核保申请数据(核保申请就是指一个正处于核保过程中的保单),然后通过 PolicyDao 获取保单数据,之后将这两个数据进行整合,得到想要返回的 DTO。

可以看到,前面这段代码虽然位于核保服务中,但除了依赖了核保服务中的代码(即获取核保申请)之外,还依赖了本应属于单体中的代码(即获取保单),这就是在拆分的过程中会遇到的最典型的代码耦合,只有把这些耦合点全部解除,新的核保服务才能彻底摆脱对于单体的二进制依赖。因此下一步要做的就是把对于获取保单代码的依赖进行解耦。

解决方案非常的简单直观。简单总结就是三步走:

  1. 调自己;
  2. 调服务;
  3. 组数据。

在这里插入图片描述

首先让核保服务调用自己的数据库拿到核保申请数据。然后在单体中创建一个新的 API,让它返回保单数据,在核保服务中的防腐层(ACL)中调用这个 API,拿到保单数据。

最后,在原来的代码处对两部分数据进行组合。由于这个新建的 API 也是为了给核保服务提供数据,仍然将其称为数据 API。

改造后的代码类似下面这种:

// 核保服务中的代码 - UnderwriteApplicationService.java
public UnderwriteApplicationDto getUnderwriteApplication(long policyId) {
  UnderwriteApplicationDao underwriteApplicationDao = new UnderwriteApplicationDao();
  UnderwriteApplicationModel underwriteApplicationModel = underwriteApplicationDao.getUnderwriteApplication(policyId);
  // 核保服务中的防腐层,调用单体中的API来获取保单信息
  PolicyServiceProvider policyServiceProvider = new PolicyServiceProvider();
  PolicyModel policyModel = policyServiceProvider.getPolicy(policyId);
  return getUnderwriteApplicationDto(underwriteApplication, policyModel);
}

// 核保服务中的防腐层 - PolicyServiceProvider.java
public PolicyModel getPolicy(long policyId) {
  MonolithApiClient apiClient = new MonolithApiClient();
  PolicyDto policyDto = apiClient.getPolicy(policyId);
  return convertToPolicyModel(policyDto);
}

// 单体服务中的新API - PolicyController.java
public PolicyDto getPolicy(long policyId) {
  PolicyDao policyDao = new PolicyDao();
  PolicyModel policyModel = policyDao.getPolicy(policyId);
  return convertToPolicyDto(policyModel);
}

为什么要建立这样一个防腐层呢?直接在原来的位置调用 API 不行吗?这样做不是多此一举吗?

在这里添加防腐层的主要目的,其实是为了解除对于 API 调用部分的依赖,并对获取到的数据进行转换,从用于数据传输的 DTO,转回代码处所需要的 Model。

这样的防腐层可以有效防止业务代码的腐坏,把会发生变动的部分隔离在外面。

上面的代码有很多坏味道(比如一些命名的问题和可测试性的问题),为什么不重构?答案是因为要分离关注点

现在关注的是架构解耦,并不是代码重构。一旦因为重构导致当前改造的范围扩大,就无法完成既定的交付目标了。

可能还想到一个问题,就是这里是否可以使用抽象分支来进行增量重构,即将 UnderwriteApplicationService 提取成一个接口,将原来的类作为旧实现,再写一个新实现来解耦。其实是没有必要的。

因为在最上层已经通过特性开关来实现了这个“分支”,就没有必要在代码级别再使用抽象分支了。

也就是说,使用了绞杀植物模式用服务替换旧模块,代码已经复制出来一份了,在复制出来的这部分中,就没必要再使用修缮者模式了。


七、总结

“反向代理”和“数据同步”是之后的基础,微服务拆分的方方面面都离不开这两个实践。

如果数据库是 Oracle,强烈建议使用基于 DBLink 的同义词来进行“数据同步”。

其实严格来说,这并不算是同步,而只是一种转换,但它可以让暂时不用考虑数据的问题,降低了认知负载的同时,还能帮梳理数据的所有权。等代码解耦完成,在核保库中所创建的同义词,都是需要拆分到核保库中的数据表。

除此之外,还分享了“用 API 取代代码依赖”,这也是比较基础的一个实践,是代码解耦的基本方法。

后面的课程里要讲的数据访问解耦、存储过程解耦等,都使用了和这个实践类似的方法。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/530558.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用GPT-4 写2022年天津高考作文能得多少分?

正文共 792 字,阅读大约需要 3 分钟 学生必备技巧,您将在3分钟后获得以下超能力: 积累作文素材 Beezy评级 :B级 *经过简单的寻找, 大部分人能立刻掌握。主要节省时间。 推荐人 | Kim 编辑者 | Linda ●图片由Lexica …

vue工程搭建

1&#xff1a;查看vue及npm版本&#xff1a; 2&#xff1a;执行npm init nuxt-app <project-name>语句&#xff1a; 若出现npm ERR! code ENOLOCAL 请执行如下语句&#xff1a; npm cache verify npm cache clean --force npm i -g npm npm install -g cnpm --regis…

数据库(mysql语句)循环语句

例题1&#xff1a; 20到50之间能被5除余1的所有自然数的和 EDECLARE i int DECLARE s int SET s0 SET i20 白WHILE i <50 BEGIN IF(i%51) SET s s i SET ii1 END PRINT20到50之间能被5除余1的所有自然数的和是cast(s as varchar(20)) 例题2&#xff1a; 实现如下图 代码…

设计模式之门面模式(Facade Pattern 外观模式)

一、模式定义 门面模式(Facade Pattern)&#xff1a;外部与一个子系统的通信必须通过一个统一的外观对象进行&#xff0c;为子系统中的一组接口提供一个一致的界面&#xff0c;外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。门面模式又称为外观…

中国南方Oracle用户组沙龙活动:大环境下的Oracle数据库的机遇与挑战

2023年03月12日(周日)在杭州索菲特西湖大酒店 (浙江省杭州市上城区西湖大道333 号)&#xff0c;中国南方Oracle用户组创始人之一&#xff1a;周亮&#xff08;zhou liang&#xff09;组织举办了主题为《大环境下的Oracle数据库的机遇与挑战》活动&#xff0c;大约有50名左右的人…

YMatrix 5.0 故障自动转移功能新实现,运维更方便!

前言 分布式数据库一般都实现了数据多副本的存储以保证数据的高可用性。在多副本存储的基础上&#xff0c;通过切换活跃的存储副本来实现故障转移&#xff0c;是常见的做法。 YMatrix 5.0 实现了在数据库集群所有数据分片上的故障自动转移&#xff0c;完全实现了数据库集群的…

一文带你深入了解分布式数据的复制原理!!

在分布式数据系统中&#xff0c;复制是一种重要的能力。简单来说&#xff0c;复制就是将数据的副本存储在多个位置&#xff0c;通常是在不同的服务器或节点上。这样做有几个关键的优点&#xff1a; 使得数据与用户在地理上接近&#xff08;从而减少延迟&#xff09;&#xff0…

渗透测试--3.1嗅探欺骗攻击

目录 1.中间人攻击 2. 社会工程学攻击 SET使用实例——建立克隆钓鱼网站收集目标凭证 SET工具集之木马欺骗实战反弹链接 后渗透阶段 钓鱼邮件 总结 1.中间人攻击 中间人攻击&#xff08;Man-in-the-middle attack&#xff0c;简称MITM&#xff09;是一种常见的网络攻击…

一文带你完整了解数据的编码!!

数据编码是将数据转换为计算机可读取和处理的二进制格式的过程。在数据存储中&#xff0c;正确的数据编码非常重要&#xff0c;因为它能够保证数据的完整性、可靠性和可读性。 数据编码可以确保数据在存储过程中不会发生错误。通过使用适当的数据编码规则&#xff0c;可以防止…

三十二、自定义镜像

1 、Docker镜像的原理 Docker镜像本质是什么? Docker中一个centos镜像为什么只有200MB&#xff0c;而一个centos操作系统的iso文件要几个G? Docker中一个tomcat镜像为什么有500MB&#xff0c;而一个tomcat安装包只有10多MB? 操作系统组成部分: 计算机组成原理 进程调度子…

OPPO 关停“造芯”业务 ZEKU:一场大胆的尝试的结束

前言 近期&#xff0c;OPPO 关停了其“造芯”业务 ZEKU&#xff0c;结束了其自主研发处理器的尝试。本文将对这一事件进行探讨&#xff0c;分析其背后的原因及其对整个行业的影响。 1. ZEKU的成立与使命 在2019年&#xff0c;OPPO旗下的ZEKU研究所成立&#xff0c;标志着OPP…

Redis缓存穿透、缓存雪崩和缓存击穿

缓存穿透 缓存穿透&#xff0c;是指查询一个缓存和数据库中都没有的数据。正常的使用缓存流程大致是&#xff0c;数据查询先进行缓存查询&#xff0c;如果key不存在或者key已经过期&#xff0c;再对数据库进行查询&#xff0c;并把查询到的对象&#xff0c;放进缓存。如果数据库…

Centos和Windows之间通过主机名实现相互访问

一、业务需求 在内网环境中,我们想直接通过特定的主机名称去访问我们的服务器,而不用去记忆服务器的IP地址,且不想通过nginx等代理工具或域名配置内容来操作。 二、分析 通常我们可以直接在浏览器中输入【localhost】后按下回车键后就可以访问我们的主机80端口的内容了;那…

计算机网络大作业(Wireshark抓包分析)

实验要求 wireshark的深入学习与掌握&#xff0c;如过滤器的使用&#xff0c;归纳方法通过实验阐述ARP的工作原理利用实验结果分析 ICMP 协议的报文结构字段定义基于实验数据深入分析 TCP 协议的连接过程原理&#xff0c;报文的分片等功能从校园网发起向外网中某 Web 服务器的…

基于正点原子电机实验的pid调试助手代码解析(速度环控制)

这里写目录标题 下位机与PID调试助手传输的原理代码讲解(基于正点原子)解析数据接受和数据发送的底层函数数据接受数据帧格式环形数组以及怎么找到它的帧头位置crc校验 数据发送数据上传函数 通过前两节文章&#xff0c;我已经了解了基本的pid算法&#xff0c;现在在完成了电机…

MatrixGate 5.0 性能再升级,加载速度提升三倍!

前言 数据的加载速度在数据处理和分析领域一直是一个挑战&#xff0c;为应对这一挑战&#xff0c;YMatrix 数据库开发了 mxgate 高速数据加载工具。最近&#xff0c;随着 YMatrix 5.0 的 GA&#xff0c;新版 mxgate 与上一版本&#xff08;4.8.1&#xff09;相比&#xff0c;速…

Linux 虚拟机 磁盘扩容

概述 在单台虚拟机上部署了过多服务&#xff0c;导致磁盘使用过度达到98%。 现在扩容提高磁盘容量&#xff0c;增加10G。 现象 df -h df -ih du -s具体步骤 VMware 扩容 关闭虚拟机的情况下执行&#xff0c;类似于生产环境下需要关闭服务器&#xff0c;从而添加硬盘等相关操作…

Liunx基础命令 - rm删除命令

rm命令 – 删除文件或目录 rm命令来自英文单词“remove”的缩写&#xff0c;中文译为“消除”&#xff0c;其功能是用于删除文件或目录&#xff0c;一次可以删除多个文件&#xff0c;或递归删除目录及其内的所有子文件。 rm也是一个很危险的命令&#xff0c;使用的时候要特别…

从BinDiff到0day 在IE中利用CVE-2019-1208

前言 如上所述&#xff0c;CVE-2019-1208是UAF漏洞&#xff0c;这类安全漏洞可以破坏有效数据、引发进程crash、并且可以精心利用最终导致任意代码执行。而对于本文介绍的CVE-2019-1208而言&#xff0c;成功利用此漏洞的攻击者可以获得系统当前用户权限。如果当前用户具有admi…

理解Java虚拟机——JVM

目录 一、初识JVM 二、JVM执行流程 三、内存区域划分&#xff08;JVM运行时数据区&#xff09; 3.1 本地方法栈&#xff08;线程私有&#xff09; 3.2 程序计数器&#xff08;线程私有&#xff0c;无并发问题&#xff09; 3.3 JVM虚拟机栈&#xff08;线程私有&#xff0…