老旧项目二次开发指南
老旧项目二次开发指南
背景:
最近新入职公司,负责技术。由于各种原因现在项目全权交由我们团队负责,之前的研发团队不再参与(及以后可能完全联系不上)。作为技术负责人,又刚入职公司压力巨大。经过两个多月的改造,算是接手得还算行。该项目存在的问题比较典型,特此记录。欢迎各位大佬批评指教。技术能力有限,文中所说解决方案(思路)只代表笔者的愚见。
项目架构
后端: spring-boot+dubbo+redis+mybatis+oracle+jeager+apollo+flyway
前段: node+react
紧急程度划分
整个项目存在的问题较多,基于自己多年研发的直觉,对问题进行简单的优先级划分。
★★★★★ : 风险规避——必须立即处理
★★★★ : 架构优化——保证业务进度的前提下,优先处理
★★★ : 性能优化——保证业务进度的前提下,尽快处理
★★ : 重构再调整——新服务但需兼容原有规范
★ : 坚持原有规范
存在的问题及解决之道
架构层面
微服务并不微(★★)
现状
多个服务的代码量远超许多单体服务;
调整原因
使用微服务架构,就使用微服务理念
解决之道
需求评审,合理定位,严格按照微服务的理念设计服务(掌握粒度,避免过度拆分)。
依赖代码不受控制(★★★★★)
现状
1.技术框架二次封装:由于spring框架提供的便利性,上个团队应该是出于对技术规范的考虑,对整个项目所使用的技术框架进行了二次封装,技术框架做了封装;
2.依赖第三方团队代码:项目中存在对三方非开源代码的依赖;
调整原因
相信这一点不用多说,不受控制的东西千万不要使用。使用一段无法控制的代码,不知道啥时候会爆雷;当没有利益做桥梁,很难期望别人会给予恩惠。要不被人锁住咽喉,唯有将不稳定的因素解决。
当然对于这样全面的技术栈替换,测试的支持必不可少。这也是我将优先级排到此处的原因。由于项目刚接受,刚好产品、测试都需要对项目进行全面了解。这样避免到以后真的被技术锁喉时,还需要来进行一次全方位的测试(这工作不是一般的小)。
解决之道
就框架的二次封装而言,相对比较容易解决;把项目中使用到的技术整理出来,逐一使用开源技术替换即可;好在是对开源技术的二次封装,大家的使用技术比较统一,项目中并没有太多依赖二次封装框架的代码存在。少量的存在也通过反编译的方式生成了源码。
其中最麻烦的是上个研发团队自己封装的工具包,整个项目涉及十几个。由于一些缘由无法获得该部分源码,没有办法对于这样,也只有使用最原始的方式——反编译(这无疑是整个项目最头疼的一部分)。
开源项目版本老旧(★★★★★)
现状
项目框架老旧,项目框架被二次封装,导致依赖的项目主体技术版本老旧,不能及时更新。
调整原因
1.项目安全是任何一种技术被采用必须被考虑的问题。一个三年前更新的技术版本,让我无法放心使用;以后技术版本越老旧,以后升级成本就越高。这也是需要先行处理开源技术被二次封装的原因。
2.过时的版本,也意味着使用不到新的技术特性。
解决之道
由于已经处理好了项目框架被二次封装的问题,对于开源技术的升级相对来说比较愉悦,各个技术官网、论坛都能找到帮助文档。
没有事务(★★★)
现状
1.单体服务事务:使用不规范,部分CUD操作甚至没有开启事务;
2.分布式事务:虽然项目框架中有引入seata事务解决分布式事务,但可能是代码先行框架后用,代码中存在大量复杂SQL,导致seata不能使用。
调整原因
没有事务带来的可怕性,不言而喻。
解决之道
由于目前对整个项目业务并不熟悉,因此笔者目前还未深入处理这块;只是先行升级开源框架,使用spring-boot 提供的优雅停服务功能来减少由于直接关闭服务带来的数据错乱问题;
笔者目前对这一块的想法本来是想通过横向切面统一给所有非GET接口都加上事务,但由于考虑到整个项目目前可以正常使用,且重新发版时间还不是特别紧因此未做处理,在此提出仅供大家参考。
另外对于分布式事务,由于项目中存在大量复杂SQL,原有seata框架没有办法继续使用(这也是为啥之前引入了此框架没有使用的原因)。笔者考虑到跨服务的事务毕竟是少数再结合目前项目进度紧张的实际情况,暂时未做处理。目前的想法是等服务先行全面测试之后,再通过调用链路服务梳理出所有的跨服务写数据业务,逐一排查处理。
缺少主要服务(★★★★)
现状
没有字典服务,字典服务、用户信息服务、权限服务为同一个服务(暂且叫做“主数据服务”)没有分离;
需要说明下:
字典服务:指维护字典数据的服务,及简单的key-value数据,例如:性别(1:男,2:女),民族(1:汉,2:满,3,蒙古...)。
用户信息服务:指维护用户信息的服务;
权限服务:指用户权限信息的维护,设计各个子服务操作权限,菜单权限的单独维护。
服务是否需要如此拆分,各个业务场景不同,需慎重考虑。只是笔者评估目前接手的项目适合如此拆分。
调整原因
主数据服务过重,性能将成为短期可预见的瓶颈。尤其是包含了字典服务的功能,而且该项目原有的架构还存在大量重复解释同一字段的问题,必然加重服务负担。
解决之道
笔者结合项目的实际情况,对服务的拆分目前没有彻底进行,而是循序渐进(在迭代中重构)。目前只是将相对独立的字典服务先抽取出来。以后需要使用字典数据的时候直接调用字典服务。要求所有研发人员在迭代开发的过程中逐步替换掉原有走主数据的方式,改为直接调用字段服务。权限服务和用户信息服务,由于涉及到的业务相对复杂,考虑到项目稳定性的问题暂时未做处理(待后期业务熟悉)。
网关服务定位混乱(★★★★)
现状
后端没有统一的网关服务,每个服务单独校验用户登录信息,前段node服务只做了接口转发并没有承担应有的大前端工作。
调整原因
网关服务混乱,每个后端服务都有对用户登录信息的校验,这无疑是没有意义的,反倒使代码不够整洁。
之后权限认证任何的调整都将牵涉到所有的服务。
而且如果现阶段不准确的定位各个服务职能,由于每个人的喜好(代码习惯)不同,必然后导致这个服务越来越混乱。
由于“不信任原则”,每个服务都会过重的对客户端进行校验。
解决之道
前端虽有node服务,但由于并没有实际运用起来,在结合目前的人员问题。因此决定后端引入网关服务,将各个服务的权限认证业务抽离,统一由网关服务承担。
说明:
笔者将该服务重新拆分,放弃大前端模式的主要原因是:
1.原有本应由原有node服务所聚合分离不同客户端接口的功能,并没有在node中,而是在后端接口中看到各种webController,AppController代码;
2.受限于前端目前人手及人员技术。
3.笔者自身对前端架构不够熟悉,前端负责人也赞同此设计。
数据库层面
未分库分表(★★★★)
现状
未分库:整个工程四百多张表全部创建在同一个用户下;
未分表:用户基本信息表,字段上百,存在大量非常用字段;
调整原因
由于项目是整体交接,之前的团队退出,新来接手的团队对之前的项目完全不了解,因此为了避免项目变得更乱,数据库分离迫在眉睫。
因为所有子服务的表全部放到同一个用户下面,如果不从技术层面实现数据的分离,由于join操作的便利性,必然会产生更多的跨服务关联查询。
解决之道
1.通过python脚本(正则匹配)找出各个子服务中在使用的表,按照CRUD操作进行归类;
2.对于已经存在的多个服务同时存在CUD同一个表的情况,仔细确定表的归宿问题;对于无法确认归宿问题的表先保存记录,待后续业务熟悉之后再次确认(非常感谢之前的开发者,整个项目梳理下来这样的表也只有十几个);
3.将所有表都按照服务归类之后,通过使用数据库提供的公共同义词,对跨服务使用的表分配对应的CRUDE权限(除非像上文2中提到无法确认归宿的表之后,禁止分配CUD权限)。
依赖数据库(★★)
现状
使用触发器
使用自定义函数
使用存储过程
调整原因
单一职责,数据库只存储数据,业务逻辑交由代码。
解决之道
禁止使用数据库存储之外的其他功能。
缓存层面-主要说明redis缓存
使用不同的序列化方式(★★★)
现状
项目中没有使用统一的序列化方式
调整原因
1.不同的序列化方式使得对缓存问题的排查,数据不够直观;
2.相同的数据由于序列化方式的不同导致同时存在多份;
解决之道
1.技术选型确定合理的序列化方式;
2.新开module,完成对redis客户端的统一封装RedisService;
3.应用spring-boot提供的自动装配技术,统一装配RedisService bean;
4.使用RedisService替换原有服务中所有使用redis客户端的地方。
没有充分使用数据类型(★★★)
现状
没有充分利用数据累类型,只使用String类型
例如用户基本信息,直接序列化成jSON字符串存储,而不是使用hash,导致当需要使用用户信息中的某个字段时得全部取出用户信息。
调整原因
无端的性能损耗,无端增加IO
解决之道
通过python脚本,分析统计出所有的大数据Key,在代码中逐一排查审核。
性能层面
在循环中CRUD数据库(★★★★)
现状
批量执行CUD等操作时,没有使用批量操作执行器,而是使用SQL拼接,更有甚者在for循环中执行CUD;(笔者不是说SQL拼接有问题,而是SQL拼接和使用批量执行器需要权衡,一般大批量的CUD操作更推荐使用批量执行器。笔者这里强调的主要是for循环逐一进行CUD操作的情况)
调整原因
循环中调用接口是编码的大忌,无需多言,尤其还设计跨服务调用,IO,时间都是几何级增长。
解决之道
使用调用链路记录,统计出所有有问题的调用。再分析出所有循环调用接口的链路交由开发人员逐一处理。
在循环中CRUD接口(★★★★)
现状
在循环中跨服务调用
调整原因
无法接受的性能损耗
解决之道
使用调用链路记录,统计出所有有问题的调用。再分析出所有循环调用接口的链路交由开发人员逐一处理。
返回大集合对象(★★)
现状
旧接口请求的数据返回不够精细,往往存在一个大的DTO在多个地方使用,而每个接口却只使用少量字段。
调整原因
1.返回大对象会暴露一些不必要的字段,存在泄漏数据的风向;
2.增加IO消耗。
3.三个月之后,各个接口的业务逻辑会让你头疼。
解决之道
VO独立,接口返回字段按需返回,避免大的DTO,VO设计。
存在重复解释字段情况(★★)
现状
1.对字典数据的解释没有统一放到前端处理,而是由后端各个服务解释翻译;
2.后端接口没有分离,存在同样的字段A服务解释了之后B服务接着重复解释,
具体表现为:
请求:web——>A——>B;
响应:B解释字段——>A解释字段——>web;
AB服务对统一字段存在重复解释。
调整原因
重复的工作,无端的性能消耗
解决之道
接口分离,同时提供解释字段和不解释字段的接口(处于对现有接口的兼容,因此提供解释字段的接口),这样消费者可以按需获取。
说明:
笔者推荐的架构风格为,所有的接口不对字典数据做解释处理。全部交由前段展示时翻译。
这样的好处有:
1.对于字典数据的处理,只需要在用户登录系统时从后台请求一次返回保存到本地即可,一次请求多次使用。
2.将解释字段的工作交由各个客户端处理,减少服务器压力。
代码规范(★★)
现状
1.命名不规范;
2.关键字段,关键逻辑缺少注释说明。
3.该使用枚举没有使用枚举;
4.存在魔法值;
调整原因
1.好的命名不需要注释;
2.没有注释的代码,三个月之后谁还看得懂;
3.没有好的命名,没有明确的注释,又不是枚举,谁会知道1,2,3具体表啥;
4.没有明确定义的1,2,3参与逻辑计算,任谁也不知道是干啥的。
解决之道
1.老服务的命名问题不做处理,干掉强迫症;
2.在迭代开发中增加代码注释;
3.新开发研发按照编码规范实施,使用sonar检测代码质量,不合格代码一律不予以合并。
项目文档(★★)
现状
没有任何文档
解决之道
1.给产品和测试提需求,要求逐步完善项目需求相关文档。
2.技术相关文档在迭代开发中同步完善。
笔者留言
以上调整,只是笔者对当前项目存在问题处理方式;由于技术能力有限,此中难免有不合理之处,诚邀各位大佬批评指教。
以下连接为笔者平时对技术的整理归档也欢迎各位大佬评论交流!