目录
一、背景
1、用户反应某个报表导出时等待很久也没有导出来,我F12查看导出接口在1.5min的时间请求被取消,后端服务器的网关(如Nginx、Apache)或后端应用本身可能设置了90秒的超时,用户一直等待也没有导出文件,所以需要优化导出逻辑加快导出时间。
二、发现问题
1、本地调试
1.1安装启动arthas
(1)、下载地址:https://arthas.aliyun.com/arthas-boot.jar
(2)、启动 java -jar arthas-boot.jar
(3)、选择目标应用id回车
(4)、执行tarce命令 -n 5 表示最多捕获5次方法调用,使用--skipJDKMethod false包含基础库调用耗时,还可以添加这个参数'#cost>500'表示只显示超过500毫秒的。
java
trace 全类名 方法名 -n 5 --skipJDKMethod false

关于arthas的命令,可以安装idea插件arthas idea
选中方法名称鼠标右键,Arthas Command中可以选择相应的命令,可以复制出来,直接使用
1.2代码逻辑分析及初步优化
从代码看有三个主要逻辑,第一个使用远程调用的方式获取数据大概12万条,第二个是循环获取数据对某个字段进行国际化如下图,
第三个是把数据写入excel方法。
通过打断点发现,在第二个逻辑国际化方法中阻塞时间很久,所以决定先优化国际化的逻辑,放弃循环结果集,改为改成了sql关联国际化表的形式。
1.3使用arthas追踪耗时情况并进一步优化
解决完字段国际化后,重新看一下耗时情况。
从耗时情况分析远程获取数据方法占用主要时间,所以需要优化查询逻辑,经过查看sql执行计划extra 中useing filesort,没有在排序字段上创建索引导致创建索引后重新看耗时时间,获取数据缩短到50s。数据写入到excel耗时13s左右。总耗时63s左右小于90s本地可以正常导出。
2、服务器调试
本地调试好以后本以为问题就解决了,但是发布到生产服务器后,导出还是1.5min后取消了,没有什么效果,所以在生产服务器上进行了两个测试。
arthas下载命令:curl -O https://arthas.aliyun.com/arthas-boot.jar
然后启动和选择目标服务进入arthas终端
2.1、测试一使用arthas查看修改后的代码是否生效
在arthas终端中执行:
java
jad 全类名
反编码查看代码是否修改后的逻辑,我发现代码已经是修改后的。
2.2、测试二使用arthas追踪耗时情况
执行trace 全类名 方法名 -n 5 --skipJDKMethod false
后在生产上执行导出
2.3、结论
通过上面结果可以发现,73.86%耗时的是把数据写入excel的方法。所以优化重点是数据写入excel。
三、解决问题
1、原来数据写入excel的方法
循环遍历所有的数据,如果数据是实体类对象则使用反射的方式获取对象中的属性值,这里有多少数据量就会使用几次反射,反射开销很大。
2、修改成使用fastexcel
FastExcel 使用了缓存,避免重复使用反射方法获取属性值,提高了性能。还有就是字段国际化,可以使用处理器进行处理(自定义了I18nCellWriteHandler处理器),所以sql中关联表国际化的逻辑去掉了。
这个是优化后的追踪方法调用耗时情况,数据写入excel速度显著提高耗时32秒左右,总耗时54秒左右。
ReflectionBeanMap 核心实现如下,initPropertyDescriptors 初始化属性描述符缓存,getValue时从缓存中获取PropertyDescriptor,反射调用 getter 方法。
java
public class ReflectionBeanMap<T> implements BeanMap<T> {
private final Class<T> beanClass;
private final Map<String, PropertyDescriptor> propertyDescriptors;
private final BeanMapConfig config;
public ReflectionBeanMap(Class<T> beanClass) {
this(beanClass, BeanMapConfig.defaultConfig());
}
public ReflectionBeanMap(Class<T> beanClass, BeanMapConfig config) {
this.beanClass = beanClass;
this.config = config;
this.propertyDescriptors = initPropertyDescriptors(beanClass);
}
/**
* 初始化属性描述符缓存
*/
private Map<String, PropertyDescriptor> initPropertyDescriptors(Class<T> beanClass) {
try {
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class);
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
Map<String, PropertyDescriptor> result = new LinkedHashMap<>();
for (PropertyDescriptor descriptor : descriptors) {
// 过滤掉没有 getter 方法的属性
if (descriptor.getReadMethod() != null) {
result.put(descriptor.getName(), descriptor);
}
}
return Collections.unmodifiableMap(result);
} catch (IntrospectionException e) {
throw new RuntimeException("Failed to introspect bean: " + beanClass.getName(), e);
}
}
/**
* 获取属性值 - 核心反射调用
*/
@Override
public Object getValue(T bean, String propertyName) {
if (bean == null) {
return null;
}
PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
if (descriptor == null) {
if (config.isIgnoreMissingProperties()) {
return null;
}
throw new IllegalArgumentException(
"Property '" + propertyName + "' not found in " + beanClass.getName());
}
Method readMethod = descriptor.getReadMethod();
if (readMethod == null) {
if (config.isIgnoreMissingGetters()) {
return null;
}
throw new IllegalArgumentException(
"No getter method for property '" + propertyName + "' in " + beanClass.getName());
}
try {
// 反射调用 getter 方法
return readMethod.invoke(bean);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(
"Failed to get value for property '" + propertyName + "' from " + beanClass.getName(), e);
}
}
/**
* 设置属性值
*/
@Override
public void setValue(T bean, String propertyName, Object value) {
if (bean == null) {
return;
}
PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
if (descriptor == null) {
if (config.isIgnoreMissingProperties()) {
return;
}
throw new IllegalArgumentException(
"Property '" + propertyName + "' not found in " + beanClass.getName());
}
Method writeMethod = descriptor.getWriteMethod();
if (writeMethod == null) {
if (config.isIgnoreMissingSetters()) {
return;
}
throw new IllegalArgumentException(
"No setter method for property '" + propertyName + "' in " + beanClass.getName());
}
try {
// 类型转换和反射调用 setter 方法
Object convertedValue = convertValue(value, descriptor.getPropertyType());
writeMethod.invoke(bean, convertedValue);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(
"Failed to set value for property '" + propertyName + "' in " + beanClass.getName(), e);
}
}
/**
* 获取所有属性名
*/
@Override
public Set<String> getPropertyNames() {
return propertyDescriptors.keySet();
}
/**
* 获取属性类型
*/
@Override
public Class<?> getPropertyType(String propertyName) {
PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
return descriptor != null ? descriptor.getPropertyType() : null;
}
/**
* 值类型转换
*/
private Object convertValue(Object value, Class<?> targetType) {
if (value == null) {
return null;
}
// 如果类型匹配,直接返回
if (targetType.isInstance(value)) {
return value;
}
// 使用配置的转换器进行类型转换
return config.getTypeConverter().convert(value, targetType);
}
}
我写了一个便于理解的例子
java
public class Test {
public static void main(String[] args) throws Exception {
List<BaseUser> list =new ArrayList<>();
//添加模拟数据
for(int i=0;i<=10000000;i++){
BaseUser baseUser1=new BaseUser();
baseUser1.setActiveFlag(i+"");
list.add(baseUser1);
}
long startTime = System.currentTimeMillis();
/**
* @description 原来的反射方式
*/
for(BaseUser item:list){
Class cl=item.getClass();
Field nameField=cl.getDeclaredField("activeFlag");
nameField.setAccessible(true);
nameField.get(item);
}
//打印耗时时间
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
System.out.println("原来的反射方法执行时间:" + (executionTime) + "毫秒");
long startTime1 = System.currentTimeMillis();
/**
* @description 原来的反射方式
*/
Class cl =BaseUser.class;
Method method= cl.getMethod("getActiveFlag");
for(BaseUser item:list){
method.invoke(item);
}
//打印耗时时间
long endTime1 = System.currentTimeMillis();
long executionTime1 = endTime1 - startTime1;
System.out.println("修改后的反射方法执行时间:" + (executionTime1) + "毫秒");
}
}
执行结果
java
原来的反射方法执行时间:1041毫秒
修改后的反射方法执行时间:170毫秒
四、总结
1、本地优化了sql和结果集中字段国际化逻辑,在本地导出速度得到很大的提高,但是发版到生产环境后,数据写入excel逻辑是耗时的主要原因,通过arthas实际的监控了每个方法的耗时情况,方便线上问题解决。
2、对于本地和线上的差异,我猜测是线上和本地jvm版本与启动参数不一样导致差异。