还是上一篇中提到过的API,通过调查我们发现这个API在需要返回的数据量比较大的有些单次调用中CPU可能达到90%到100%。所以即便是调用量不大的情况下,这个API一样是造成整个系统性能低下的最大贡献者。其中有一次调用返回时间更是长达10s。其实CPU的使用率高的话,响应时间自然就长,需要很长时间的计算才可以返回。
优化的思路还是把单个查询分成多次,降低每次返回的数据集的大小。数据量比较大的表最好是单独查询,不要去和其他表join。比如一张表中我们查出的结果集是10k,和另外一个只有20的结果集相乘就是几十万了,随便再jion一两个表就上百万了。凡是上万的结果集我都不太放心,更不要提10万,百万。另外一个好处是,单独查这一张表的时候不用去LEFT JOIN,INNER JOIN可以解决所有问题,而且JOIN后面的表都是unique的record。有一个查询的结果集特别大,通过分析后我认为是因为查询的时间范围很广,有13个月的结果。为了进一步让单个查询的结果集变小,决定用时间段来分割。比如分成两次查询,各查半年左右。总之我的原则就是单次查询的结果集要小,这样给MyBatis去处理结果集的压力就小。通过对每次查询打印执行时间可以看出来,结果集大的时候返回特别慢,而且CPU也很高,通过工具可以看到ResultSet的getLong,getString等方法调用次数很多。如果还有很多typeHandler那给Mybatis的压力就更大了。
这样分了之后单次查询返回的时间很快。但是问题就是怎么把这些数据合并起来。因为是树状结构的数据,相当于从根部要获得叶子。给你一个“师”的ID,需要查出所有他小面的各级单位的各种数据。一个SQL join到底的结果就是结果集很大,我的做法是一个SQL查出所有“旅”,一个SQL再查出所有“团”,依次类推。如何把这些数据合并呢?一级一级去循环遍历的话必然是一个多层的循环。想象一个5层的循环,没有哪一个程序员愿意这样写。我的解决方案是用HashMap,这就是Hash的好处,寻址很快,不用遍历去找。这样的话避免了多层循环,多层循环的时间复杂度应该是O(n*m*L....),这样改写之后至少可以变成O(n+m+l....)。还是空间换时间的做法,结果也可想而知,就是产生的临时变量很多,GC很频繁,minor GC很频繁。性能测试之后发现,当TPS到100左右的时候CPU的GC占比有时会有10%左右。但是暂时没有想到更好的办法,目前也还在这个项目可以接受的范围。希望以后能想到更好的办法。不过更好的办法可能是改写需求,让这个API不要一次性返回那么多数据,也就没有挑战性了。。