工作学习总结一(2019)

前言
整理了相关工作上的内容,觉得比较有趣的设计思路和实现方式,用于记录一下。
 
一.动态指标配置
1.1 需求描述:
为了使得客户对于精准化营销手段,有着一系列直观的感受,这边需要有一个会员看板,显示各个门店下会员的动态详情。会员看板有着不同的图形,而每一个图形要求动态数据拉取。概念图如下:
 
1.2 需求分析
在需求分析之前,笔者要BB。这是一年多前刚刚进公司写的,写了两个多星期,写到差点离职,其实细细的扣细节,还是有很多代码量的。好的,闲话不多说,直接开始吧。
 
首先,这些指标一共分为5大类,分别为基础指标(上面的白板),漏斗指标,饼图指标,环形图指标,折线图指标,但是考虑到业务拓展,难保后续会有其他图形指标。那么这边可以基本确认的是,我们会使用工厂模式。
然而,各个指标之间,还是有着一些区别的
 
基础指标:基础指标比较复杂的地方在于,有一些基础指标他的显示其实本质上是多个基础指标而形成(四则运算)的复合指标
漏斗指标:漏斗指标相对来说是比较复杂的,因为他的本质上是由8块内外指标复合而成,而每一块指标都会去请求不同的数据,整个的漏斗指标描述的是从访客到忠实有效会员的递进,呈现一个T字形的漏斗
饼图指标:饼图指标由三个会员数据汇合而成,需要展示对应的性质会员的数量与占比。同时饼图指标的数据很来源很特殊,是从数据中台抽取而来,是进行跨库访问的。(然而比较开心的是,这个我不用管,因为平台封装了)
环形指标:环形指标在这里更多的是显示占比关系,然而很特殊的客制化需求是,环形指标的占比内容显示永远都是n层/对外层。并不支持有多个环的时候,n层和n+1层之间的对比
折线指标:折线指标的横坐标暂定为当前月份以及前推六个月份,但是这里又有一个很特殊的东西,他有一根线的每一个纵坐标的数据取值是一年内的,也就是说我2020年2月份的数据是由2019年2月到2020年2月这边汇总的。不得不说的是这个指标我做的相对来说比较客制化
 
在分析完各个指标后,他们又提出了一个需求,我需要传参,要支持数据过滤。(诸如增加店铺的数据过滤)
 
最后必须要强调的是,每个指标都是动态配置出来的,而且一经配置,后续不需要修改。
 
1.3实现概览
首先我们先解决动态配置问题,这里照搬了mysql语法分析树的概念实现。其实每一个指标,抛开那些装饰的部分,和各自的个性化显示,在进行拆分到最细粒度都是一条SQL。
这点其实很重要,没有这个其实写不下去的
 
看一下数据库表结构数据设计
 
这是对应的主表,其中parameter指的是各个指标个性化的东西。(这一步也非常重要,因为它实现了将实现了将差异化的东西放在了一起)
而其中的dsc_id指的是跨数据库ID,用于之前说的那个跨数据库范围,从数据中台抽取数据
然后高潮来了,其中的 factual_logic_po(事实逻辑) 可以理解为每个指标所对应的sql的table
statistics_index 指的是这个table的field
condtion 指的是where条件
dimension 指的是group by分组条件
statistics_pattern 指的是对应分组的聚合函数
因此有了这些东西之后,我们的sql语句就会变成 select sum(field) from table where 1 = 1 group by field 一个简单的sql就这么组装成功了
 
 
然后是子表,之前也有分析过,除了基础指标之外,其他的任意指标其实本质上都是复合指标。所以这边有子表去组装整个大的指标
表结构如下
子表其实很简单,因为在我的设计中,它只存放了各个id,它只是一种引用,而并非具体实现
其中的seq与horizontal_seq指的是各个指标的排列顺序。是的单个seq撑不起来的,非要整两个
 
 
这是表结构的概览,这里需要讲两点,一点是paramter自定义参数,一点是condtion
 
单独抽开来讲这两个,我认为是很有必要的,因为这两者之间本质上都是将个性化的东西抽象出来,一个是将不同的sql的where条件抽象,一个是将不同的指标的个性化需求抽象
 
看一下数据结构
 
这个是condtion的结构,其中field指的是字段(这个字段指的是我PO里面的字段,而非数据库字段),operation指的是操作符合,value指的是值,conOperation指的是连接符,placeHolderSet指的是 是否用占位符(就是sql拼接的时候,用?还是直接拼接值)
 
然后是个性化的paramter参数
 
其中,符合指标如下:
@符号表示的是 之后的是作为一个参数,而非具体值。(笔者在之前做过一个数据模板动态导入的,做到后面才发现5*4有的时候 这个5很难描述到底是一个id值(指的是一个具体对象)还是一个数值,所以后续都加了@符号用于标识)
 
环形图,其中的rate指的就是第N层对最外层的占比
 
折线图,其中的beforeMonths指的是前推多少个月,singleMonthRange指的是单个数据的纵向深度是12个月(这个诡异的需求)
 
以上其实全是数据库表结构的,现在我们看一下具体代码实现。当然这边只有概览,不会讲细节。
其实从结构导向来看,我的各个指标只需要返回给前端对应的数据结构而已
 
 
之前也说了,本质上是工程模式
 
看一下类图:
其中 IReportIndexCalculator 定义抽象化对外行为,分别为init(初始化)和 calculator(计算)
AbstractCalculator中定义各个对于指标来说的抽象行为和子类模板(钩子方法),具体类的方法我想了想还是没贴,因为感觉不算特别重要。
但是需要说明的是 AbstractCalculator的方法其实就是抽象类的抽象方法,针对于那些共性的东西 进行统一化化处理,比如说SQL的组装,上下文的组装。然后对应的个性化的东西,比如说不同的返回前端的数据结构我们需要在子类中具体实现
 
然后工厂类创建指标代码如下:
同时这边我也定义了相关的枚举类,用于标记属性
然后就是一个数据结构了,毕竟之前也说过,不管我内部逻辑怎么复杂 我最终返回给前端的是一个个数据结构。同样的 我上下文也是不同的数据结构
 
这是返回给前端的数据结构,事实上在抽象接口中,我只是返回一个 ReportItem就可以了。这也是抽象的一种运用( 编译期与运行期的差异
 
当然这些都不难,比较繁琐的是 实际业务中部分指标是复合指标所以我们代码里有会有一个递归查询组装的过程
代码如下
在init初始化context上下文过程中,我发现如果有复合指标我就会循环的往下执行,并且组装数据
 
其实到了这步基本上的都好了,非要再往下的话 就是sql怎么组装呀,个性化展示怎么整呀,其实这些都不重要。这个每个人的写法可能都不一样。这里不进行展开探讨
 
对了讲一个小玩意,我们知道java有一个很恶心的东西 ArrayList<FatherA>和ArrayList<Children>是两个不同的泛型(好像叫啥 泛型的擦除 )
 
然后对于这个我是这么处理的
通过这种传引用对象的功能 将 DoughnutReportItem转化成了 ReportItem。因为我的代码是抽象的,针对于 ReportItem这个基类实现的
 
1.4优化和思考
在写完发现一个问题,就是有点慢。虽然前段已经分开来多个请求了,但是对于一些复杂指标还是表较慢。所以这里做了两个优化,分别是异步和缓存
 
异步是因为我可以分开来请求,缓存是因为在实际过程中,发现多个复合指标用到了同一个基础指标,而这个基础指标我就不用做多次数据库load了
 
代码如下:
RPC异步  这个异步是放在RPC异步中的,本质上是调用的多线程的Future异步
ps:这个必须要提醒一点,dubbo rpc异步有一个深坑 就是dubbo异步调用传递性导致嵌套调用返回null值的bug,可以参考这篇博客
https://www.jianshu.com/p/33cfdf396153
 
然后关于指标的命中和缓存如下:这边大概是会,从内存对象里去load相同的指标,用map内存对象做接收
 
更多的优化:我的异步是在controller层面的,其实复合内部也可以异步,这个还没做。
主要是漏斗图的8个指标转起来比较慢,其他的都还好
 
 
 
二.抽象业务单据以及应用
 
1.1需求描述
我现在有一堆业务单据,他们有着相同的类似的地方,但又不全是相同的。
举例来说,我对于这些单据都有明细的录入,但是不同的明细录入会有不同的特定操作,同时类业务单据都有Excel导入.
 
1.2需求分析
实际上,论到OOP的三大特性,我们耳熟能详。但是真正有体会的却是在这一年中,其中感触最深的还是抽象吧。(虽然继承也有很多想法)
 一个系统对于抽象的把握程度,一定上能够看出一个系统的健壮性,代码的优雅程度。同样的,我觉得抽象是一个考虑业务把握度比较高的要求。站在全局的角度去思考,哪些东西他们的共性是哪些。而这些共性的东西是否会随着业务的发展而发生变更?我是否能根据这些共性的属性来进行一些基准的操作
以上说法,可能会显得比较抽象。那我们看一些列子,以需求描述中的业务单据为主
 
1.3实现概览
针对于不同的业务单据,笔者将其公共的属性抽象化出来,实现一个基础PO。然后子类去继承这些共有的属性。如下图UML
 
 
可以看的出来的是,这里有两个抽象一个是BeanObject。这个抽象类,是平台级别的抽象,代表的是所有Bean对象的抽象,本身用于描述一个对象的实体。
然后BillBasePO是一个基础业务抽象,描述的是我这一系列单据的共有属于。这个抽象类不能过于简单,因为过于简单的抽象类代表着抽象化程度不高。也不能过于复杂,因为不能含有更多的个性化东西,尤其是随着后续业务的变更,代码的扩展。发现有一个类突然出现,他明明也算是业务单据,但是这个类却和最基础的BillBasePo中的属性不同(他没有BillBasePO中的属性)。那么他就只能游离于规则之外了。游离于规则之外就是代表着他不能享受抽象公共方法带来的红利
 
我所认为的抽象不仅仅只是说将部分共有的属性抽象化出来,我所更加偏向于这些共有的属性它所描述的抽象对象,有着自己的抽象行为。(这只是个人理解,纯粹的公共属性,而不带有公共行为在我看来是没有很好的把握业务的核心,当然这还是我个人看法)
 
由于我是进销存单据,所以我对于BillBasePO的公共属性定义如下:订单号 业务日期 制单日期 制单人编号 制单人姓名 汇总数 汇总金额 状态 备注
很幸运的是,表结构也都是我定义的,所以在现有规则内,我都是加了这些字段。
 
但是很明显一个业务单据的描述,它不可能仅仅只有这一些基础的字段,他必然会含有很多个性化的特定的东西,所以这边才会有很多业务BasePO和具体的PO。
同样的我的明细也是一个类似于这样的结构,这里不做过多累述。
 
那么在实现了业务单据的抽象化过程中,我定义了许多抽象化行为:
公共的单据明细录入,我们开分析一下这段代码
1.第一步通过工具类获取当前单据的明细是否存在数据
2.通过构造器传入对象,通过装饰器装饰成特性化业务对象
3.做数据结构保存
4.做主表数据汇总
5.做主表数据保存
而实际上这些动作并没有和任务业务单据做强制性关联,所以他的适用性很高,所有的其他单据我都是这么写的
 
同样的,我们看下面一段代码
这段代码我写的注释比较清晰,我不做过多说明。其实正是因为有了抽象的单据,所以我也在可以在导入的明细中定一个父类处理这些公共的行为。
当然还有其他方面的抽象,比如说打印
我就两行代码 一个组装数据 一个转化为Map对象。
其实这边还有很多,就不一一例举了。包括库存的控制,库存流水的控制。而这一些都是抽象行为。(库存这块实际我是组装了一个库存对象来进行操作的)
先有了抽象属性,之后才会有这些抽象方法。
 
1.4优化和思考
来来来,我们举出几个错误的例子
 
之前在定义这些PO的时候,针对的是不同业务场景,本意上是不同的业务场景所需要的PO字段属性不一样。但是在后续拓展的过程中,我发现有些PO就是不断要加字段,以妥协于业务。由于层级过深,往往不好使用。
实际上,一个业务场景针对于一个PO很正常,甚至是一个很好地管理。但是随着业务的拓展,会发现有越来越多的PO,然后又由于当时的规划可能只是只适用于当时的场景,那么冗余的PO就越来越多。
这个时候就不可避免的要做业务方面的整合。(信我,实际生产环境中,代码业务方面的review很少见的)
 
所以,针对不同业务场景,而且后续还有比较大的拓展的,(尤其是单个对象,适用不同业务场景的)个人不太建议做这样的抽象划分。
 
我理解的业务抽象的最大意义在于把多个不同的对象,抽取其共有属性和行为,而达到代码的复用。当然个性化的东西可以在子类或者通过组合进行实习(组合高于继承)
 
事实上,这玩意难也难在第一步,怎么样去做业务抽象,哪些属性和行为可以抽象出来。这些确确实实需要一些业务开发经验
 
关于继承
继承本身是面向对象OOP三大特性之一。简单基础的先不提,感触比较深的是,继承是调用super的方法,同时实现子类个性化的东西
这里必须要强调的是,实现的仅仅是个性化的东西。
 
笔者之前在做客制化版本代码的时候,个性化的东西 大量引用到了继承。
看下面一个列子:
这个是子类方法,调用父类的 payInfosBuild然后再父类中放了一个钩子,子类去做了实现 receiptPayInfoBuild
 
强调继承是想说明,子类中需要实现的是个性的化的实现。如果父类中方法一个不好分离。就要重构将子类有可能需要重写的内容单独抽取出来,方便子类实现
不能直接去图方便,把父类的方法抄过来,改一下。因为父类方法是有可能发生变化的,到时候你就把公共的方法给覆盖了
 
但是这样做会引起一个问题,就是父类方法中,会有很多细碎的方法,全是用来方便子类实现的
这个的处理方法就是,业务整合重构,将该抽象化的个性化的东西 抽象成一个对应的方法。当然这个是需要一定的时间的
 
 
 
三.事件驱动模型
1.1需求描述
我现在的退款有一套很长的链路,包括线上门店,线下商城。现在我不管其他系统的申请单怎么样。我要求在中端系统(中台)这边的单据都是一样的。另外点了一个确认,后续单据自动生成,自动退款。
 
1.2需求分析
这里有两个困难点,第一个是业务规划整理,第二个如何用状态去做驱动。
业务规划的难点在于 如何去收口业务流程。这里的业务流程分为以下几个:(当时我画的)
然后,业务这块的收束,我们的老大当时做了一个业务流程图的。(然后我找不到了)
 
在确认了业务流程之后,摆在面前的就是如何实现。如何实现点了一个确认,后续单据自动生成。
之前这块我的想法是在点击A单据的操作后,去在服务内调用B单据的一个服务,以此形成一个链状反应。但是我们老大觉得这样不合适,业务耦合性过大。而且当时设计的时候,是以状态变更作为驱动。
最后的解决方案就是,定义业务单据代理,定义状态变更代理,定义监听器,监听单据状态的变更最终去驱动这些单子
其实这个本质上是Java的事件机制
 
1.3实现概览
这里我抽象了一下 概念图,大致如下
 
效果如下:
 
来解析一下代码,
 
首先是创建业务代理类,然后创建对应的业务对象事见,由代理类去驱动业务对象事件。
 
然后看一下 proxy的代码
 
在服务启动的时候,spring注入监听器,对于这个A业务单据 ,注入B单据的监听,同时B单据的事件。
 
注册本身比较简单 只不过放到一个map容器中。然后回到刚刚的业务代码,调用了publisher的状态变更方法
 
fireBusinessStatusChanged方法中,我写了一个Spingboot的异步方法来进行调用 下游单据的事件
最终调用的事件监听的,通过工厂类获取到业务单据对象 在调用业务单据对象的具体方法
doOperation 就是所有业务单据的下游操作。
 
自此整个状态驱动完成,简单的来说 定义一个抽象业务单据 定义一个抽象单据行为。而在此其中起到串联的是event事件,继承的是java的 EventObject事件对象
再定义一个事件分发类,映射事件对象和监听器的关系,最终调用的是工厂类,由 event.getSource()来进行业务区分,进行驱动业务单据的Proxy
 
1.4思考和优化
其实这整个框架并不是我搭建的,是由我领导搭建起来的。但是我感觉其中的设计还是很精妙的,照着整个的思路,我后续又在自己的工程内搭建了一套。
但是这样做,我感觉是有一些缺点的,
1.业务单据之间的流转是固定的,A单据流转到B单据或者C单据只能通过业务逻辑做规划和判断。
2.相关代码还是有一定侵入量的,我需要再bean容器初始化的时候,做塞值。(事实上这个还好,代码的侵入量不大)
 
关于第一点,我之前的处理方案如下:
在分支中,实现了一个工厂类
工厂类决定分支指向
 
然后再具体的实现中,再决定分支流向
这是我实际生产环境中的相关代码,他通过业务逻辑实现类 单据流程的扭转。
 
那么我们引入一个责任链的概念。
责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递:
 
在实际场景中,财务审批就是一个责任链模式。假设某个员工需要报销一笔费用,审核者可以分为:
* Manager:只能审核1000元以下的报销;
* Director:只能审核10000元以下的报销;
* CEO:可以审核任意额度。
用责任链模式设计此报销流程时,每个审核者只关心自己责任范围内的请求,并且处理它。对于超出自己责任范围的,扔给下一个审核者处理,这样,将来继续添加审核者的时候,不用改动现有逻辑。
 
责任链中,有以下几个角色。
1.Handler处理类,可能会有很多个Handler类处理同一个请求,然后根据业务限制将请求自己处理或者丢给下一个请求(对应Proxy)
2.Chain链路,简单理解为这是一个Handler的有序容器,然后迭代内部的Handlers,依次执行
3.Request,请求对象,用来传送对象。(对应BaseBillEvent)
 
同样的责任链有很多变种,诸如手工调用。
当然责任链还有一种比较复杂的玩法,也就是每一个Handler都去处理Request。而这种被称为拦截器(Interceptor)或者过滤器(Filter)。他的本意不在于找到一个Handler处理Request。而是在于每一个Handler均去处理Request
比如:日志写入。权限校验
 
 
这边基本结束了,而我觉得引入责任链的这个概念,能够更好的处理这种复杂业务流程。而不是每一个都是在业务代码内回调处理逻辑。
 
 
 
四.复杂报表实现
1.1需求描述
我现在有一张报表要看,但是数据取值很麻烦,涉及的业务单据10多张,每个业务单据之间还有很多逻辑运算,然后这些业务单据数据也很大。我现在要求速度要快,查询出来的数据要实时。
 
1.2需求分析
这一类问题,统称为报表展示问题。其实之前的做法就是写视图,通过视图拼接业务单据来实现功能。然后必然的这样的瓶颈在于SQL。
之后,为了提高SQL,我们会对SQL做很多优化。Explain分析,建立相关索引。但是这个终究是有瓶颈的,取决于业务体量。
在之后,我的一位同事用到了oracle的物化视图,物化视图的本质其实是一个物理表。他存储远程表的数据,也称之为快照。然后既然本质是物理表,那么必然要提到他的Refresh,这里分为On Demand和On Commit两种
但是,这次在别人的指点下,我并没有这么去做。换了一个思路,我用表去实现。其实之前也有写过这样的。包括库存流水表和存货结转表。但是实话实说,我之前还真没想到动态报表用物理表去实现。
 
好的,既然确定了一个大概的思路之后,我们现在想着如何去实现他。那么我第一秒反应过来的是在任何业务单据发生变化的时候,就往里面插入。
然后,这个由于业务场景很多(我实际上做的是一个进销存报表),那么每一张业务发生变化时,我需要考虑那个节点对应的性能问题。但是我又不希望,这个插入数据表这个动作影响我的原本的业务逻辑。
所以,我就想写成异步方法。那么问题又来了,我这张报表对数据准确性要求还是很高的,万一我异步方法失败了怎么办(异步方法报错,主线程还是往下跑的)。那么我在改回同步方法,但是这样又慢,而且我不太希望在做这个业务操作的时候,去驱动其他业务关联性不大的动作(比如入库,他的本质上就是一个入库仓增加库存,同时改一个状态。但是他还有很多其他的衍生需求,比如说回写上游单据,增加库存流水,修改库存数。有些方法我们避免不了,但是我们能避免就尽可能的规避)
那么,在继续考虑异步方法之后,我就要考虑数据丢失这类问题,然后我需要做一个类似事务补偿的东西。当到了这一步的时候,我发现了另外一个很尴尬的问题,那就是由于并发问题,会导致我频繁的update和insert数据。
其实insert还好,他不会锁行,但是update就不一样了,在高并发场景下,它就很容易出事。当到了这一步的时候,我感觉掉进了一个深坑里面。
 
然后我们老大,给了一条明路,最后的解决方案是半物理表加上半动态数据组装,当然还有事务补偿机制。
最终敲定下来的方案是,我会在每天的某个时间点,定时的去批量修改数据。这样的话就不会对这张表进行反复的磁盘IO交互,至少一次性刷一堆数据的话,还是一个顺序IO。然后每次当查询的时候,若是查询当天的数据,再以内存的形式拼接上去。
因为,实际应用场景里面,客户不可能天天看报表的。
 
那么这样的话,相当于每次查询都是查一张物理表。当然这个物理表后续会变得很大,所以我们会对于查询的相关维度预设分区键。然后每天的“日结”,我们会做表数据的插入。同时,也在这个地方做了数据校验,检查当天是否存在丢失数据场景,并重新推送
 
其实代码设计虽然说比较复杂吧,但是思路确实很重要。我们直接看代码显示吧。
 
1.3实现概览
首先是插入数据这块,代码如下
所谓日结的业务概念在于,每家店铺在每天结束营业的时候,点一下,看看今天收了多少钱。这个时候我们异步生成报表数据,那就是这个店铺当天的数据了(后来他们又提出了诡异的指定日期日结,因为忘记点了)
 
内部方法如下:
这里的逻辑,本质上我复用的是我写的重算进销存数据,备注比较清晰,不详细说明,看一下核心数据组装类
这个for循环的条件还是比较新奇的,我是根据日期来进行一天天累加的。这就是,在数据load完成做的数据拼接。最后调用数据保存
 
插入数据这块完成之后,我们再看一下,动态数据这块是怎么展示的。(当天数据内存拼接)
 
本质上调用的也是重算代码,也就是刚刚上面贴的。这里面又涉及到点多态的思想,对外的接口可以很多,但是核心逻辑处理类就是一个,这样一个收的口子是一个之后,代码就很容易排查问题,而且不会有很多不必要的冗余代码
 
再看一下,数据补偿机制这块
     
 
核心代码在这块,当时其实我是想写日志的,后来我发现异步方法写日志,拿不到返回值,写日志感觉怪怪的。主要也是这个重推不算特别急,所以就做了推送,但是没有重推次数限制和日志记录。这个后续要做修改的,如果后续对这个有要求的话
 
1.4思考和优化
很想说一句话,有的时候我们不是要把简单的问题复杂化,而是把复杂的问题简单化。之前无论是视图也好,物化视图也好,或者每个动态节点插入数据也好。其实写起来都比较复杂。
而这样一种通过业务也好,定时任务也罢。把数据拉到表里,查询出来的数据必然是性能远高于视图。
同样的,我们不需要考虑过多的并发问题,数据库表频繁写入问题。
不过这样开发成本比较大,像简单的报表,用视图挺好的,估计整一个报表平台,实施人员自己写写SQL也是可以的。
话又说回来了,要不是这张报表数据取值逻辑复杂,数据体量大,要求实时,也要求快,我们也不会这么设计。其实看得业务场景的,不同业务场景针对于不同设计。
 
 
 
 
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值