JAVA数据库同步系统2.0总结

原先V1.0的系统,是根据一个旧的系统修改出来的,公司的不少同事对我说是整了好几年,整烦了很多人,开始还在怀疑自己能否做好这个东西,后来果不出所料,因为原系统本来就有很多设计不合理的地方,又没有文档,改起来真是使出了吃奶的劲了,不过好处是把公司这套后台体系基本都搞明白了,原先的系统一个Service类,有个几十个方法,由于后来业务需求很多,重写Service吧方法实在是太多了,Ctrl+C其实也不是好办法,后来没办法的办法把一些业务逻辑整到Controller里去了,后期也是越改越乱,虽然能正常使用,但总是出问题,每次用这个东西,我都得陪着加班到21-22点钟,实在是太折磨人了。当我向老大提出,给1个月时间重构的时候,他也是不同意啊,后来好在我坚持了一下,趁他们在测试1.0系统的时候,我已经偷摸开始整2.0系统了,老大执拗不过去,让我去重构了。说实话,这个东西不重构,以后会害惨自己和后续其他同事,况且平时写Controller,Service写的都烦了,不仅是个表现的机会,还是个用来练手的好机会。

先简单说下使用java数据库同步的原理:不同于使用DB的备份恢复,这套系统由用户在前台配置需要同步的表格及其他配置后,就可以在前台页面,导出这些表格的数据压缩包了,导出后,将压缩包通过某种形式(U盘,网络传输,远程调用等)搞到导入方的机子上,导入方解析压缩包,同步表结构、表数据。


简单的说就是这样子,实际开发中Clob,Blob等字段都需要做特殊处理,很麻烦,因为通过 Sql更新这些字段,超过4K根本无法处理。我们的做法是将这些字段的内容通过输出流导出到txt文件中,在导入时再通过输入流的形式导入到数据库中。还有同步表结构,因为有自定义表单系统,所以写的存储过程中,通过这些自定义表单的系统表关联查询DB数据字典表,判断是否为增列,增表,最后根据不同情况生成建表语句、索引等;

好了核心算法总结到这里,1.0和2.0核心算法是相同的,不同的是修改了一些设计不合理的地方,以及一些隐藏的逻辑混乱的地方。

下图是重构后的包图:


在进行重构时,特意将系统设计的更通用,其实是有一些目的的,这个系统通用化后,不仅将适应公司的系统,还能更适应其他系统,绝对不和其他包产生任何依赖关系,能解耦的全部解耦,哪怕有冗余。将原来几十个方法的Service分解为多个遵循单一职责原则的类,比如导出时,如何抽取数据?通过SqlGenerator生成SQL,再由DataPicker数据提取器根据生成的SQL进行数据提取,记得去年看一本书,还提过SRP还是重中之重,今年自己确实有很深的感悟,SRP原则是面向对象的基础,不遵循的结果就像以前同事写的代码,Service中几十个方法,里边有生成sql的,有提取数据的,有负责加密的,当你需要重新实现他的接口时,天呐简直是噩梦啊,所有的全部要重新写,CTRL+C我都觉得累,而且这样做会有大量的冗余,试想一下为了重写几个方法,你要维护上百个方法。也许你会想到继承Service,只重写自己的方法,但是这样做,到最后你自己就晕了,光找个方法都要一会父类一会子类的来回找,上一个同事有部分代码就是这样写的,看的我都快抓狂了。而实际正确的做法,就是SRP原则所说的,每个类只负责一类事情,比如SQL生成器只负责生成SQL,其他一律不管,比如数据提取器,它只负责提取数据,今天我的提取器是通过db提取的,明天也许我可以从excel中提取,后天我可以通过xml提取等等,我只需要重写的是一个提取器。所以现在认为SRP原则真的是面向对象的基础。这是最大的感悟。

另外,个人认为SRP原则不同于其他OOP原则,比如开闭原则,不可能说完全遵守,那是不现实的,况且有很多设计模式是违反开闭原则的,不遵守的目的是为了不做“过度设计”。技术是用来服务业务的,该扩展的地方就应该抽象,而在不需要扩展的地方进行抽象,那就是脱裤子放屁,你留个扩展点,效率慢不说,这个扩展点你却永远用不到,这就是过度设计了。我对扩展点的理解就是,业务上此处有很大的变动空间,在各个业务中会有变化,那么就需要抓住这个变化点,将其进行抽象,抽象出一个接口或者是一个抽象类中的抽象方法。而能够准确找到这个变化点的人,不仅应该十分熟悉业务,还应熟悉设计的技术,否则这个系统只会越改越乱,到最后就改“死”了。

我认为一个好的系统,在当对他进行扩展时,你要做的应该只是实现接口就可以了,如果你需要去看源代码,那么只能说明当时设计考虑不充分,没有找到正确的扩展点。接口的作用不仅仅是一个契约,其实更重要的是接口能将“业务分离”出去,接口回调于变化点,我想这才是接口最大的意义吧!想当初,刚入行时,对于接口一直不明白他有什么存在的意义,仅仅是Mvc中,为了在Service中写一个方法,而机械的去定义出一个接口来。

当然我认为能运用大量设计模式的系统,一定是一个业务很复杂的系统,日常小的业务系统也许大部分都用不到这些,只要照着MVC写好就可以了。

下面贴上自己的重构时,开发日志,算是留作一个纪念:


15.8.17至8.21:
本周将标准规范的导出模式完成了重构开发。
导出模式的简要介绍:
1.导出前构造同步对象,构造的途径有配置文件、db、XML,通过三者的解析器获得同步对象,最后回调;(包括回调,只是修改其中一些对象)
2.导出时按照同步对象封装的属性,通过数据提取器进行数据提取,提取的过程中,调用SQL生成器生成导出SQL,接着提取器中使用SQL提取数据,提取数据后回调接口,给予修改导出数值的机会;
3、导出完成后,打压缩包,回调。


15.8.23:
今日主要增加了一个回调接口的实现类,callback.impl包下的NotStandardExportPostProcessor,该回调目前只实现了导出前方法回调,用处是用于非标准导出模式时候,
可以通过synch_t_setting中的filtercol列进行设置类表的筛选,因为设置类表在之前的版本中,会全部下发,不分OA和规范下发,这样会造成一个问题,主要问题出在OA下
发时,某人在进行下发时,会把除了自己以外的设置表数据下到地方去,而下的这个数据对应的业务表,可能根本在地方就不存在。所以这个回调方法会解决这个问题,目前支持
tableid和tasktypeid进行筛选,通过前台配置,每张设置表中的tableid和tasktypeid可能实际对应的物理列名不是tableid,所以设置时,需要知道实际列名对应tableid
的关系,按照列名1=tableId,列名2=tasktypeid这种格式进行设置。未来非标准规范下发,model等表格的下发,因为也需要筛选,所以未来只需要通过设置model的
同步对象filtercol属性,就可以做同样的处理了。


今日重构了导出前回调的接口,抽象出了一个父接口ExportPostProcessor,各模式子接口通过继承该接口,达到约束的目的,否则以后每一个导出模式的回调接口都需要开发
人员,自己去写,容易造成接口的不统一,实际应该是每个模式的接口只是名字不同,而方法签名都应该是相同的,才能保证各个导出模式中,统一的回调方式。


15.8.24:
重构一部分导入功能,解析这一块基本上完成了重构。导入解析时候,会有一个回调,只允许有一个实现类,因为导入中间表时候,有可能插入的不是导出的表,而是别的表,
因为可能有这种需求,所以解析时,必须要把插入中间表的这些数据替换成导入的表名。后续导入,同步对象好像也需要修改对象中的表名。
重构了之前写的同步解析管理器,该类的方法全部改为静态方法了,这个管理器有点像工厂类,没必要做实例化,所以搞成静态的更合适。加解密管理器也是如此处理;
重构了导出SQLGENERATOR部分代码,把原先写死的固定几张表永久导出,改为了读取配置文件,因为此处和业务关联性太强,没必要,搞成配置的很通用。
SynchContext改成了原型模式,防止并发问题,不过这一块需要后续跟进测试,大量类级变量使用在这几个类上,理论上改成原型可以避免这些问题。


15.8.25:
梳理了一遍旧版的导入算法,将原先混乱的获取同步对象步骤改为了从导出信息SynchInfo.txt中获取同步对象,在导入时,并不需要过多的同步对象信息,基本只需要一个
导入到的表名就够了,所以原则上不需要导出时再搞个SynchInfo出来,但是后续导入后,刷视图需要tableId,处理很麻烦,这个步骤应该在同步表结构的存储过程中去调用,
问题是这个不太确定,只好在synchinfo中把业务表的tableid一同导出来,并且在synchPO中加入了一个新的属性TableId,这个属性用处仅仅是为了刷视图方便,不过我觉得
更好的办法还是应该在同步表结构的存储过程中自行调用,因为在那里,很容易就能得到tableId,不用这么麻烦。
原版中,获取同步对象需要读取导入方的数据库、配置文件,这就导致原版导入必须保证一个顺序,任务类型、任务信息必须要提前于业务表导入,否则会找不到关联信息,造成
一个后果中间表synch_t_decryptdata中明明有这个表的数据,最后却无法导入。
正确的逻辑应该是根据导出方所有导出表直接进行导入,不应该和导入方的配置文件和数据库挂钩!
按照上面的逻辑,重构了部分解析代码、接口,所以解析后,必须获得导入的同步对象。需要注意的是,因为提供了回调,允许修改导入的表名和原表名不同,所以要注意同步对象
中,物理表属性要同时修改为回调后的值!
synchPO中新增属性tableId;
导出的抽象类方法,重构。endExport被修改为了final方法,禁止子类进行修改,同时该抽象类也因此只能适应数据包为zip包的处理,改成final是因为提供了标准的synchinfo
文件,不管子类如何修改,这个文件必须有,也必须按照固定格式,否则导入时候,无法按照通用规则解析。除非同时实现新的导出和导入解析接口。


15.8.26:
重构了导出部分算法,将几个方法改成了final,固定由基类实现这几个方法,这几个方法将在zip流中,总是第一个导出synchInfo文件,导入时会按照写入流的顺序进行解析。
重构了部分导入解析算法,使得每次解析时,总是第一个解析synchInfo.txt文件!同步顺序也应该记录在该文件中,但是目前还没有。需要后续修改


15.8.27
重构synchContext,将原先static的根据接口名返回实现类的一些方法,重新梳理到了CallbackManager中,这个管理器通过beanId、接口名、配置文件key返回相应的bean,
供调用者使用,做了集中管理。去掉各个组件中各自调用回调的方法,统一使用回调管理器进行获取bean,并按照一定逻辑,正常抛出异常。


15.8.30
将sqlgenerator从datapicker包中分离了出来,以前认为这个包放在提取器中最合适,实际上导入器也需要生成SQL,还是分离出来更清晰。导入和导出时sqlgenerator中的子包


在synchPO中新增了orignalPhysDBName属性,原因:
回溯1.0版本的导入机制:解析时,分别解析业务数据和大文本数据,分别插入synch_t_decryptdata和synch_t_blobclob表
中,在导入时,也是先将一张表的业务数据插入到表中,有blob和clob列的先插入empty_blob()或clob()代替,待全部业务数据插入完后,再去查synch_t_blobclob表,查到
这个表的blob、clob数据,再统一插入;
在2.0版本中,由于可能存在导出的表和导入的表不是同一张表的需求,这个可变性是很强的,所以必须要将这一块分离出去,否则你每次都要修改源代码,而如果分离出去,抽象
出一个接口,只需要在自己的实现类中判断表名直接修改表名就可以了。这个区别很大,不这么做的话需要开发人员读懂源代码才可能找到正确的修改地方,这么做告诉你实现哪个
接口,去实现就好了。这个和加入属性有什么关系,是因为导入机制进行了修改,原先A表导出,导入A表。现在A表导出,导入B表,最开始思路是同步时,将所有数据(包括业务
数据和大文本数据)全部直接插到B表,然后调用存储过程,负责B表搬到A表,但问题是这样的话,大文本数据会折腾2次,所以要改成:将B表所有业务数据导入完成后(还是空clob和
blob),直接调用存储过程,数据搬到A表后,再将大文本数据插入A表。
问题来了,我在导入解析时,由于回调可以修改表名,已经将A表的大文本数据插到synch_t_blobclob改为了B表名,如果对解析这块做特殊处理,势必影响代码的通用性,所以
只能在导入大文本数据时动手脚,在导入它们时会根据表名查询synch_t_blobclob,所以查询时加个or条件,即表名=A表 OR 表名=B表,问题解决~保证了通用性,逻辑依然
清晰。


15.8.31~9.1
加入了开发模式的概念,在开发模式下,导入只进行到解析完成,也就是说只会插入中间表数据,synch_t_importsql,synch_t_decryptdata,synch_t_blobclob表,
方便查询错误,不会插入任何数据到其他表格中。同样,也会导致导入数据时的回调不会回调。同样的导出时,如果为开发模式,会将导出SQL记录在synch_t_exportsql表中,
也不会记录导出日志信息,很方便。每次导入导出时,自动清除这些表!
重构完成了数据导入器。


15.9.4
今天导入成功一部分,但是CLOB等字段导入为空,将之前的一些想法又改了一下。改动主要在存储过程上
首先存储过程中,同步时候不再进行更新语句,而是先查询临时表中的数据是否在业务表中实际存在,如果存在,删除提交,之后一次性全部插入到表中。


15.9.6
今日首次全部数据导入成功!同时修改了一些原来的BUG,并对导出部分进行了修改;
1、V1.0导出时,只会导出非BLOB,CLOB列信息;相应的导入时,会根据导出的列信息(业务数据文件夹中的文件第一行)进行导入SQL生成,此时导入方根本不知道这些列,导入语句
最后也不会插入empty_clob()和empty_blob()到表中,而到最后插入这些数据时,通过Jdbc的resultset.getClob()时,获得的是一个null对象,直接引起后续失败!!发现
这个问题后,对应的解决办法也想好了,但是一直在这个问题上考虑:到底是在导出方将所有列信息全部导出还是在导入方查询系统表自己拼上这些列?最后我选择了在前者上,因为
导出方的表结构是基础的,导入方必须根据导出方结构去生成导入SQL,而不应该根据自身去生成,有可能一种情况,导出方表4列,其中0列CLOB,导入方该表5列,其中1列CLOB,那么
如果按照后者方案,会发生导入方自己把clob列更新成了empty_clob(),这就是问题。
2、接下来这块是碰到的问题,连接问题,原来在更新clob时,会通过jdbcdaoTemplate获取连接,一直以为和mybatis里获得的连接不是同一个,结果是错的,对比hashcode发现,实际
是一个连接,底层连接池会根据线程固定分配给同一个数据库连接,所以这块问题多虑了。
3、9月4日碰到的更新问题,SQL写错了导致更新不成功,现在改成了更新。不再删除。另外存储过程中禁止写COMMIT,移除了;
4、较1.0版本,事务全部纳入SPRING管理,不再有脱离SPRING事务管理的代码,只要有错误抛出到service层,直接回滚全部导入数据;
5、对FILTER_COL添加了2个支持选项,支持CSID,TASKID.CSID为当前任务类型下的业务表,这些表所关联的引用表ID。TASKID通过DOCID获取任务ID,前提也需要为OA下发,
当然这些选项均在NotStandardExportPostProcessorPartSettingData类中,只有在导出模式中重写并回调了该接口才会按照规则执行。


15.9.7
1.对主键是否为NULL进行了验证,在导入导出方均进行了验证,如果只设置了STATUS或者未设置任何主键均报错,如果多个主键中设置了STATUS,自动跳过,在同步数据的存储过程中,
直接排除了STATUS主键!原因写N遍了,不写了。看同步数据的存储过程注解
2.之前MODEL,FACTOR涉及部分数据导出,导致了每次下发必须要永久下发!现在由于FILTER_COL,设置表也需要部分下发,所以设置表中,只要涉及部分数据下发的,全部要永久下发。
总结是,只要某个表需要下发部分数据,那么这张表就必须每次都要按照非增量形式进行下发!否则就有可能出现数据不全的现象。原因举例:A,B两人分别同时增加了A1表和B1表
的设置数据在C1设置表中,某一时间点A人下发(非规范下发),此人当然只会下发自己C1表中涉及A1表的关联数据,同时记录了一个时间戳;当B在之后进行下发时,如果按照原
逻辑,C1表中的涉及B1表的数据的时间戳还是之前的时间点,导致B在下发时,直接取了A人在导出日志中记录的时间戳,此时B人没有把B1在C1表中关联数据下发下去!
3.设置表导出filter_col,增加了对spfid的支持,为了保证这个包不依赖其他包,将spf包中的IAssignProjMapper的语句复制到了callbackMapper中
4.更新和插入是表级锁,删除是行级锁。
5.用电脑记录了一下导入时间,大概2W多条数据,上报导入大概需要3分钟时间,电脑比较慢且在开着后台打印日志的情况下。
6.今日完成了目前所有的导出模式的开发,整体速度很快,大概有一多半的扩展是通过实现接口中完成,完成后需要在synchConstants中加入相应的配置常量KEY,如果是增加APPID
还需要在一些组件中加入对APPID的条件调用,导出模式中80%代码是相同的,本应该将startexport方法放到抽象类中的,但是问题在抽象类中无法注入mapper等对象,另外每次要
增加对应的配置文件中的选项,太麻烦了,如果有特殊处理,还要去组件中用switch加入,还是要修改源代码,无法完全实现开闭原则,有待考虑。
7.明日写一个fascade的接口,通过对应的方法调用不同导出模式的方法
8.刷新分区的存储过程,在p#dict_t_appregister表时,一会报错,没解决。
9.formula_t_formuladef表在同步数据进行更新时也会报错,列说明无效,暂时将同步数据的存储过程恢复成delete/insert形式,避开这个问题,但是还需要追一下
10.上报这一块需要增加一个验证,根据省份编码判断目标省份分区是否为自己的上级省份。因为1.0版本中,由于实施人员在OA里配置错误,导致OA调用错误的方法,
原本是上报结果却走了下发流程,找我麻烦。上报下发都应该有这个机制,在fascade中实现即可!
11.今日重构了一部分代码,将factory包删除,有些画蛇添足,没有意义的工厂类。


15.9.8
1.今天修改了之前的同步大文本数据的机制,原先是先插业务数据---》调用同步数据存储过程---》向原表插入大文本数据,结果这里会导致formuladef表触发器出现问题,因为在同
步存储过程中,使用update,将大文本数据搞成empty_clob(),更新进去,触发器出错,原先考虑到可能需要弄2次大文本,影响效率,而实际是这样做不会造成什么效率问题。所
以现在改成了插业务数据--->插大文本数据--->调用数据同步存储过程,只是顺序和插大文本数据的表名改变了而已,调用这个部分也是分离出去在回调接口里调用的,所以改起来
很方便;
2.昨日存储过程错误的跟踪,p#dict_t_appregister表创建语句有问题,分区是错误的。formuladef表的机制已经修改了,如1所述;
3.外观模式实现完成,并在下发上报时对省份编码进行了验证,如果错误直接抛出异常;
4.重构了大部分前台配置界面


15.9.9
1.页面基本重构完成。
2.测试规范下发和SPF下发,规范下发可以调通,SPF因为APPID的问题,XCH会根据APPID找SPF域的IP地址和端口,我本机暂时测不了。但应该没问题,代码一样。
3.重构和XCH系统的交互代码。简易流程:在导出模式的回调中(目前是BGT规范下发和SPF下发),打包调用XCH发送请求。包路径在callback.impl中。当XCH进行回调时,回调
callback.remote中的各个子handler,各handler里插入bgt_t_timeplan表任务计划,每10s由callback.remote.quartz包中的synch2TaskPlan类进行读取,读取到后
再调用导入。
4.重构了大部分的OA交互代码,需要和OA联调。OA交互中,发送时需要返回给OA系统上传文件包的guid,如果为OA系统把所有回调的接口从void改成String代价太大,而且不通
用,之后建了一张中间表bgt_T_exportfile,存放fileguid和docid对应关系,回调到上传文件包时保存,待返回OA之前,取值后将记录在删除


15.9.10
1.在synchClassUtil中加入了一个对jar包的实现。原因:原先启动系统时,通过文件File的形式获取包,得到class文件再动态加载的,但是现在weblogic部署war包时,不是
将war包解压到目录下这种形式进行的,导致启动时无法通过war包获得class
如获得的文件路径为:D:/Oracle/Middleware/user_projects/domains/exp_domain/servers/AdminServer/tmp/_WL_user/_appsdir_bgt_war/4btz9w/war/
WEB-INF/lib/_wl_cls_gen.jar!/com/tjhq/synch2/callback,这是无法访问到的,现在通过判断这个路径如果包含.jar字符,通过JarFile类获取到包中的内容。


15.9.14
1.增加了对上传文件包进行解析导入的支持,通过获取当前用户的分区切换分区,该方式进行直接导入,未将其插入bgt_t_timeplan表中;
2.事务问题控制不住的,在数据库中DML之后,如果进行DDL操作,会导致自动提交,当前的导入形式是通过创建临时表,插入数据,删除临时表进行,无法控制住事务


15.9.15
1.增加了一键设置的功能,通过dict_synchronize_set存储过程,自动刷新所有已设置表的主键及filtercol属性(只刷当前表有SPFID,TASKID,TASKTYPEID,CSID,TABLEID的列)


你可能感兴趣的:(java,数据库)