Day970.数据库表解耦 -遗留系统现代化实战

news2024/11/25 16:42:21

数据库表解耦

Hi,我是阿昌,今天学习记录的是关于数据库表解耦的内容。

微服务拆分之初,需要搭建好的两个基础设施,一个是基于开关的反向代理,另一个是数据同步机制

有了这两个设施做保障,接下来就可以大刀阔斧地一一拆解了。除此之外,如何用 API 来取代代码依赖。

可能已经发现了,这个实践与其说是解决了对于单体代码的依赖,不如说是解决了对于单体数据库的依赖。诚然,对于保险系统以及大多数这种数据密集型系统来说,所有的操作最终都将体现到数据库上。在这类系统的改造过程中,最不好解决的问题莫过于数据库表的解耦了。

这种面向数据库编程在 21 世纪初的国内是十分流行的,像 Delphi、VB 等都提供了很方便的数据库连接组件,而 PowerBuilder 更是推出了可以直连数据库的 DataWindow,将数据的访问和展示耦合在了一起。

在这样的大背景下,将所有逻辑都放在客户端组件的 Smart UI 模式,以及将业务逻辑和数据访问逻辑混杂的“事务脚本模式”为什么如此流行,也就不难理解了。

然而随着业务的发展,这种模式的弊病也越来越明显,很多团队都意识到了这个问题并着手解决。但在遗留系统中,这样的代码还是随处可见。

在微服务拆分的过程中,如何界定数据库表的所有权,进而拆分给不同的服务,就成了最为棘手的工作。


一、用 API 调用取代连表查询

用 PolicyDao 和 UnderwriteApplicationDao 去查询保单和核保申请数据,再组合成我们需要的核保申请数据并返回。

然而,在大多数遗留系统中,模块对于数据的所有权边界往往是很模糊的,也就是说,在任何模块中都可以随意访问本属于其他模块的数据。

所以,对于这样的代码,真实情况往往是这样:使用一个连表查询,一次性查出所有想要的数据。

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

// 核保服务中的代码 - UnderwriteApplicationDao.java
public UnderwriteApplicationModel getUnderwriteApplication(long policyId) {
  String sql = "SELECT * FROM TBL_UNDERWRITE_APPLICATION UC, TBL_POLICY P WHERE UC.POLICY_ID = P.ID AND P.ID = :policyId";
  return executeSqlAndConvertToUnderwriteApplicationModel(sql, policyId);
}

注意,忽略了具体的执行 SQL 语句的框架,它可能直接使用了 JDBC,也可能使用了 MyBatis,这并不是重点。

重点是如何拆分这样一个 SQL 语句。在这条 SQL 语句中,TBL_UNDERWRITE_APPLICATION 是指核保申请所对应的表,显然它应该属于核保数据库,而 TBL_POLICY 是保单所对应的表,应该留在单体库中。

怎么拆分呢?其实要拆分这样的连表查询并不难,方案和处理代码依赖问题采用的方法类似,也是通过 API 的方式来替换。

在这里插入图片描述

具体来说仍然是三步走,即调自己、调服务、组数据。最终的代码也几乎与解决代码依赖的方案一样,这里就不重复说了。

用 API 调用取代连表查询和上节课的用 API 调用取代代码依赖,都用到了两种前面学到的模式,分别是改造老城区的变更数据所有权模式,和建设新城区中的数据库包装服务模式。

在核保服务和单体服务的分权之争中,开发人员作为裁判做了这样一件事儿:把保单数据的访问功能从核保服务那里剥离出来,核保服务不再拥有这部分数据的所有权,而是转给单体服务。

当然了,在现阶段拆分出来的数据,会暂且放在统一的数据 API 中,至于到底划到单体中哪个模块,不是现阶段应该关注的。这里为了变更数据所有权,我们采用把数据库封装为 API 的方式。

获取保单数据的 API 仅仅是个提供数据库数据的接口,不包含任何业务逻辑。到这里,简单的数据解耦我们就能轻松应对了。

遗留系统的代码千奇百怪,各种数据库表的连表查询更是百怪千奇,接下来来迎战更高难的复杂查询。


二、为复杂查询建立单独的数据库

下面这张图是从一个真实的遗留系统中提取出来的 SQL 语句,对具体的内容做了模糊处理,但重要的是了解它的长度和复杂度。

在这里插入图片描述

这种复杂查询在遗留系统中屡见不鲜,甚至在有的项目中遇到过需要十几张 A4 纸才能完整打印的 SQL 语句。

如果非要硬来,对上面这种复杂 SQL 也用 API 调用来替换,那成本和风险都是相当高的,而且有些甚至是做不到的。

比如有些查询会把分页逻辑也写在 SQL 语句里,但如果 WHERE 条件中包含单体服务中的表,把这个依赖转换成 API 查询,然后在内存中再去过滤,分页就会乱掉。

那么对于类似这样的复杂查询应该如何处理呢?可以针对这样的查询,提取一个单独的查询数据库出来。

在这里插入图片描述

新建一个独立的数据库,把核保和单体数据库中的数据都同步到这个数据库中,核保服务可以直接连接到该库中进行查询操作。

由于该库包含了查询所需的全部表,因此 SQL 语句甚至不需要做任何修改,只需要区分查询和修改数据库的连接地址就可以了。

这样在一定程度上还实现了读写分离。在未来剥离单体服务中对于核保表的依赖时,也可以让单体服务访问查询数据库。以后再从单体中继续剥离其他服务时,也同样可以复用这个库,可谓一举多得。

多个微服务共享一个数据库,这不是典型的反模式吗?注意这里共享的并不是业务数据,而是查询数据,是只读的。它并不需要独立演进。

比如如果单体库独立进行了修改,并且不需要让核保服务知道,查询库就不需要跟着修改,反之亦然。

如果单体库的修改需要通知核保服务做出相应的修改,可能就需要同时修改查询库,并且重新部署,这也是很正常的情况,因为反正都需要重新部署核保服务。

最重要的是,通过新的、单独的查询数据库,极大地降低了认知负载,再也不需要去修改复杂的 SQL 语句了,这其实就是报表数据库模式。


三、冗余数据

除了单独的查询数据库,还有一种方式是将复杂查询所用到的数据,都冗余到核保库中。

这样,复杂 SQL 仍然不需要做修改,改造的成本也非常低。

在这里插入图片描述

都有哪些数据需要冗余到核保服务中呢?

表面上看,是核保服务中,所有查询 SQL 中所涉及到的表和字段的并集。

不过实际上,应该先把这些表和字段分类,对不同的类别采取不同的处理策略。

1、快照数据的冗余

第一种类型是快照数据。快照数据是指那些只关心数据当时的状态,而不关心数据后续变化的场景。

可以把这类数据存储(或缓存)在消费端,一方面需要这些数据的时候就不需要远程获取了,提升了性能,另一方面当远程服务宕机的时候,也不至于影响消费端服务。关于这类数据,我们最常见的就是微信头像更新。在点击好友头像的时候会更新一下头像,平时看到的都是他以前用的头像,这个头像就是本地存储的一个快照。

平时聊天和视频,头像是否是最新的我们并不关心,等到点击头像的时候再更新,也不会觉得有啥问题。相反,如果一定要保持头像的实时更新,对每一个手机客户端来说,都是非常严重的资源消耗。遗留系统中其实也存在类似的数据。

比如每个核保申请都有一个核保员对其进行核保。假设某个核保员叫张三,后来改名为李四,我们是希望由他核保的历史核保申请信息上,显示的仍然是张三,还是改名后的李四呢?如果仍然可以显示张三,那么我们就说核保申请上的核保员姓名这个信息,可以存储为快照数据。

按照大多数遗留系统的设计,核保申请表上存储的就是员工表的主键。当员工(核保员也是员工,因此信息存储在员工表里)改名后,连表查出来的员工姓名肯定会发生改变。我们在做服务拆分的时候,可以用 API 调用来取代这个连表查询,从而得到员工的新姓名。但当查询极其复杂,无法用 API 调用来取代的时候,你就可以回过头来想想,某个核保申请上的核保员姓名,是否必须是最新的,是否可以仅存一份快照数据而没必要更新。


冗余数据放在哪?有两种处理方法。

第一种方法是冗余到主表上。比如核保申请表上本来存储的是员工主键,现在可以增加一列姓名。在创建核保申请时,同时插入员工的主键和姓名。在查询时,不通过员工主键连表查询员工表,而是直接查出姓名。采取这种方法时,需要对原有 SQL 进行少量修改。

第二种方式是冗余到新表中。有时候要冗余的某张表的字段会很多,比如除了员工姓名,可能还需要员工号、员工级别、员工所在分公司等。除此之外,还包括申请核保的员工信息、保单的投保人信息、被保人信息等等。如果这些字段都冗余到主表,就会喧宾夺主,关注点不聚焦。因此我们可以把这些字段分别冗余到不同的新表中。

在这里插入图片描述

值得注意的是,新表的名称应该根据业务去命名,而不能跟原表的名称一样。

像申请人、核保员都来自原来的员工表,没有办法在核保库中建一个员工表,来解决这两份数据的快照。

因此要建一张核保申请人表,一张核保申请核保员表,分别来存储申请人和核保员的快照数据。

在快照表中,不再像原表那样以员工 ID 为主键,而应该以核保表的主键为主键,将原来的一对多关系改为一对一。

采取这种方法时,原有 SQL 的改动就更少了,可能只需要改一下表名。


2、业务数据的冗余

第二种需要冗余的数据是业务数据。业务数据与快照数据不同,它的变化会影响到当前业务,因此不能按快照进行冗余。

比如被保人的年龄在申请的时候填错了,进行了修改,如果还是以快照数据来存储,在核保服务中就无法得到新的年龄,就会得出不同的核保结论。

因此对于冗余到核保库中的业务数据,必须进行同步,以得到最新值。在实际项目中,必须与业务人员一起讨论哪些数据是快照数据,哪些数据是业务数据。

这一点开发人员是决定不了的,必须由业务人员来决定,但开发人员可以给出一些建议。比如业务人员在得知要将核保员姓名改成快照的时候,他们和开发人员之间很可能会发生这样的对话:

业务人员:系统使用了很多年,都是随时可以查出最新的核保员姓名,这么改不符合业务(注意这里可能并不是真正的业务,而是软件系统培养出来的操作习惯,开发人员要善于捕捉这一点)。

开发人员:那我们想想在纸质办公时代,核保员就是在核保申请上签个字或盖个章,证明是他操作的核保申请。这个签章其实就是快照,他当时叫什么名字,签章就是什么名字,以后改名了当时的签章也不会变了,不是吗?业务人员若有所思:……开发人员趁热打铁:所以快照才是真正符合业务的,对吗?

业务人员:纸质时代签名盖章很可能是无奈之举,引入软件系统不就是为了带来这些便利吗?而且像报表之类的功能,比如按核保员名称去查询他所核保的所有核保申请,如果改名了就查不出来了,会非常不方便。

开发人员:您说的很有道理,报表这里我们会特殊处理以满足功能,但是在当前这个复杂查询的场景里,是否可以只查出当时的快照呢?

业务人员:可以吧……这当然是一个假想的偏理想化的对话情景,真实的情况下要想说服业务人员可能相当困难,因为看问题的视角不同。但也是非常值得去尝试的,因为一旦存储成快照数据,的确会给服务拆分带来极大的便利,显著地降低认知负载。

注意,以上两种类型的数据,都只需要冗余我们用到的字段,不需要的字段就没必要冗余了。

在第一次创建主表数据(即核保申请)的时候,可以连带着把所要冗余的快照数据和业务数据,一起插入到核保库的相应表中。

当业务数据发生变化的时候,再通过同步机制将其同步到核保库中。


3、参考数据的冗余

第三种需要冗余的数据是参考数据,即 Reference Data。

参考数据是指那些对其他数据进行分类的数据,如国家的名称和缩写(如 CHN、USA 等)、机场的三字码(如 PEK、DAX 等)、货币代码(如 CNY、USD 等)。这些数据都是静态不变,或变化很慢的数据,因此也有人称之为静态数据。

在单体的遗留系统中,这类参考数据往往存储在单体的数据库或配置文件中,只有单体服务可以访问到。

在从单体向外拆分微服务的时候,这类参考数据也是需要冗余的,否则拆分出来的服务就无法使用了。

不管参考数据存储在数据库还是配置文件中,都可以直接在新服务中也创建一份,直接使用即可。这是认知负载最低的解决方案。

如果由于某种原因导致参考数据发生了变化,直接在两个库或配置文件中都进行修改就可以了,毕竟这种情况发生的概率很低,没有必要在它们之间建立同步机制。

当然更理想的方式是将参考数据加载到缓存中,供多个服务使用,这样就没有必要冗余了。

如果遗留系统原来就是这么做的,当然可以直接复用。但如果不是,就没有必要非按照理想的方式来,毕竟这需要额外的工作量。

在遗留系统现代化的过程中,要切忌这种发散式的思维方式。

本来目标是服务拆分,结果却引入了参考数据治理这个新的任务,导致工作不聚焦,工作范围扩大。


四、总结

一张表格,对比了拆分查询 SQL 的三种方案,以及上面没有提到的一些优势和不足。

结合自己的实际需求,对照表格选择更合理的方案。

在这里插入图片描述

解耦数据库表的方法都是基于查询的 SQL,对于 INSERT、UPDATE 和 DELETE 的 SQL,也可以用 API 调用来取代。


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

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

相关文章

Python合并同名Labelme标注文件内容

Python合并同名Labelme标注文件内容 前言前提条件相关介绍实验环境Python合并同名Labelme标注文件内容Json文件代码实现输出结果json文件 前言 本文是个人使用Python处理文件的电子笔记,由于水平有限,难免出现错漏,敬请批评改正。 (https://b…

Sping核心知识点总结

Spring框架日渐成熟,已经成为java开发中比不可少的部分,框架这东西我的理解里属于工具型应用,意味着如果没有大量实践之前之间研究理论 研究源码之类的 体会会很效率会很低,所以个人建议萌新先找个项目做一做,感受一下…

【C/C++的内存管理】

欢迎阅读本篇文章 前言🍕1. C/C内存分布1.1有关C/C的一道题目 🍕2. C语言中动态内存管理方式:malloc/calloc/realloc/free🍕3. C内存管理方式3.1 new/delete操作内置类型3.2 new和delete操作自定义类型 🍕4. operator …

论国内如何免费使用GPT4

什么是GPT,能做什么? GPT,全名为Generative Pre-trained Transformer,是一类基于Transformer架构的自然语言处理模型。GPT的主要功能包括: 文本生成:能够根据给定的输入生成合理的文本,如文章、…

双向链表实现约瑟夫问题

title: 双向链表实现约瑟夫问题 date: 2023-05-16 11:42:26 tags: **问题:**知n个人围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去&…

Java进阶-Collection集合

1.Collection集合 1.1数组和集合的区别 相同点 都是容器,可以存储多个数据 不同点 数组的长度是不可变的,集合的长度是可变的 数组可以存基本数据类型和引用数据类型 集合只能存引用数据类型,如果要存基本数据类型,需要存对应的…

ubuntu20.04开机界面黑屏,只有一个光标闪烁

接下来我就把我的解决方法完整的发出来,因为我也是非常的绝望,终于在不断尝试中解决了问题 首先开机界面就是这个东西,一直卡在这不动了,原因就是,内存被用完了,无法加载出图形化界面 解决方法&#xff1…

springboot基于vue的MOBA类游戏攻略分享平台

系统分析 系统可行性分析 1、经济可行性 由于本系统本身存在一些技术层面的缺陷,并不能直接用于商业用途,只想要通过该系统的开发提高自身学术水平,不需要特定服务器等额外花费。所有创造及工作过程仅需在个人电脑上就能实现,使…

Redis学习--下载与安装

Redis下载与安装 Redis安装包分为windows版和Linux版: Windows版下载地址:https://github.com/microsoftarchive/redis/releases Linux版下载地址:https:/download.redis.io/releases 在Linux系统安装Redis步骤: 1.将Redis安装…

JENKINS部署-学习踩坑日记

1、JENKINS情况介绍 使用docker安装JENKINS,教程可以在网上搜到,步骤执行; 2、服务器情况介绍 JENKINS部署在A服务器上面,要把项目从gitlab上面拉取下来,然后编译推送jar到B服务器,然后通过docker-compose…

Linux:文本三剑客之sed编辑器

Linux:sed编辑器 一、sed1.1 sed编辑器1.2 sed编辑器的工作流程1.3 命令格式1.4常用选项1.5 常用操作1.6 实际应用 一、sed 1.1 sed编辑器 sed是一种流编辑器,流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。sed编辑器可以根据命…

理解JVM

认识JVM Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。 什么是字节码? 字节码就是jvm能理解的代码。即扩展名为 .class 的文件。 我们日常的java文件先编译成.class 文件 然后在jvm上运行。 个人觉得 内存区域是理解JVM相关的基石。所以彻…

微服务简介,SpringCloud Alibaba Nacos的安装部署与使用,Nacos集成springboot

目录 一.认识微服务 1.0.学习目标 1.1.单体架构 单体架构的优缺点如下: 1.2.分布式架构 分布式架构的优缺点: 1.3.微服务 微服务的架构特征: 1.4.SpringCloud 1.5Nacos注册中心 1.6.总结 二、Nacos基本使用 (一&…

【C++】深入剖析C++11新特性

目录 一、C11简介 二、统一的列表初始化 1.{}初始化 2.std::initializer_list 三、声明 1.auto 2.decltype 3.nullptr 四、范围for 五、final和oberride 六、STL中一些变化 1.array 2.forward_list 3.unordered_map和unordered_set 七、右…

RabbitMQ养成记 (2. java操作MQ快速入门,日志监控,消息追踪)

快速入门 刚开始我们就一步一步来, 先搞什么spring集成。 先使用原始的java代码来操作一下MQ。 这里给新手兄弟的建议,这种技术性的学习 一定要先动手,从简单的地方动手,一步一步来,不然上来就搞理论或者复杂的应用很…

JDBC API

注册数据库驱动 Class.forName("com.mysql.jdbc.Driver"); 所谓的注册驱动,就是让JDBC程序加载mysql驱动程序,并管理驱动 驱动程序实现了JDBC API定义的接口以及和数据库服务器交互的功能,加载驱动是为了方便使用这些功能。 获…

Spring IOC相关注解运用——下篇

目录 一、Configuration 二、ComponentScan 1. 说明 2. 测试方法 3. 运行结果 三、PropertySource 1. 说明 2. 测试方法 3. 测试结果 四、Bean 1. 说明 2. 添加驱动依赖 3. 将Connection对象放入Spring容器 3. 测试 五、Import 1. 说明 2. 测试方法 3. 运行结…

从一道go逆向出发,讨论类tea的逆算法

tea代码很短,经常被直接复制为源码(而不是像标准算法那样调库)。在ctf逆向中也算比较常见,复杂度适中。 例题是一道go逆向,经go parser处理后,核心代码如下图。 panic算是go的专有名词,类似异常…

吃透 Spring AOP (1.理解概念)

理解 什么是AOP AOP,全称面向切面编程。 它可以说是对面向对象OOP的思想升华。从总的理解来讲,AOP是横向对不同程序的抽象。这个思想要不断实践动手之后,才会有很深刻的理解 理解 代理模式 在理解AOP之前,我们首先要单独说一个…

FFMPEG录屏(16)--- MAG(Magnification)捕获桌面

最近增加了对Magnification API捕获桌面的支持,记录一下过程和其中遇到的问题。 参考资料 Magnification API overview Magnification API sample webrtc screen_capturer_win_magnifier.cc Structured Exception Handling (C/C) 前言 我又不得不吐槽一下了&a…