本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue
一、前言
最近一直忙着参与公司的新项目开发,由于临期上线,正在对系统进行性能压测,在这个过程中,发现一些代码有性能优化的空间。因此决定写一篇文章,把本次以及今后,遇到的性能优化的 case 都记录下来,希望对大家们的编码水平能够有所帮助。
二、Java 基础
2.1 字符串拼接
在我们的系统中,存在着大量的缓存,这些缓存的 key 值根据请求参数的不同而拼接起来,如下代码所示:
优化前1 2 3 4 5 6 7 8 9 10 11
| public class LastPriceCache { private String keySuffix = "last_price_%s";
public double getLastPrice(int id) { return redisService.get(this.generatorKey(id)); }
private String generatorKey(int id) { return String.format(keySuffix, id); } }
|
字符串拼接存在性能损耗,如果 id 值是可预期的,完全可以将其在内存中缓存起来,如下代码所示:
优化后1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class LastPriceCache { private String keySuffix = "last_price_%s";
private Map<Integer, String> keyMap = new HashMap<>();
public double getLastPrice(int id) { return redisService.get(this.generatorKey(id)); }
private String generatorKey(int id) { String result; if(!keyMap.containsKey(id)) { result = keyMap.get(id); } else { result = String.format(keySuffix, id); keyMap.put(id, result); } return result; } }
|
2.2 装箱类型
在我们项目中的问题代码,是在使用 BigDecimal
进行除法操作时,精度保留的小数位数的变量使用了包装类型。可惜在我自己的电脑上没有办法复现出来,也许是公司的电脑配置太低了,哈哈。
因此我就用包装类型累加来举例把,虽然例子都烂大街了,但能说明问题。将如下代码中的 Long
改成 long
,就会得到不一样的耗时结果。
1 2 3 4
| Long value = 0; for(int i = 0; i < 100_0000; i ++) { value += i; }
|
2.3 Stream 合并
在我们的系统中,存在分页查询用户订单的需求,涉及到对源数据的过滤/截取/排序操作,如下代码所示:
优化前1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| List<Order> sourceOrder = ...;
List<Order> result = sourceOrder.stream().filter(e -> e.getAmount() > 0).collect(Collectors.toList());
if(startId > 0) { result = result.stream().filter(e -> e.getId() >= startId).collect(Collectors.toList()); } if(endId > 0) { result = result.stream().filter(e -> e.getId() < endId).collect(Collectors.toList()); }
if(result.size() > limit) { result = result.subList(0, limit); }
Collections.reverse(result);
|
JDK 1.8 中引入了流式操作,很大程度上使代码变得更加简洁,但是使用不当也会拖慢性能。在上面代码中,滥用了流式操作,完全可以进行合并操作,且后续的截取和排序操作也可以整合在流式操作中,如下代码所示:
优化后1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| List<Order> sourceOrder = ...;
Stream<Order> stream = sourceOrder.stream();
stream = stream.filter(e -> e.getAmount() > 0);
if(startId > 0) { stream = stream.filter(e -> e.getId() >= startId); } if(endId > 0) { stream = stream.filter(e -> e.getId() < endId); }
Comparator<Order> comparator = Comparator.comparingLong(Order::getId); if(!isAsc) { comparator = comparator.reversed(); }
List<Order> result = stream.limit(limit).sorted(comparator).collect(Collectors.toList());
|
三、三方包
3.1 FastJson 预热
在我们项目中,采用 FastJson 作为序列化库。FastJson 虽然号称速度非常快,但是其在首次序列化时速度却是让人大跌眼镜,在压测环境下,一下次就被暴露出来了。
只需要在程序启动时,静态加载以下两个 FastJson 类,问题就可以解决。
1 2 3 4
| static { new ParserConfig(); new SerializeConfig(); }
|
3.2 浅拷贝
之前项目中,直接使用 spring 框架的 BeanUtils
进行浅拷贝,在压测中也发现其耗时比较严重。而使用同为 spring 提供的 BeanCopier
则性能很好。
BeanCopier 工具类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class BeanCopierUtils { private static final Map<String, BeanCopier> CACHE = new ConcurrentHashMap<>();
public static void copyProperties(Object source, Object target) { BeanCopier copier = getBeanCopier(source.getClass(), target.getClass()); copier.copy(source, target, null); }
private static BeanCopier getBeanCopier(Class<?> sourceClazz, Class<?> targetClazz) { String key = generatorKey(sourceClazz, targetClazz); BeanCopier copier; if(CACHE.containsKey(key)) { copier = CACHE.get(key); } else { copier = BeanCopier.create(sourceClazz, targetClazz, false); CACHE.put(key, copier); } return copier; }
private static String generatorKey(Class<?> sourceClazz, Class<?> targetClazz) { return sourceClazz + "_" + targetClazz; } }
|