异步化
问题场景:调用的服务处理能力有限,或者接口的处理(或返回)时长较长时,就要考虑异步化。
异步化
异步:不用等一件事做完,就可以做另外一件事,等第一件事完成时,可以收到一个通知,通知你这件事做好了,就可以再进行后续处理。
业务流程分析
标准异步化的业务流程:
-
当用户要进行耗时很长的操作时,不需要再界面傻等,可以把任务保存到数据库中记录下来,可以给用户一个类似于进度条的任务处理进度,提高用户体验
-
当用户提交新的请求:
a. 任务提交成功:
i. 如果我们的程序还有多余的空闲线程,就可以立即处理这个请求
ii. 如果我们的程序的线程都在繁忙,无法继续处理,就把请求放入等待队列里
b. 任务提交失败:
i. 拒绝这个请求,再也不执行
ii. 通过保存到数据库中的记录来看到提交失败的任务,并且再程序空闲的时候,把请求从数据库中 取出来执行
-
我们的程序(线程)从任务队列中取出任务依次执行,每完成一件事就修改一下任务的状态
-
用户可以查询任务的执行状态,或者在任务执行成功或失败时能得到通知(发邮件、系统消息提示、短信)从而优化体验
-
如果我们要执行的任务非常复杂,包含很多环节,在每个小任务完成时,要在程序(数据库)记录任务的执行状态
问题:
-
任务队列的最大容量应该设置为多少?
-
程序怎么从任务队列中取出任务区执行?这个任务队列的流程怎么实现?怎么保证程序最多同时执行多少个任务
线程池
为什么需要线程池?
-
线程管理比较复杂(什么时候新增线程、什么时候减少空闲线程)
-
任务存取比较困难(什么时候接受任务、什么时候拒绝任务、怎么保证大家不抢到同一个任务)
线程池:帮助你轻松管理线程、协调任务的执行过程
线程池的实现
无需自己实现,如果是使用 Spring 框架,可以用 ThreadPoolTaskExecutor 配合 @Async 注解来实现(不推荐)
可以使用 Java 中 JUC 并发编程包中的 ThreadPoolExecutor 来实现非常灵活自定义线程池
ThreadPoolExecutor 线程池参数:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threaFactory, RejectedExecutionHandler handler )
线程池参数
如何确定线程池参数呢? => 结合实际的业务场景和系统资源来测试调整,不断优化
回归到业务中,考虑业务中最脆弱的环节(系统的瓶颈)在哪里?
corePoolSize:核心线程数(正式员工) => 正常情况下,我们的系统应该能同时工作的线程数(随时就绪的状态)
maximumPoolSize:最大线程数(哪怕任务再多,你也只能招这么多人) => 极限情况下,我们的线程池最多有多少个线程?
keepAliveTime:空闲线程存活时间 => 非核心线程在没有任务的情况下,过多久要删除(开除临时工)=> 释放无用的线程资源
TimeUnit:空闲线程存活时间单位,分钟、秒
WorkQueue:工作队列 => 用于存放给线程执行的任务,存一个队列的长度(一定要设置,不要说队列长度无限,因为也会占用资源)
ThreadFactory:线程工厂 => 控制每个线程的生成、线程的属性
RejectedExecutionHandler:拒绝策略 => 任务队列和可用线程数都满的情况下,我们采取什么措施,比如抛异常、自定义策略
资源隔离策略:比如主要任务(VIP 任务)一个对列,普通任务一个队列,保证这两个队列互不干扰
线程的工作机制
刚开始没有线程,也没有任何任务
来了一个线程,核心线程池有空闲进程(正式员工)=> 直接处理任务
又来一个任务,发现核心线程数还没用完,直接处理这个任务
又来两个任务,但是核心线程数已经用完了,任务被放到队列(最大长度workQueue 是 2)里等待,而不是新加线程
再来一个任务,但是任务队列已经满了(当前线程数 > corePoolSize = 2),已有任务数 = workQueue.size = 2,新增线程(maximumPoolSize = 4)来处理新任务,而不是丢弃任务
到任务 7,但是我们的任务队列已经满了、临时工也满了(当前线程数 = maximumPoolSize = 4),则调用 RejectedExecutionHandler 拒绝策略来处理多余的任务
如果当前线程数超过 corePoolSize,又没有新的任务给非核心线程来执行,那么等 KeepAliveTime 时间达到后,就会把这个线程释放。
一般情况下,任务分为 IO 密集型和计算密集型
计算密集型:吃 CPU,比如音视频处理、图像处理、数学计算等,一般是设置 corePoolSize 为 CPU 的核数 + 1,可以让每个线程都能利用好 CPU 的每个核,而且线程时间不用频繁切换(减少打架、减少开销)
IO 密集型:吃带宽/内存/硬盘的读写资源,corePoolSize 可以设置大一点,一般以 IO 的能力为主
开发实现流程
-
自定义线程池
/**
* @author zzt
*
* 自定义线程池
*/
@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;
}
}
-
提交任务到线程池
public class QueueController {
@Resource
private ThreadPoolExecutor threadPoolExecutor;
public void add(String name) {
CompletableFuture.runAsync(() -> {
System.out.println("任务执行中:" + name + ",执行人:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, threadPoolExecutor);
}
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);
}
}
反向压力
通过调用的服务状态来选择当前系统的策略(比如根据 AI 服务的当前任务队列数来控制我们系统的核心线程数),从而最大化利用系统资源
反向压力实际上是流量控制的一种解决方案,使调用方和处理方的能力相匹配,保护系统各个节点处于持续的正常工作状态