记一次优化
条目 :代指项目中的被优化的主要单元;
节点 :一个条目的整个生命周期会存在多个节点;
被优化的是?
项目针对每个条目设置了若干节点,对节点的要求进行格式化管理,根据格式化的内容,在条目各个节点设置提醒,以此推进各个节点的达成。
具体节点不做展示,大概分为:
- 初期异常
- 多种逾期
- 验收、签收要求
- 回访要求
- 溢出要求
- 自定义要求
若在条目各个业务点变动时处理节点,节点的处理效率很高,但是对代码的侵入性特别大,而且条目细节繁多,有些标签逻辑会在条目很多位置发生变动,非常容易出错,且工作量很大;所以决定使用任务定时扫描满足逻辑条件的条目,节点的逻辑划分为条目维度的标签,由条目数据变动时刷新条目数据的任务存放到es中,统一进行节点的处理;(包括触发、闭环、动态刷新、标签刷新等)
主要定时任务

第一版方案
数据库:由一张主表 +各节点子表组成,主表存储子表的id和节点的类型,通过该id和类型判断该记录是哪一个条目;第一版中各项节点共用的字段如:“说明”、“发生时间”等字段放在了各项子表中,而它们本应该是主表的宇段。
数据查询:数据的查询都是直接查询数据库,包括但不限于提醒列表、导出提醒、列表红点数量。
条目标签:第一版做的节点触发和闭环逻辑比较简单,每分钟将有改动的条目标签逻辑更新到es中。
节点数量:2个。
待生成节点提醒的条目数量:32+万,数量会逐渐增加。
节点提醒触发流程:
- 条目标签数据同步到es;
- 定时任务根据规则去es中查询符合触发逻辑的条目;
- 将满足节点提醒要求的条目新增提醒数据到数据库;
节点提醒闭环逻辑:
- 条目有变动时同步标签数据到es;
- 定时任务根据规则去es中查询符合闭环条件的条目;
- 将满足闭环条件的条目待处理提醒转为己处理;
缺点和短板:
第一版方案是数据库+es标签的方案,对提醒数量的估算太过于乐观,查询不仅仅是单独对提醒数据的查询,各个界面的用户权限不同,导致数据库查询时关联的表有七八张,数居量小的时候没有问题,但数据量上来后,查询会特别慢,红点接口与列表查询接口逻辑相仿,不同的是红点接口在系统任何位置刷新页面都会重新调用接口,耗时长的话非常影响系统的使用,有一天测试环境刷新页面耗时一直在二十秒以上,就是这个原因号致的。
第二版优化
数据库:将原来各节点共用的字段从子表转移到主表中,子表中只存储各节点特有的字段,修改了很多取值赋值逻辑,表结构设计的更加合理,增加了外部数据表,存储与外部相关的数据。
数据查询:将原来的纯数据库查询改为es查询,极大地提升了查询的速度,节点与条款(另外一种不基于条目的提醒)维度不同,各个页面的展示维度也不同,所以构建了两个es索引,一个存储条目维度的所有提醒数据,另一个存储条款维度的提醒数据。同时改为es查询提醒列表,菜单栏和节点顶部的红点数量,导出等。
生成闭环和刷新:定时任务从es中查询符合触发或闭环的条目,处理提醒到数据库;刷新任务刷新数据库提醒数据,再同步数据到es中。在提醒触发闭坏或刷新的时候,先将数据更新到数据库中,再根据条目更新条目提醒到es。
凤险标签:第二版时增加了几种逻辑复杂的节点提醒,有些标签行储到es中,只能标识该条目的一段逻辑是否满足该项节点的提醒要求,加上不重复生成的逻辑,就不能判断该条目是否需要生成新的提醒,一些需要放到数据库查询(隐患)。
数据排除:根据外部数据生成的提醒,而有几千条数据是不需要生成的,且要做到动态配置,时不时会增加很多,开始是代码写死的,后来优化为数据库表存储,试验了几种方式的包含判断,最后发现将数据存储到HashSet中判断是否包含效率最高,甚至比某些其他集合快上百倍。
节点数量:6个。
待生成节点的条目数量:32~ 45万。
主要定时任务:

节点提醒触发流程:
- 将符合逻辑的条目标签数据同步到ES;
- 定时任务根据规则去ES中直询符合触发逻辑的条目;
- 将满足提醒条件的条目新增提醒数据到数据库;
节点提醒闭环逻辑:
- 条目有变动时同步标签数据到ES;
- 定时任务根据规则去ES中查询符合闭环条件的条目;
- 将满足闭环条件的待处理条目提醒转为已处理;
缺点和短板:
第二版优化后,标签查询和列表查询都放到了es中处理,这两个方面直询速度很快,但有些标签逻辑复杂,比如其中一种,根据两个变量生成提醒,标签设计成是否满足节点条件,具体生成哪一种或者合成为一条提醒,还是已经生成了不需要再次生成,这些逻辑只能放到触发任务处理规则时,数次查询数据库判断,只是前几种节点满足逻辑的条目数量并不多,耗时并不是特别长。其次,由于触发闭环任务是根据最开始最简单的节点设计的,处理目前逻辑复杂的节点时,有很多逻辑重复、 无效调用、代码也被改的面目全非,这方面也有很大的问题。
触发任务在处理规则之前需要同步外部数据,并更新这些数据的标签到ES中,以便后续两种提醒的生成和关闭,需要记录历史数据,所以每次同步之前会逻辑刪除之前的数据,再将拿到的外部数据插入到表中,也就是说每执行一次数据量就会增加一倍,是纯数据库操作,同步数据和处理标签很慢,而且会越来越慢。
第三版优化一阶段
数据库:新增外部数据的历史表,存储外部数据需要记录的历史数据,这样对于每次的任务来说,每次将前一天的数据转移到历史数据表中(目前两张记录历史数据的表已有近五百万数据),而数据表中数据保证最新且物理删除旧数据,保证表中数据不会随着任务的执行递增,同步数据和处理标签耗时大大减少。其他方面,有些需要代码流操作的数据,改为直接在查询数据库时处理。
更新ES: 提醒有一个同步所有标签数据的方法,每次调用会同步所有标签逻辑的值到es,但很多时候,只是需要更新单个标签,但为了图方便直接调用了同步所有的方法,优化为根据业务按需调用,所以优化了很多更新数据到es的代码,比如更新外部数据时只更新相关的一个标签,大概可以节省两万次数据库查询。更新提醒数据到es增加了异步更新。
线程池优化:这是第三版方案的主要优化点,由于增加了某节点,数据量飙升,生产满足规则的条目有二十多万,但该节点逻辑非常复杂,按开发时设计的规则,无法通过ES直接判断是否需要生成,生成维度也很复杂,数据库操作频繁,有数据库操作的话,必要的数据库I/O耗时无法避免。导致触发闭环定时和刷新任务一度需要近两个小时,这是背景。
处理提醒是二十个线程并行处理的,也就是是说同一时间最多有二十个条目在处理提醒,假若一秒钟可以处理40个条目的活,需要大概八十分钟才能处理完,这显然时间太长,闭环每个小时执行一次,上一次还没执行完,下一次就需要执行了,所以考虑将线程的并行数量增加,讨论之后決定增加一个参数控制的创建线程池方法,参数放到配置中心,仅限处理节点提醒相关的业务使用,一开始井不确定生产的机器到底能承受多高的并行线程,做了配置线程池后,在测试环境测试80个线程处理,速度大概快了三分之一,跟随其他优化一起放到生产后,在生产上测试机器的负载,自20个线程起,线程数每翻一倍,处理速度大概可以在原有的基础上提升三分之一左右,且越来越少,逐渐将线程池的配置增加到400,此时CPU占用最多到四成左右,继续增加线程数的话,处理速度的提升非常有限,且cpu的负载已经没有太大变化了,此时处理提醒的耗时已经缩短到20分钟以内。
将处理提醒的并行线程增加到400后,处理速度得到显著的提升,但再往上增加提升很有限,说明限制处理速度的短板己经不在这里了。
触发闭环和刷新:重构了定时任务的代码逻辑,将重复调用、可省略的数据库查询等都做了优化,简化了方法调用;分离了触发和闭环,增加了灵活的参数控制各项节点,支持处理单个条目和单个节点。
条目标签:根据调用位置精确控制更新的标签,无意义的更新不去更新。
节点数量:7个。
待生成提醒条目数量:52+万。
定时任务:

凤险触发和闭环流程:相较于第二版优化没有流程上的改动。
其他优化:
- 将生成方法,使用抽象类 + 多个子类继承的方式将方法解耦;
- 将需要查询数据库的刷新逻辑放到es中查询,规则更新时再更新es;
- 涉及外部的节点触发闭环每天仅需要执行一次即可,通过redis标识锁的方式实现每天仅执行一次,执行触发任务后清除redis中标识;
缺点和短板:
本次优化了定时任务的整体逻辑,优化了很多代码层面的东西,将生产服务器的性能发挥起来后,发现限制速度的短板是查询es或数据库。
具体的处理提醒逻辑并没有很多改动,就是说原有的数据库查询还是存在的,最好是将数据库的查询去掉,尽可能将耗时的位置优化掉。
es的数据更新是使用一个独有线程。
第三版优化二阶段
第三版一阶段优化Code Review时讨论是否有办法再进行优化,将定时任务时间压缩到十分钟以内,讨论发现是可以用es的嵌套索引试试,即:将需要查询数据库的数据在更新标签时更新到es中,查询时直接将配置的规则转化为es的查询条件,满足条件也不用再查数据库,es返回的数据就直接拿去用,减少了大部分的数据库查询;也就是将上面线程优化后的短板补齐。
针对需要查询数据库判断是否生成或关闭的节点提醒,拿出其中一种节点提醒进行了测试,证明是可行的,后面对类似的节点提醒进行了优化,减少了大量的数据库连接。
定时任务:

目前的缺点和短板:
虽然已经优化好几次了,但还是有些东西是可以优化,可以做到更好的,比如规则的配置,如果做成动态的高级查询,会灵活很多,目前只能根据设计好的标签中配置规则,增加其他规则只能修改代码。
总结
代码优化是一个需要不断积累、 不断解决问题的过程。随着需求的不断更迭,开始设计再完美的代码,为了兼容一次又一次的新需求,也会变得越来越冗余,可读性也会越来越差,当目前的代码实现不能够满足业务需求,或者对时间或空间的要求大于预期,代码就需要提升下运行的效率了。
节点提醒的难点就在于处理数据,条目数据有50+万,规则有10个,相当于500+万条数据,每天N次处理,每个节点可能会多次查询或修改数据库,只要跟数据库有交互,必要的耗时就无法避免,所以问题的解决思路很重要,需要从多个方面考虑,以下是一些优化思路。
数据库
只要有数据库操作就一定需要耗费时间,尽量减少数据库操作;
查询避免全表打描,能批量尽量批量;
增加索引,字段需符合业务规范,有必要可以拿空间换时间;
ES
es查询速度非常快,根据业务对查询的需求,将数据库数据迁移到es进行查询操作;
避免使用 LIKE 查询ES,目前模糊查询是用 WildcardQuery(通配符查询)实现的,而它的效率可能并不是很理想;
线程池
线程的创建和销毁都会造成一定的开销,尽量发挥服务器的最合适的性能来处理数据,同一模块的逻辑处理尽量使用同一个线程池;
线程数并不是越高越好,应提高的是CPU利用率,在计算逻镇密集的业务场景中基于CPU核心数決定线程数量,在I/0空集的业务场景中尽量提高线程数,当然也不是越高越高,需要根据机器性能来判断,保证在等待I/0结果时多核CPU不被闲置;
多线程
根据数据合理地利用多线程,配合上面的线程池,选择最合适的并行或异步逻辑去处理数据;
代码鲁棒性
尽可能的考虑更多更复杂的业务场景,多写测试类,多做自测;
扩展性
设计时就应充分考虑各段代码可能的扩展场景,降低代码的扩展成本;
效率
合理的运用语言的各种特性,常常限制于开发者的开发经验,多沟通多交流很重要;
可读性
注释和日志尽量合理,命名尽量规范,避免使用过于生僻,非主济的语法和代码逻辑;降低其他人的维护成本;