新结算系统项目总结
- 1. 项目介绍
- 2. 项目划分
- 2.1 设计阶段:
- 2.1.1 旧结算系统的设计总结
- 2.1.2 新结算系统架构选型
- 2.1.3 新结算系统模块设计--模块设计
- 2.1.4 新结算系统实体设计--er设计
- 2.1.5 新结算系统关键算法--详细设计
- 2.1.6 稽核设计
- 2.1.7 交互设计
- 2.2 开发阶段
- 2.3 验证阶段
- 2.4 发布阶段
- 2.5 优化-维护阶段
- 3. 项目中做的优点
- 4. 项目中做的缺点
- 5. 总结
本文只是介绍作者在公司中参与的一个项目,以及参与项目的感受。
1. 项目介绍
项目背景:因旧结算是c++单体程序,面临单体程序的性能瓶颈,而且旧结算程序由完全独立的6个字程序构成,子程序之间的调度性能低,c++程序调试,维护效率低,同时面临着没有专业的c++开发人员维护的问题。因此,计划对旧结算程序进行升级改造。
项目计划:在不改变旧结算程序的基础上,重构结算系统,简化程序复杂度,提升性能。由java生态中的大数据完成,因为java开发人员多,没有c++开发人员。增加结算系统中稽核的新功能。
项目参与人员:产品1名,开发2名,测试1名。
项目时间:没有准确时间,越快越好。(因为目前一个人有多个项目参与,随时面临打扰风险)
我在项目中的工作:旧结算系统就是我维护的(我主要还是java开发),因此对结算业务熟悉,同时熟悉旧结算系统架构,熟悉旧结算系统的代码和一些关键算法。同时因为参与2018~2020年结算,了解客户结算系统的需求和痛点。所以,我在整个项目中是核心设计者,核心开发者,同时提供旧结算系统的支持。
2. 项目划分
项目共被分为了5个阶段,2个部分。
5个阶段是设计阶段,开发阶段,验证阶段,发布阶段,优化阶段。
2个部分是结算部分和稽核部分。
2.1 设计阶段:
在设计阶段主要完成旧结算系统的设计总结,新结算系统架构设计,新结算系统的模块设计,新结算系统的实体设计,新结算系统的流程设计,新结算系统的关键算法设计。
2.1.1 旧结算系统的设计总结
在最一开始,参与项目的人员需要了解旧结算系统,需要了解结算的业务需求。而我主要是总结旧结算系统的业务需求,旧结算系统的实体结构,旧结算系统的业务流程,旧结算系统数据流等。
这个阶段,我需要负责给参与的其他人讲解旧结算系统的组成,旧结算系统的使用,以及如何从0开始搭建一个旧结算系统。
在这个过程中,总结输出了旧结算系统的er文档,旧结算系统的业务设计文档,旧结算系统的安装配置手册,旧结算系统的使用说明手册。
当然,旧结算系统也不是完全没有文档,可以借鉴大约25%左右。
2.1.2 新结算系统架构选型
因为是基于java生态圈,因此待选的架构当时只考虑从spring batch和sprak中选择一个。
在大家讨论确定架构时,当时主要是2个开发人员和一个领导以及其他两个开发人员参与(一开始确定参与开发的人员是4个)。
经过讨论,最终确定使用spring batch。
原因如下:
- spring batch是spring 家族的一员,与公司新系统的微服务模式兼容。
- spring batch是纯java开发,公司目前全部的开发人员要么是java后端,要么是前端。
- spark需要额外了解spark大数据的其他架构,技术门槛较高。
其实我个人对使用什么架构没有一定的倾向,这两个对我来说都是技术盲区,都需要从零开始学习,然后才能参与开发。
而且这个项目对我来说是一个非常好的增加项目经验的机会,所以不管是选择哪一个技术栈,我都需要从0开始的。
但是为了跟上大家讨论的思维,还是简单的了解了下这两个架构的特点。
总的来说,公司的数据量确实还没有达到必须使用spark这样为超大数据量量身定做的地步,不管是spring batch还是spark都能满足新结算的性能需求。
其次,spark想要运行起来,需要了解一些配套的比如HBase,HSQL,等等一些额外的技术栈。光熟悉这些技术栈可能就需要很长的时间。
而spring batch则不一样,它和其他spring 架构一样,只需要了解很少的知识,你就能上手开发。
所以,最终大家一致决定使用spring batch。
为了更好的使用spring batch开发,我系统的学习了spring batch.
spring batch技术专栏
github地址:
https://github.com/a18792721831/studybatch.git
文章列表:
spring batch 入门
spring batch连接数据库
spring batch元数据
spring batch Job详解
spring batch step详解
spring batch ItemReader详解
spring batch itemProcess详解
spring batch itemWriter详解
spring batch 作业流
spring batch 健壮性
spring batch 扩展性
2.1.3 新结算系统模块设计–模块设计
在旧结算系统中,将结算分为了6个子程序,这些子程序之间完全独立。这6个子程序分别是采集、预处理、批价、出账、账前回退和账后回退(在电信行业结算基本上都是这几步,算不上公司商业机密)。
当初在开发的旧结算系统时,大概是在10年左右,当时机器的性能比较差,因此选择了临时文件作为本地缓存数据的方式,以此来提升性能,同时为了保证程序的有效性,因此将整个结算系统分成了多步。
旧结算系统的子系统执行有着严格的顺序,必须先采集=>预处理=>批价=>出账。如果中间的哪一步出错了,还必须先使用回退程序,然后重新执行。
整个旧结算系统操作起来非常繁琐,而且非常容易出错,采集和预处理都是在服务器的内存中操作,操作的都是临时文件,因此如果机器宕机,那么数据是找不回来的,也没法记录断点更没有办法快速恢复。
结算是每月一结算,旧结算系统因过程容易出现问题,因此需要手动执行。成本非常高(人是最贵的)。
基于这些痛点,加上现在机器性能提升,以及新架构的支持,新结算的系统模块进行了非常大胆的改进。
将原来6个子程序或者说模块,直接合并为1个。
合并为1个模块,到底是进步还是退化呢?
在旧结算系统中,分成多个子程序,最核心的原因是前置操作的有效性。比如结算最后一步出错,那我前面的已经完成的操作还是有用的,并不是完全无用,需要丢弃的。因此,在旧系统中,分成多个子程序,就是保证前置操作有效,不会因小失大。
在新结算系统中,使用的是spring batch架构,spring batch架构本身就支持快速恢复,断点记录等功能。即使在某个时间点程序异常或者宕机,也可以快速恢复的。
在旧结算系统中,使用临时文件,是因为在旧系统中,需要将数据全部加载到程序中,才能进行操作。
而在新系统中,是分批处理的,一次只处理一批就行。
所以,在旧结算系统中,内存的要求是很高的,都是在20G以上。在新结算系统中,内存的要求非常低,甚至最小配置为256M的机器都能运行。而且支持根据内存调整(控制分批中每一批的数据数量,也就是spring batch的chunk的数量)
旧结算系统中,子程序之间的调度需要有一个程序不停的死循环询问操作是否完成,是否进行下一步操作。性能比较慢,而且还耗费资源。
新结算系统中,只有一个模块就不存在调度的问题了。
旧结算系统中的每一个子程序都是有状态的,执行的顺序也是必须的。
在新结算系统中,将6个子程序融合为一个整体,将结算程序变为无状态的。
综上,我认为将6个模块合并为1个,是在新架构的支持下,考虑现在硬件资源而做的一个大胆的改进。
2.1.4 新结算系统实体设计–er设计
新结算系统的实体设计中摒弃了旧结算系统中一些无用的,或者可以去掉的一些配置,同时结合新架构,增加了一些配合新架构使用的数据表。
毕竟旧结算系统已经使用了10年左右了,整个结算的业务比较成熟,而且还不能很大的修改结算的界面使用习惯,因此这部分改动很少。
2.1.5 新结算系统关键算法–详细设计
在旧结算系统的c++程序中,有大量的for循环和if判断,以及很多的自定义异常,通过异常做正确的业务流程控制。
在新结算系统中,为了改进这种编码风格,同时新系统是基于jdk11的,因此大量的使用了lambda表达式,以及jdk8中的流的操作。
同时改变了c++用异常控制流程的风格,而是使用数据驱动流程。在进行某个操作之前,使用流的过滤操作,保证操作不会出现预期之内的异常。至于预期之外的异常,本来就该继续抛出,而不是直接忽略。
同时为了去除或者说避免多层嵌套代码,使用了jdk8中的Consume,Function和Predicate函数。以及这些函数的组合。
这样消除了多层嵌套代码,同时,代码的密度也是c++代码密度的10几倍。
2.1.6 稽核设计
稽核部分是在结算部分之后的,有了结算部分的经验,稽核部分也大量参考了结算部分的很多经验,所以基本上差别不大。
稽核程序被分为了结算前稽核和结算后稽核。
稽核详细设计和模块以及er设计差不多的。
稽核部分完全由我一个完成,其他人主要参与审核。
2.1.7 交互设计
前面都是程序设计,还缺少了spring batch的交互。
spring batch的交互考虑了2种方案:spring batch admin和xxl-job。
考虑到交互的习惯,选择了国人开发的xxl-job作为spring batch的交互。
全部的设计都有相关的文档输出,包括模块通信图,er图,状态装换图,数据流图和流程图。
2.2 开发阶段
当设计阶段完成后,并且设计通过了审核后,就到了开发编码阶段。
现在公司微服务的编码都是符合DDD设计思想的。因此代码的开发在DDD的指导下,完成开发。
flyway管理脚本;统一的日志;jpa做为核心的dao。统一的异常等等。这些都是统一的编码规范,需要通过sonarLint检查才能提交。
在开发中,也使用了guava的本地缓存加快性能。
大量的注释等等。
编码中使用函数,实现了一些有意思的写法:
比如基于函数实现责任链模式,
基于函数实现简单工厂模式,基于泛型和函数实现构建者模式,状态模式等等。
2.3 验证阶段
验证阶段主要是测试人员验证,出现问题之后快速修改即可。
整个项目共有13个小问题。编码的问题率还是很低的。(代码基数不小)
2.4 发布阶段
整个项目采用docker镜像的方式发布,采用docker-compose管理。
所以需要将应用程序先打包为镜像,然后开发docker-compose的配置,最终给出新结算系统的安装配置手册和使用手册。(这两个手册都是我开发的)
将应用程序打包为镜像,使用的是jenkins打包,打完包后将镜像上传到harbor镜像私有仓库中。
然后开发docker-compose的文件,在现场机器上使用docker-compose启动即可。
2.5 优化-维护阶段
发布之后就是优化阶段了,这个阶段也是最长的了。在我写这篇文章的时候,项目正处于刚完成发布的优化-维护阶段的初期。
3. 项目中做的优点
- 系统的学习需要的架构,为开发与设计提供技术基础。
为了能更好的在spring batch中开发与设计,进行系统的学习。(系统的输入才有系统的输出)系统的学习了之后,才能根据spring batch的特点,进行合适的设计与开发。 - 积极使用jdk新特性,改变不好的编码风格。
jdk8的新特性,可以在很大程度上提升代码密度,减少无用代码逻辑,直击数据。 - 编码中遵循DDD设计思想。
在DDD思想的指导下,开发的代码都是框架无关的,从spring batch框架,切换到其他框架,只需要改动很少的几个对外接口,内部的核心逻辑是不需要做任何改动的。 - 融入设计模式
没有设计模式,我们也能实现这些功能。但是使用了设计模式,可以更加科学的实现功能,对之后的可维护性和程序的扩展性,健壮性都有很大的提升。 - 使用本地缓存
频繁的交互数据库肯定不行,最好的解决方式是使用专门的缓存redis,但是又不能将新系统做的太过复杂,因此退而求其次,使用guava的本地缓存是一个不错的选择。既能增加减少数据库交互,有不会增加系统复杂度。 - 引入了nacos配置中心
旧系统中的配置是文件配置,没有版本管理,没有格式检查,没有动态刷新,没有权限管理。谁都可以修改配置,修改完还不知道对不对,只能重启试试。
新系统使用了nacos配置中心,有版本管理,有格式检查,能动态刷新,有权限管理。正好解决了这些痛点。 - 性能的提升
说实话,在做之前,我是不确定是否能完全满足需求。但是至少从已通过的验证来说,全部的痛点和新需求都满足了。特别是性能上,是老系统的10倍以上。
4. 项目中做的缺点
- 伪集群
新结算系统不是严格意义上的集群,但是新结算系统又能在xxl-job的支持下,多机器配合。所以,我认为这是一个伪集群。xxl-job是一个分布式任务调度系统,xxl-job是支持分布式的,因此虽然新结算是单体的,但是却能启动多个实例,用xxl-job进行调度,近似实现集群。
不过,这样也有缺点,当两个实例操作相同的数据时,存在并发问题。这也是伪集群的一个致命问题。 - 没有进行性能验证
公司数据量太少,所以就没有进行大数据量下的性能验证。但是就同等数量下的性能验证(现有数据量,万级,生产是百万级),性能提升了10倍以上。
不过这个性能还需要在大数据量下进行验证。 - 不能滚动部署
最好的部署是基于kubernates下的滚动部署。
但是客户没有kubernates的维护售后人员,因此选择一个折中方案,使用docker-compose进行部署。
这样就不能实现自动化滚动部署,出现问题不会自动回滚了。
只能手动部署,出现问题手动回滚。
不过,这个倒不是很严重的问题,因为这个结算系统只是给少数的和财务有关的维护人员使用,难用点,客户到是能接受。
5. 总结
整个项目从2020年11月开始到2021年2月中完成,历时3个月半。期间对旧系统进行全面的总结,学习新架构,进行新系统的设计。包含了架构设计,模块设计,实体设计,流程设计,关键算法设计。设计审核评审通过后,结合科学的开发思想指导,完成编码任务。其中用到了DDD开发思想,jdk高效特性,比如流操作,lambda表达式,函数等,以及设计思想的知道,统一规范的异常和日志,以及提交前sonarLint的检查等。在完成验证后,开发部署脚本,编写手册。开发docker-compose的部署脚本,编写安装配置手册,编写使用手册。
整个项目难度不大,因为我本来就是维护旧系统的人员,因此在业务方面基本上没有难度。挑战是如何快速的学习新架构,并快速的学以致用,在新的设计中进行体现。如何以不同的层次,进行系统的设计,设计完成后,如何评审自己的设计是否合理,能否站在不同的高度,不同的角度评估设计。接下来就是如何将设计进行诚实的实现。(在开发结算的时候,出现了编码与设计不一致的问题(就是本地缓存,设计时没想到,实际编码的时候,增加的),在开发稽核的时候,在一开始就设计好了)最后则是整个项目的部署上线,如何站在使用者的角度,以最简单的方式教会使用者使用新系统,如果使用者不能快速上手使用,那么是不是新系统的交互太过复杂。以及站在使用者的角度上,考虑新系统的部署,新系统的使用,新系统的维护,以及新系统的参数调优。
在进行这个项目之前,我只是负责编码实现,从来不会考虑为什么要这样实现,更多考虑的是如何实现,是使用for循环,还是使用while循环。经过这个项目我学到了很多。
这是迄今为止,我最完成的参与的项目了。
在项目中,体会最深的就是设计。整个三个半月中,抛去过年的半个月,那么实际上就是三个月,在减去打扰时间,真正的时间应该在2个月多一点。
但是实际上,结算和稽核的设计开发,设计评审就花费了差不多1个半月左右。实际编码只占全部的大概四分之一左右的时间。但是这个确实让我体会到了设计的重要性。(就像网上流传的一句话:选择有时候比努力更重要)当设计不合理的时候,开发出来的程序只会更加不合理。
在整个项目中,随着时间的进行,我自己的角色实际上也在发生变化。
一开始是旧系统的使用者,此时我要找到旧系统的痛点和不足。同时也要发现旧系统好的地方,在新系统中予以保留。
接着是设计者,我要在全面了解需求和现状的基础上,构思和设计新的解决方案,并准确简明的表达出来,这样别人才能认同你的观点,给与你支持。
设计者是细心的,设计者是全面的。设计不可能一蹴而就,不会一下子达到目的,需要不断的优化,反复考虑程序的细节,才能写出好的设计。
接着是开发者,当设计已定的时候,拿到设计,需要准确的使用编程语言实现设计。在开发阶段,我觉得更像是一个翻译,将设计图纸翻译为代码。那么如何保证翻译中的信达雅,就是在开发过程中需要考虑的了。
验证测试我只是修改出现的问题,实际测试我是没有参与的。(当时一边修改结算的bug,一边设计稽核)
最终是使用者。在发布阶段,需要忘记自己已经掌握的技能,假设自己是一个什么都不同的人,那么,如何教会一个什么都不懂的人使用新结算,部署新结算,就是在这个阶段中最核心的任务。
在项目中随着角色的变换,考虑问题的侧重点也在不断的变化。不过,总体而言,这个项目对我的意义很重要。
让我明白了一个项目从立项,到设计,到开发,到部署上线的全过程。亲身体验了这其中每一个过程需要进行的操作。