Java性能优化一则
背景:
最近在做Provider Search项目,其中有一个环节就是DBA有一个脚本会定期把一些特定的数据从测试环境逐级推到PROD。这就要求我们有一个测试脚本来测试DBA的这个数据脚本是否正确。
早先时候,因为所有的数据都是存在Oracle中,所以测试起来比较方便,因为两台Oracle之间可以创建一个连接,这样我们只要运行一个SQL中间用minus来把两边查出来的数据相减就知道数据是否有差异。但是现在测试环境中我们把数据迁移到了MySQL,而DBA暂时还没时间去研究Oracle到MySQL之间的数据连接。所以我们QA的测试脚本需要重新实现。
实现方法:
如果按照最简单的方法,我们只要查询两组数据的记录数是否一致就可以了,但如果有数据差异的情况下,DBA就不太好调查问题。所以我们就在Oracle和MySQL上分别运行同一条SQL取出一组数据,以DataSet方式返回。然后把这些数据放入HashMap中。再遍历HashMap把相同的数据删除掉,到最后就剩下那些不同的数据,我们就可以让DBA去调查数据不一致的原因。
问题浮现:
以前在两台Oracle之间进行数据比较的速度是非常快的,即使结果集有一百万条记录也可以在几分钟之内就比较完返回结果。但现在把数据取到客户机内存却存在严重的性能问题。把数据载入到DataSet就花费了非常多的时间。测试了一下一百万条记录的数据集,需要半个多小时才能完成载入到内存。而从Oracle载入数据到内存比从MySQL载入要慢的多。
以这样的速度来比较所有的数据估计得用上大半天时间,明显是不高效的。跟原先的程序速度比较起来相关十万八千里。捉摸着估计是代码里有臭虫。
开始调查:
虽然已经是晚上10:30,不过这个挑战实在有点提神。于是我打开代码开始查看。
有一种灵感驱使着我直接打开从数据库读取数据到DataSet的那些代码。看了几眼代码就非常兴奋,就像管道工人发现管道堵塞的原因一样兴奋。请看以下代码:
public DataRow[] Rows = new DataRow[0]; public void AddRow(DataRow row) { DataRow[] _rows = Rows.clone(); this.Rows = null; this.Rows = new DataRow[_rows.length+1]; for(int i=0;i<_rows.length;i++) { this.Rows[i] = _rows[i]; } this.Rows[this.Rows.length-1] = row; _rows = null; }
看到没有,恶心不?每当向DataSet添加一行的时候都要重新创建一个数组来装进这个新的元素。DataSet的相关代码是我很早以前从网上复制过来的,没经过审核就直接用上去,没想到里面竟然有如此耗性能的代码。当数据集里有N条记录的时候,这个算法的复杂度就是O(1+2+3+...+N)。
问题解决:
思索了两分钟,决定用List替代数组来装数据,于是就有了以下代码:
List<DataColumn> dataColumns = new ArrayList<DataColumn>(); public void addColumn(DataColumn col) { dataColumns.add(col); }
后来经过测试,从一条SQL原先从MySQL载入数据到DataSet需要10分钟,现在只要几秒种。性能提高了非常多。
结论:
1. 用clone()的方法会消耗掉一些内存,当数据量大的时候,会很快把内存消耗掉,从而导致GC频繁操作,从而占用比较多的CPU。
2. 当数据库结构里的元素数量有频繁扩展的需要的时候,不要选用数组。因为这样就需要我们去新创建一个更大的数组。而数组在内存里必须是连续的空间,所以开辟一个新的数组空间是比较耗费资源的。这种情况下,我们应该选取链表结构来存储数据。
本例后面使用的ArrayList其实也是数组结构,但是当它在扩展空间的时候会根据一个算法来自动新增N个空元素,以达到提高效率的目的。
public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } }
--Kenny