项目系统使用异步业务流程(线程池详细实现)

❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
🍊 记得点赞、收藏、评论⭐️⭐️⭐️
📣 认真学习!!!🎉🎉


  在最近做的一个BI项目中,需要调用第三方的AI大模型生成返回数据,但因为生成需要时间,所以出现了用户等待时间过长的问题。同时也要考虑到业务服务器可能面临大量请求处理,导致系统资源紧张,严重时甚至可能导致服务器宕机或无法处理新的请求。因此我们应当考虑异步化的解决方案。
 

异步化

1.介绍

同步:一件事情做完,再做另外一件事情(烧水后才能处理工作)。
异步:在处理一件事情的同时,可以处理另一件事情。当第一件事完成时,会收到一个通知告诉你这件事已经完成,这样就可以进行后续的处理(烧水时,可以同时处理其他工作。水壶上的蜂鸣器会在水烧好时发出声音,就知道水已经烧好了,可以进行下一步操作)。

 

2.异步业务流程分析

  在我们的系统中,主要是做的是一个智能BI系统,根据用户传递的数据生成图标。因此异步的流程是怎样的呢?用户在点击提交后就不需要在当前界面等待,他们可以直接回到主界面,或者继续填写下一个需要生成或分析的数据。提交完成后,他们回到主页,在主页上就可以看到图表的生成状态
  因此我们需要建立一个任务队列,将任务放到队列里面,排队等待AI服务器进行处理,那么怎么能够异步和上述要求呢?所以我们再次引入一个新的知识,“线程池”

 

线程池

1.为什么需要线程池?

1.线程的管理比较复杂(比如什么时候新增线程、什么时候减少空闲线程)
2.任务存取比较复杂(什么时候接受任务、什么时候拒绝任务、怎么保证大家不抢到同一个任务)
 

2.线程池的作用

  可以帮助你轻松管理线程、协调任务的执行过程。
扩充:可以向线程池表达你的需求,比如最多只允许四个人同时执行任务。线程池就能自动为你进行管理。在任务紧急时,它会帮你将任务放入队列。而在任务不紧急或者还有线程空闲时,它会直接将任务交给空闲的线程,而不是放入队列。

 

3.线程池的实现方式

1.在 Spring 中,我们可以利用 ThreadPoolTaskExecutor 配合 @Async 注解来实现线程池(不太建议)。
2.在 Java 中,可以使用JUC并发编程包中的 ThreadPoolExecutor,来实现非常灵活地自定义线程池。

 

4.线程池的参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {

●第一个参数 corePoolSize (核心线程数)。这些线程就好比是公司的正式员工,他们在正常情况下都是随时待命处理任务的。如何去设定这个参数呢?比如,如果我们的 AI 服务只允许四个任务同时进行,那么我们的核心线程数应该就被设置为2-4
●第二个参数 maximumPoolSize (最大线程数)。在极限情况下我们的系统或线程池能有多少个线程在工作。就算任务再多,你最多也只能雇佣这么多的人,因为你需要考虑成本和资源的问题。假设 AI 服务最多只允许四个任务同时执行,那么最大线程数应当设置为四。
●第三个参数 keepAliveTime (空闲线程存活时间)。这个参数决定了当任务少的时候,临时雇佣的线程会等待多久才会被剔除。这个参数的设定是为了释放无用的线程资源。你可以理解为,多久之后会“解雇”没有任务做的临时工。
●第四个参数 TimeUnit (空闲线程存活时间的单位)。将 keepAliveTime 和 TimeUnit 组合在一起,就能指定一个具体的时间,比如说分钟、秒等等。
●第五个参数 workQueue (工作队列),也就是任务队列。这个队列存储所有等待执行的任务。也可以叫它阻塞队列,因为线程需要按顺序从队列中取出任务来执行。这个队列的长度一定要设定,因为无限长度的队列会消耗大量的系统资源。
●第六个参数 threadFactory (线程工厂)。它负责控制每个线程的生成,就像一个管理员,负责招聘、管理员工,比如设定员工的名字、工资,或者其他属性。
●第七个参数 RejectedExecutionHandler (拒绝策略)。当任务队列已满的时候,我们应该怎么处理新来的任务?是抛出异常,还是使用其他策略?比如说,我们可以设定任务的优先级,会员的任务优先级更高。如果你的公司或者产品中有会员业务,或者有一些重要的业务需要保证不被打扰,你可以考虑定义两个线程池或者两个任务队列,一个用于处理VIP任务,一个用于处理普通任务,保证他们不互相干扰,也就是资源隔离策略。

备注:
线程池的设计主要分为 IO 密集型和计算密集型。
  对于计算密集型的任务,它会大量消耗 CPU 资源进行计算,例如音视频处理、图像处理、程序计算和数学计算等。要最大程度上利用 CPU,避免多个线程间的冲突,一般将核心线程数设置为 CPU 的核数加一。这个“加一”可以理解为预留一个额外的线程,或者说一个备用线程,来处理其他任务,以防止以外发生。这样做可以充分利用每个 CPU 核心,减少线程间的频繁切换,降低开销。在这种情况下,对 maximumPoolSize 的设定没有严格的规则,一般可以设为核心线程数的两倍或三倍。
  而对于 IO 密集型的任务,它主要消耗的是带宽或内存硬盘的读写资源,对 CPU 的利用率不高。比如说,查询数据库或等待网络消息传输,可能需要花费几秒钟,而在这期间 CPU 实际上是空闲的。在这种情况下,可以适当增大 corePoolSize 的值,因为 CPU 本来就是空闲的。比如说,如果数据库能同时支持 20 个线程查询,那么 corePoolSize 就可以设置得相对较大,以提高查询效率。建议根据 IO 的能力来设定。在这种情况下,对 maximumPoolSize 的设定不易过大,因为IO密集型的任务对CPU要求不大,设置太大,会造成上下文切换过于频繁。

 

5.线程池的开发

  如果要创建线程池,首先需要在你的项目加上一个ThreadPoolExecutorConfig.java(线程池配置类)。

@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadFactory threadFactory = new ThreadFactory() {
            private int count = 1;

            @Override
            public Thread newThread(@NotNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("线程" + count);
                count++;
                return thread;
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), threadFactory);
        return threadPoolExecutor;
    }
}

 

6.线程池的测试

开始编写队列测试的代码。

public class QueueController {

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    @GetMapping("/add")
    public void add(String name) {
        CompletableFuture.runAsync(() -> {
            log.info("任务执行中:" + name + ",执行人:" + Thread.currentThread().getName());
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, threadPoolExecutor);
    }

    @GetMapping("/get")
    public String get() {
        Map<String, Object> map = new HashMap<>();
        int size = threadPoolExecutor.getQueue().size();
        map.put("队列长度", size);
        long taskCount = threadPoolExecutor.getTaskCount();
        map.put("任务总数", taskCount);
        long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
        map.put("已完成任务数", completedTaskCount);
        int activeCount = threadPoolExecutor.getActiveCount();
        map.put("正在工作的线程数", activeCount);
        return JSONUtil.toJsonStr(map);
    }
}

之后调用接口进行测试
在这里插入图片描述
由于现在刚开始,没有任何的线程和任务,当有新任务进来,发现当前员工数量还未达到设定的正式员工数(corePoolSize = 2),则会直接增聘一名新员工来处理这个任务,在后端控制台查看信息:
在这里插入图片描述
在 add接口,发送一个任务2;
又来一个任务2,发现当前员工数量还未达到设定的正式员工数(corePoolSize = 2),则会再次增聘一名新员工来处理这个任务,在后端控制台查看信息:
在这里插入图片描述在get接口查看队列长度为 0,之前的任务都没有进入队列里,因为都交给线程 1、线程 2了。
在这里插入图片描述在 add接口,发送一个任务3;
又来了一个任务3,但是正式员工数已经达到上限(当前线程数 = corePoolSize = 2),这个新任务将被放到等待队列中(最大长度 workQueue.size 是 4) ,而不是立即增聘新员工,在后端控制台查看信息,发现没有线程去执行:
在这里插入图片描述
在 add接口,发送任务4、任务5、任务6;
在 get接口查看队列长度为 4,现在队列满了,正式员工也满了:
在这里插入图片描述
在 add接口,发送任务7;
再来一个任务7,我们将增设新线程(最大线程数 maximumPoolSize = 4)来处理任务,而不是选择丢弃这个任务,在后端状态查看信息:
在这里插入图片描述
就很好的证明了一点。如果新来一个任务,这时候是先执行任务队列中前面的,还是说先执行新加入的?这种情况下,他就会先执行这个任务7。
在 add接口,发送任务8;
再来一个任务8,我们将增设新线程来处理任务,在后端状态查看信息,这个任务由线程 4 处理:
在这里插入图片描述
现在队列满了,正式员工满了,临时工也满了;
可以到 get接口查看:
在这里插入图片描述
在 add接口,再发送一个任务:任务9;就会显示不让你提交了,直接被拒绝掉,报错了,回到后端控制台也是报错。在这里插入图片描述
 

7.线程池运用到项目之中

使用CompletableFuture运行一个异步任务,之后可以在其中执行自己的代码即可。

@Resource
private ThreadPoolExecutor threadPoolExecutor;

CompletableFuture.runAsync(() -> {
            // 调用 AI
            String result = aiManager.doChat(biModelId, userInput.toString());
            String genChart = splits[1].trim();
            String genResult = splits[2].trim();
            Chart updateChartResult = new Chart();
            updateChartResult.setId(chart.getId());
            updateChartResult.setGenChart(genChart);
            updateChartResult.setGenResult(genResult);
            // todo 建议定义状态为枚举值
            updateChartResult.setStatus("succeed");
            boolean updateResult = chartService.updateById(updateChartResult);
            if (!updateResult) {
                handleChartUpdateError(chart.getId(), "更新图表成功状态失败");
            }
        }, threadPoolExecutor);
BiResponse biResponse = new BiResponse();
biResponse.setChartId(chart.getId());
return ResultUtils.success(biResponse);
  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李奕赫揍小邰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值