多线程使用优化时间

一、问题发现

​ 这个问题是被测试发现的,说对13个文件同时进行处理,请求之后没有报错,但是也没有给前端返回报文,调用接口看看日志后发现,接口进行到一半网关超时,gateway timed out,但是日志打印却打印到最后执行完成,应该是网关挂了(网关设置时间1min),默认没有给前端返回。

二、解决历程

1.因为是调用第三方,寻思代码没法优化,后来发现,还是有可以优化的空间,因为其中有一部分是html转成pdf再转成图片,再截图传给第三方base64,很麻烦,问我为什么不直接html转图片,因为线上环境没有什么字体包,报Fonts.version()错误,百度说可以解决,但是接触不到线上环境,就此作罢,百度如何优化时间。。。。。
在这里插入图片描述

2.优化第一步

(1)之前是for循环里面根据文件生成图片,图片是身份信息、日期之类的弄成图片给第三方,对一个人来说是公共信息,所以提到调用第三方for循环之前执行,以为能控制在一分钟以内,但是可想而知,微乎其微的时间差,所以还是网关超时。。。。。

//日期生成base64
String date = new SimpleDateFormat("yyyy年MM月dd日").format(new Date());
variables.put("name",date);
 try{
        dateStrr = ImageUtlis.createImageByInfo("personal_infotoImg", variables, templateEngine, pdfFonts());
        log.info("生成日期base64!");
    }catch (Exception e){
        throw new BusinessException(Constants.ERROR_CODE,"出现异常,请联系管理员!");
    }
}
log.info("身份证base64:{}",idNoStr);
log.info("日期base64:{}",dateStrr);
for (FileList fileList : fileLists) {
      //调用第三方
}

(2)将需要调用第三方的方法、以及涉及的修改方法封装成方法异步调用@Async,同时因为修改数据涉及好几个表,就设置了@Transactional(rollbackFor = Exception.class),代码如下

public class TestService(){
	public void hhh(){
	sss();
}
@Transactional(rollbackFor = Exception.class)
@Async
public void  sss(){
      //生成图片和循环调用第三方
}
}

在方法里面调用异步方法不生效问题:(解决参考https://www.yii666.com/blog/52415.html)

解决方案:其实我们的注入对象都是从Spring容器中给当前Spring组件进行成员变量的赋值,由于TestService使用了AOP注解,那么实际上TestService在Spring容器中实际存在的是它的代理对象,总的来说改成下面代码:

public class TestService(){
	public void hhh(){
	MaterialSignPdfServiceImpl signServiceProxy = SpringUtil.getBean(MaterialSignPdfServiceImpl.class);
signServiceProxy.sss(signAndPushXGVo);
}
@Transactional(rollbackFor = Exception.class)
@Async
public void sss(SignAndPushXGVo signAndPushXGVo){
      //生成图片和循环调用第三方
}
}

(3)以为问题解决,突然发现有数据不同步问题,前端根据后端返回跳转到首页,我先给前端返了成功,前端返回到首页又查了一下数据,发现用户还要进行操作(看起来是时间快了很多,但是因为异步数据。导致还没落库,查出来当然还有继续进行重复操作),继续想优化。。。

3.多线程优化时间

异步可能会导致数据不同步问题,此时采用多线程调优

public  static  ExecutorService executorService = Executors.newFixedThreadPool(8);
for (FileList fileList : fileLists) {
    Runnable tge = () -> {
            //业务代码,我是调用本类中的一个方法
    };
    executorService.submit(tge);
}
// 停止
executorService.shutdown();
// 等待线程完成
while (!executorService.isTerminated()) {
    try {
        // 等待 300 毫秒
        executorService.awaitTermination(300, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
        throw new BusinessException(Constants.ERROR_CODE,"等待出错,请联系管理员!");
    }
}

多线程调优发现问题:

在这里插入图片描述

原因:(1)本次使用的线程池newFixedThreadPool,发现是使用了多线程的默认拒绝策略ThreadPoolExecutor.AbortPolicy,所以再executorService.shutdown();上一次请求线程池对象用的线程池对象都是同一个,调用到executorService.shutdown();所以导致了线程池已经关闭了,还继续去请求任务。

【线程池调用了shutdown()之后,再向线程池提交任务的时候,如果你配置的拒绝策略是ThreadPoolExecutor.AbortPolicy的话,这个异常RejectedExecutionException就被会抛出来。】

2)线程数量固定的线程池(定长线程池),可控制线程最大并发数,超出的线程会在队列中等待。而队列LinkedBlockingQueue,LinkedBlockingQueue很大,任务不多的情况下,不会因为任务多导致调用到拒绝策略。
参考文章:https://blog.csdn.net/fen_fen/article/details/124889762

解决:每次产生子线程时,重新设置线程容量

在这里插入图片描述

4.此时遇到又新的问题,主线程并没有等子线程完成才执行之后的代码,这导致推的数据有问题,但是时间到了,确实数据库数据正常,继续进行优化,改变拒绝策略CallerRunsPolicy,还有队列区别,参考https://blog.csdn.net/zyzn1425077119/article/details/121330271

最后没改,去掉之前等待所有子线程完成的方式,以及重复新建线程池,

主线程之后要将所有子线程产生数据推送,但是看最后日志,只推送了两条数据,也就是说没有拦截到所有的子线程就进入主线程了,但是看数据库数据,数据是全部产生了14条,开始研究怎么子线程完成才执行主线程~~~

(4)拒绝策略

AbortPolicy:默认策略,丢弃当前被拒绝的任务,抛出RejectedExecutionException异常。

DiscardPolicy: 这个拒绝策略就是丢弃当前被拒绝的任务,不抛出异常;

DiscardOldestPolicy: 这个拒绝策略就是将最先进入工作队列等待的任务丢弃,然后接受新的任务

CallerRunsPolicy: 这个拒绝策略就是谁调用了本线程池,则谁来执行该任务,保证的任务不会丢失。

shutdown只是阻止新任务加入到队列中,它仍然会让已经执行的以及在就绪队列中的任务接下来执行,只不过主线程不会阻塞下来等剩下的线程来执行。

在这里插入图片描述

三.解决结果

1.设置*CountDownLatch(**倒计时闩锁)*进行判断子线程是否完成,因为有try…catch,一开始是写在try里面的,发现子线程出错会一直等待,直到网关超时,改成finally,同时在catch设置参数,如果catch中有问题停止子线程,同时记录错误,在所有线程结束后,抛出错误不执行以下程序,同时主线程是事务管理,会回滚之前数据。

参考原文:https://www.cnblogs.com/BlogNetSpace/p/17126831.html 等待所有线程完成再执行,问题解决

//初始化线程池,这种比固定的更节约资源
public  static ThreadPoolExecutor executorService = new ThreadPoolExecutor(7, 20, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10));


@Override
@Transactional(rollbackFor = Exception.class)
public List<Attachment> replenishSign(PDFParamsReplenishVo pdfParamsReplenishVo) throws Exception {
    SecurityUser securityUser = (SecurityUser) SecurityUtils.getSubject().getPrincipal();
    log.info("用户信息:{}", JSON.toJSONString(securityUser));

    //业务代码。。。。。。。。。。。
    。。。。。。。。。。。。。。。
    。。。。。。。。。。。。

    Map<String,String> resp = new HashMap<>();
    String idNoStr = "";
    String dateStrr = "";

    //身份证号、日期生成base64
    //业务代码。。。。。。。。。。。。。。。。。。。。
    。。。。。。。。。。。。。。。。
    

    AtomicBoolean expFlag= new AtomicBoolean(false);
    //多线程调用第三方
    CountDownLatch latch = new CountDownLatch(fileLists.size());
    for (FileList fileList : fileLists) {
        String finalDateStrr = dateStrr;
        String finalIdNoStr = idNoStr;
        executorService.execute(() -> {
            QysSignVo qysVo = new QysSignVo(fileList.getAttachmentId(), null, signResult.getId(), fileList.getAttachmentId(),
                    fileList.getSignKeyWord(), pdfParamsReplenishVo.getBase64agent(), finalDateStrr, finalIdNoStr, securityUser.getAgentName(), securityUser.getAgentIDNO());
            try {
                this.qysSign(qysVo);
            } catch (Exception e) {
                expFlag.set(true);
                log.info("第三方异常信息:", e);
                Thread.currentThread().interrupt();
            }finally {
                latch.countDown();
            }
        });
    }
    latch.await();
    //线程完成后,判断是否多线程异常,抛出错误
    if(expFlag.get()){
        throw new BusinessException(Constants.ERROR_CODE,"签字异常,请稍后重试!");
    }
    
    //线程完成后执行后续推数据操作
    //获取线程过程中产生的数据
    List<Attachment> attachments = attachmentServiceClient.findByObjectId(signResult.getId());
    log.info("根据结果表查出数据为{}", JSONObject.toJSONString(attachments));
    ArrayList<AgentQueryVo>  agentQueryVo = new ArrayList<>();
    if(attachments != null && attachments.size() > 0){
        for (Attachment attachment : attachments) {
            //。。。。。组装数据
            agentQueryVo.add(agentQuery);
        }
        this.queryXG0000(agentQueryVo);
    }
    return attachments;
}

2.同时对于html最后生成图片的方法,在进入模块时放入前端一开始调用的方法里,进行异步调用存入redis缓存,预加载节约时间

/**
 * 将身份证、日期等图片base64存入缓存
 */
@Async
public void HtmltoImgInfo(String agentCode,String idNo){
    Map<String,String> resp = new HashMap<>();
        String idNoStr = "";
        String dateStrr = "";

        //身份证号生成base64
        Map<String,Object> variables = new HashMap<>(16);
        if(redisHelper.hasKey(agentCode+"_idNoStr")){
            idNoStr = redisHelper.get(agentCode+"_idNoStr",String.class);
        }else{
            //身份证生成图片
            variables.put("name",idNo);
            try{
                idNoStr = ImageUtlis.createImageByInfo("personal_infotoImg", variables, templateEngine, pdfFonts());
                log.info("生成身份证图片!");
            }catch (Exception e){
                throw new BusinessException(Constants.ERROR_CODE,"出现异常,请联系管理员!");
            }
            if(org.apache.commons.lang3.StringUtils.isNotEmpty(idNoStr)){
                redisHelper.set(agentCode+"_idNoStr",idNoStr,60 * 60);
            }
        }

        //日期生成base64
        String date = new SimpleDateFormat("yyyy年MM月dd日").format(new Date());
        if(redisHelper.hasKey(date + "_dateStr")){
            dateStrr = redisHelper.get(date + "_dateStr",String.class);
        }else{
            variables.put("name",date);
            try{
                dateStrr = ImageUtlis.createImageByInfo("personal_infotoImg", variables, templateEngine, pdfFonts());
                log.info("生成日期图片!");
            }catch (Exception e){
                throw new BusinessException(Constants.ERROR_CODE,"出现异常,请联系管理员!");
            }
            if(org.apache.commons.lang3.StringUtils.isNotEmpty(dateStrr)){
                redisHelper.set(date + "_dateStr",dateStrr,60 * 60);
            }
        }
        log.info("身份证base64:{}",idNoStr);
        log.info("日期base64:{}",dateStrr);
}

优化时间1分钟

四、注意事项

1、多线程以及异步调用方法时都不能拿到token,需要的个人信息传参带进方法。

2、如果使用submit(callback)返回方式,调用方法得在类的内部,不好用,可以创建cache同样可以拿到子线程最后结果。

List<Attachment> cache=new CopyOnWriteArrayList<>();
 executorService.execute(() -> {
    cache.add(方法返回结果)
 }
System.out.println(cache);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花花啊秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值