项目环境:
JDK12
springboot:2.1.6.RELEASE
springcloud:Greenwich.RELEASE
业务场景
系统需要对接RFID,扫描枪扫描商品得到的EPCCode经过前端传到后端,后端API需要先将EPC通过算法转成EANCode,再用EANcode请求一个API,得到itemcode。
由于商品可能有多个,且最多有16个,如果用串行的话,由于EPC转EAN的算法很复杂,而且从EAN转itemcode还需要请求外部的API,效率会很低,所以并行是必须的。
解决方案
那么问题来了,并行的解决方案有很多,之前使用过多线程配合CountDownLatch可以做到,但是代码量太大而且代码可读性太差了,刚好最近在研究的parallelStream可以解决这个问题,于是就实践一下,本地和测试环境跑下来还可以,就等上生产看效果啦!
首先,parallelStream的适用场景是CPU密集型的操作,充分利用CPU,在我们这个业务场景中,EPC转EAN需要大量的计算,对CPU依赖大,而EAN转itemcode这一步需要请求外部API,这里主要就是等待响应,让他们一起等着几好啦。因为我们是需要将我们的结果同步返回给前端所以,这里我们不能使用execute而要使用submit,在后面get一下,保证将所有任务做完将全部结果返回给前端。
相信大家都知道,parallelStream使用的是ForkJoin框架的ForkJoinPool,而ForkJoinPool默认开启的线程数是你机器CPU的核心数,我电脑是8核的,所以它会同时开启8个线程来执行我的任务,但是我们的服务器是2核的,所以如果不对它进行修改的话,它以此最多启用2个线程来执行我们的任务,这显然不是我们想要的,所以我在这里创建了一个核心线程数为16的ForkJoinPool,为什么是16呢?因为我们的订一个订单中,商品最多是16,为了使线程够用,就设为了16.
大家可能注意到了,这里我用了Collections.synchronizedList()给ArrayList包装了一下,因为这个list我们是需要多线程add的,而ArrayList并不是一个线程安全的集合,Collections.synchronizedList()可以帮它做到这点。
一开始,我想着要不要也将ArrayList初始化为16的长度,来避免它扩容,但是,后来一想还是不用了,因为ArrayList默认长度为10,一般订单商品是不会超过这个值的,这样做的话并不会减少响应时间反而会浪费空间。
当然,我在getItemCodeByEpcCode方法里是加了个缓存的,因为每个EPC转成itemcode几乎是不会改变的嘛,所以设了一个月的过期时间。
具体实现
让我们直接来看下代码实现(具体的过程在getItemCodeByEpcCode这个方法里面,因为跟我们的主题不大,所以就不贴出来了):
@Override
public ApiResponseDto getItemCodeByEpcCode(List<String> epcCodeList) throws Exception {
List<RfidResponseDto> results = Collections.synchronizedList(new ArrayList<>());
//get httpHeader
HttpHeaders headers = tokenFeign.clientCredentialHttpHeader(BeanConstant.SYSTEM_CODE_MASTERDATAS).getHeaders();
log.info("[GetItemCodeByEpcCode API] start to get itemCode for [ {} ]", epcCodeList.toString());
ForkJoinTask<?> submit = new ForkJoinPool(16).submit(() -> epcCodeList.parallelStream().forEach(epcCode -> {
RfidResponseDto itemCode = getItemCodeByEpcCode(epcCode, headers);
results.add(itemCode);
}));
submit.get();
return new ApiResponseDto<>(AppClientResponse.GENERAL_SUCC, results);
}