前言

今天接到领导通知,然我优化一个导出Excel功能,这个功能在线上4万多条数据大概用了65分钟倒完。

捋顺代码

看一下代码是怎么写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class ExportService{
public Workbook export() {
/* 创建表格表头 */

// 在数据库中查询数据,大概在两三秒内完成,数量6W+
List<Data> list = getData();
// 创建一个Map,Key是一个对象,Value是一个List,没太太理解为什么这么用
Map<TargetObject, ArrayList<String>> dataMap = new HashMap<ProductColorModel, ArrayList<String>>();
for(Data d : list){
TargetObject obj = new TargetObject();
/* obj.set(data) */
String mark = d.getMark();
// 这里大概理解为什么要创建一个Map了,大概是把其他数据相同,但是mark字段不同的数据整理出来
// 类似一个去重操作,但是还要保留mark字段的值
if (dataMap.get(obj) == null){
List<String> markList = new ArrayList();
markList.add(mark);
dataMap.put(obj, markList);
} else {
List<String> markList = dataMap.get(obj);
markList.add(mark);
dataMap.put(obj, markList);
}
}

// 遍历map
for (Map.Entry<TargetObject, ArrayList<String>> entry : dataMap.entrySet()) {
TargetObject obj = entry.getKey();
ArrayList<String> markList = entry.getValue();
// 将所有的标识字段以‘,’分割,生成字符串,也差不多理解那个dataMap的作用了
String marksStr = "";
for (String mark : markList) {
marksStr += mark + ";";
}
/* 创建cell并赋值 obj.get() */
}
/*设置单元格格式*/
/*设置自动调整列宽*/
}
}

/**
* 数据库和导出excel的中间实体
*/
class TargetObject {
private String field1;
private String field2;
private String field3;
private String field4;
private String field5;
private Date field6;

@Override
public boolean equals(Object obj) {
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");
if (obj instanceof TargetObject) {
TargetObject model = (TargetObject) obj;
if (((model.materialNum == null && materialNum == null) || model.materialNum
.equals(materialNum))
&& ((model.field1 == null && field1 == null) || model.field1.equals(field1))
&& ((model.field2 == null && field2 == null) || model.field2.equals(field2))
&& ((model.field3 == null && field3 == null) || model.field2.equals(field3))
&& ((model.field4 == null && field4 == null) || model.field2.equals(field4))
&& ((model.field5 == null && field5 == null) || model.field2.equals(field5))
&& ((model.field6 == null && field6 == null) || ((model.field6 != null && updateDate != null)&&fmt
.format(model.field6).equals(fmt.format(field6))))) {
return true;
} else {
return false;
}
}
return super.equals(obj);
}

@Override
public int hashCode() {
return 0;
}
}

分析问题

通过阅读前面代码,发现主要问题:TargetObjecthashCode方法返回值为0。这个问题在Map的size很小的时候发现不了问题,但是在现在的6W+数据中可以很明显的发现。因为Map底层实现是数组加链表(JDK8之前,本文中项目使用的是JDK7),并且使用Key值的hashcode值作为数组下标进行存储,所以TargetObjecthashCode方法返回值固定为0会导致所有的数据都存储在下标为0的链表上,这会导致这个链表很大,即使Map扩容也不会将数据随机分散在数组中。这就导致每一次get都是往全链表遍历的方向走。在export方法第一个for循环中,dataMap.get(obj)会越来越慢,到后面基本上就相当于一个N*N的一个循环了,并且get方法在后面的if中还调用了两次,这又乘个2,虽然这是个问题相当于前面的是个小问题,但是也是雪上加霜了。

解决问题

因为这个这里的业务不熟,所以sql就不看了,并且这个sql的速度还可以,在可以接受的范围内,所以我把重点优化的问题放在了Map相关的方向上。

1、将dataMap改为Map<String, TargetObject>

2、删除TargetObjecthashCodeequals

3、为TargetObject添加getString方法,作为dataMap的Key值

4、为TargetObject添加markList字段,和addMark(String)方法,以及getAllMarkToString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class ExportService{
public Workbook export() {
/* 创建表格表头 */

List<Data> list = getData();

int size = list.size();
// 一个小的优化点:HashMap初始化容量为16,在这次6W+的导出过程中大概要扩容11次,
// 每次扩容都需要进行数组的拷贝,所以随着数据量增大拷贝次数增加,这里也会形成一个性能瓶颈,
// 所以这里最好初始化一个容量
Map<String, TargetObject> dataMap = new HashMap<String, TargetObject>(size);
// 创建一个list存储要导出的数据,因为list的遍历速度要比map快一些,前面的map作为一个类似缓存的数据,
// list这里也需要给以初始容量,因为ArrayList是数组实现,长度固定,所以每次扩容也需要进行数组拷贝,
// 也会比较耗费性能,因为除了mark,其他字段相同的数据视为同一数据,所以最终数据肯定会少于size,
// 所以这里给到一半,只会扩容一次,可以接受
List<TargetObject> dataList = new ArrayList<String>(size/2);
TargetObject obj;
//已经放入map中的数据
TargetObject hadObj;
for(Data d : list){
obj = new TargetObject();
/* obj.set(data) */
String mark = d.getMark();
String key = obj.getString();
hadObj = dataMap.get(key);
if (hadObj == null){
obj.addMark(mark);
dataMap.put(key, obj);
} else {
hadObj.addMark(mark);
}
}

for (TargetObject data : dataList) {
//将所有的标识字段以‘,’分割,生成字符串,也差不多理解那个dataMap的作用了
String marksStr = data.getAllMarkToString();
/* 创建cell并赋值 */
}
/*设置单元格格式*/
/*设置自动调整列宽*/
}
}

class TargetObject {
private String field1;
private String field2;
private String field3;
private String field4;
private String field5;
private Date field6;

private List<String> markList;

public TargetObject(){
this.markList = new ArrayList();
}

public void addMark(String mark){
this.markList.add(mark);
}

public String getAllMarkToString(){
StringBuilder stringBuilder = new StringBuilder();
for (String mark : markList) {
stringBuilder.append(mark);
}
return productsStr.toString();
}

/**
* 因为实体中大部分字段都为String类型,将所有字段拼接为一个长字符串后
* 如果除了mark字段都相同,那么这个字符串也肯定相同,这样这个字符串作为Map的key值应该没什么问题
*/
public String getString(){
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");
//也是给一个初始容量,防止多次扩容造成性能浪费
StringBuilder stringBuilder = new StringBuilder(128);

stringBuilder.append(field1);
stringBuilder.append(field2);
stringBuilder.append(field3);
stringBuilder.append(field4);
stringBuilder.append(field5);
if (field6!=null) {
stringBuilder.append(fmt.format(field6));
}

return stringBuilder.toString();
}
}

后记

优化后同样数据用了不到一分钟左右搞定,效果还不错。但是我觉得应该还有优化空间,比如sql、两次循环是不是可以一次,这个有时间或者需要再次优化再看吧!

最后推荐大家几款性能定位工具:jstack、jmap、jstat,现在我们单位用的是jstack,可以直接定位卡点,效果非常好~~