本文讨论了定制化开发中性能优化的方法和案例。首先概述了通过traceId等手段排查性能问题的常见方法,并指出某些性能问题可能由SQL执行次数过多而非单次耗时引起。随后通过三个案例详细分析了性能瓶颈和优化方法,包括通过批量查询减少数据库访问、优化大数据处理中的循环嵌套和变量计算、采用懒汉式加载优化树形控件等,旨在提升苍穹定制化开发中的代码效率。
1 前言
前几期定制化开发性能优化系列,我们分享了如何在生产环境中排查性能问题,如:通过traceId定位耗时模块,通过慢SQL定位耗时SQL,通过线程堆栈定位耗时代码,通过调用链跟踪梳理服务调用逻辑,通过监控指标分析资源影响(堆内存,CPU负载......)等。然而,不是所有性能问题都会体现在慢sql日志上,有些场景,单次数据库或redis操作耗时很小,但系统整体响应却很慢。这种情况可能不是有慢SQL,而是某些SQL被执行了次数过多,这时候就需要分析业务逻辑,优化代码解决问题。本文通过一些定制化开发中常见的代码优化场景和案例,为苍穹定制化开发代码提供一些优化思路。
2案例分析
2.1案例一
1. 背景:某项目生产环境,F7操作全选数据后(数据约2w+条),需对所选数据处理并返回上级页面展示(每条数据先做关联表单查询,并构造单据体和子单据体等数据,再插入上级页面对应的单据中),该操作耗时约为8.3分钟,且最
终页面返回504。
部分关键代码如下图:
在回调closedCallBack中对选中的数据进行处理
2. 代码分析:可以发现,代码中存在多层循环嵌套,循环体中又包含数据库查询,并且对大数据包循环使用了model.setValue更改数据,以及变量重复计算。
3. 优化方式
(1)for循环查询数据库
将多次查询优化为一次批量查询,在最外层的循环中将id添加至list集合,查询时直接以list作为过滤条件,采用in的方式,保证只查询一次数据库。
(2)对大的数据包(>100)循环使用model.setValue更改数据
参考《苍穹定制化开发规范》:
禁止对大的数据包(>100)循环使用model.setValue更改数据;对大量数据对象修改,可以循环对象使用property.setValueFast设置值,最后统一updateView如:
for(DynamicProperty property : treeEntryProperties) { property.setValueFast(newObj, objProps.get(property.getName()).getValueFast(obj)); } this.getView().updateView(entryKey); //针对修改数据局部刷新 |
(3)变量重复计算
代码中存在多处对集合长度重复计算,将for循环优化成:
for (int i = 0, n = list.size(); i < n; i++) {} |
此写法可避免每次计算数组或者集合的大小 ,且变量作用域遵循最小范围原则。
(4)for循环嵌套
对于循环嵌套,优化的思路是减少循环次数,尽可能将执行次数由i*j*k优化成i+j+k。该代码中,内层循环中子单据体的数据封装需用到父单据体中的数据,可将父单据体数据在外层中使用Map存储,key值为与子单据体关联的标识(如:单据id),value值为父单据体的数据,或直接将list转换成Map,在内层循环封装子单据体数据时再根据key值进行匹配获取数据。
4. 总结
开发中循环语句是高频次代码,若设计不合理,容易出现多层循环嵌套进行数据对比或封装,循环中操作数据库,重复执行等情况。当数据量较大时,这类写法易引发性能问题。
除上述案例涉及问题之外,循环嵌套还需关注以下要点:
(1)避免在循环中调用服务;
(2)多层嵌套循环应该遵循“外小内大”原则;
(3)提取与循环无关的逻辑至外层;
(4)捕获异常易产生额外系统开销,应避免在循环中捕获异常。
2.2案例二
1. 背景:某项目在定时任务中做大数据量查询并更新操作(查询:400w+记录,更新:200w+记录),耗时较长,部分关键代码如下:
2. 代码分析:原始代码中,对大数据采用分批次更新,设计合理。但代码实现上,只在数据更新操作时做了分批处理,查询获取更新数据源时未采用批量查询,依然存在循环遍历数据库,查询未使用索引等不当之处。
3. 优化方式
(1)分批处理:原始代码中分批实现如下图,首先,建议先在最外层循环将待更新的数据分批(定义分批次数及每次更新数据量),其次,将现有BusinessDataServiceHelper.loadSingle查询单条数据改为使用BusinessDataServiceHelper.load查询批量数据,并返回Object数组(此做法可以避免循环中遍历查询),再以Object数组为更新参数做批量更新。
(2)查询优化:确保所有查询执行计划最优,原始代码存在几处查询未建索引(索引优化参考:定制化开发性能优化专题系列(一))。
(3)业务逻辑优化:如下图,判断条件可通过前面的查询过滤条件排除,从而减少循环判断的次数,获取的结果可以做分批处理,减少不必要逻辑处理代码。
4. 总结:
针对苍穹定制化开发大数据量操作,如更新、插入,目前建议方案分批处理,分批更新或插入。需要注意以下几点:
(1)对于查询或更新,审核等如果有批量处理的方法或接口,必须使用批量处理方法和接口,批量处理可避免在循环中做大量访问数据库或业务逻辑处理,从而提高效率。
(2)事务问题:分批次更新或插入需要考虑事务问题,如果是单一数据库操作,有事务保证要求,则需将多批次操作并入同一事务中,若是跨库跨服务操作,则需使用MQ/TCC保证事务。
2.3案例三
1. 背景:动态表单中构造树形控件,使用递归实现,加载慢,关键代码如图:
2. 优化分析:原始代码采用一次性加载树形控件所有数据,有两点性能问题,一是一次性加载初始化树的时间较长,二是递归加载所有数据,会在循环中多次查询数据库(虽然单次查询不耗时)。
3. 优化方式
(1)对于树形层级信息展示,一般优化建议是优先考虑懒汉式加载的方式,即首次只加载第一层的信息,用户点击展开层级时,再加载其它数据,这样能减少数据库访问压力;
(2)如树形控件加载数据量在可控范围内,且业务要求必须一次性加载所有数据,因为该场景中数据仅是展示,建议将获取数据的方式优化为loadSingleFromCache即从缓存获取数据。
推荐阅读