多线程拉取+kafka推送
1 多线程
在本次需求中,多线程部分我主要考虑了一个点,就是线程池的配置如何最优。因为数据量级比较大,所以这个点要着重处理,否则拉取的时间会非常长或者是任务失败会比较频繁;
因为数据的量级比较大,所以我决定进行分组,然后循环,一个组作为一个任务批次丢到线程池中,当该组拉取结束后,把该组拉取的结果进行数据推送。可以理解为我们采用了小步快跑的方式;
在这个过程中我们需要考虑的点有 一、每组多少条任务比较合适、二、如何感知该组是否已经拉取完成,因为它决定了我们推送的时机;
首先是每组多少条任务比较合适这个问题;
我的策略是每组的任务数和核心线程数保持一致,然后把工作队列的大小也设置为核心线程数的大小并将非核心线程数置为0。所以这就必须要处理线程池的配置问题:
@Bean("privatePoolExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数-CPU密集型
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);
//配置最大线程数: 其值减去核心线程数就是非核心线程数
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() + 1);
//配置队列大小
executor.setQueueCapacity(Runtime.getRuntime().availableProcessors() + 1);
//线程池维护线程所允许的空闲时间
executor.setKeepAliveSeconds(30);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("BIJob-privatePoolExecutor-");
//设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
// 拒绝策略:CALLER_RUNS的含义是不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
首先可以看到我把核心线程数置为CPU核心数+1,非核心线程数置为0且阻塞队列大小也设置为CPU核心数+1,这是一个非常定制化的配置。因为这建立在我能确定该工作队列的大小能保证我每组的任一条任务都不会去执行拒绝策略[介绍1],我之所以可以确定的原因就是因为我把每组的任务数设定为了CPU核心数+1,这样即使在一个循环中所有的线程都被阻塞时,我的工作队列也能确保任务的不丢失;
接下来我们来看一下参数为什么这样配置;
首先是核心线程数为什么要配置成这么多,依据是什么;
这个点是我通过查询的结果,一种是加1这种叫CPU密集型任务,还有一种是乘2这种叫做IO密集型。之所以选择前者是因为我发现如果我采用后者那我数据拉取失败的概率较大,每次都有超时。所以我是被迫的;
接下来我们介绍第二个大问题,也就是如何感知该组的任务已经执行结束了呢?
此处需要我们使用一个类叫做CountDownLatch,伪代码:
// 将阻塞次数设定为数组长度
CountDownLatch latch = new CountDownLatch(loadList.size);
// 单次循环
loadList.forEach((value) -> privatePoolExecutor.execute(() -> {
try{
pushList.add(loadData(value));
}catch(Ex e){
log.err(e);
}finally{
// 执行一次,次数-1
latch.countDown();
}
}));
try{
// 不为0就一直阻塞在这里
latch.await();
}catch(Ex e){
log.err(e);
}
// 为0后开始推送
pushData(pushList);
使用该类之后,我们就可以感知到该组任务在什么时候执行结束了;
2 kafka部分
这部分没有什么特殊的,就是简单的导入依赖然后使用boot封装的kafkaTemplate这个Bean来推送消息;
主要介绍一下它的属性配置:
# kafka配置
# kafka服务器集群
spring.kafka.bootstrap-servers=xxx:9092,xxx:9092,xxx:9092
# 消息压缩算法,批量时提效
spring.kafka.producer.compression-type=lz4
# 生产者日志记录ID
spring.kafka.producer.client-id=xxx
# ack确认方式,0发送给主broker直接返回、1发送后要确认主broker入盘,-1发送后确认主broker入盘且从broker也入盘
spring.kafka.producer.acks=1
# 批量消息大小16KB,其中满足batch-size或linger.ms其中任意一个条件时会自动触发消息推送
spring.kafka.producer.batch-size=16384
# 生产端缓冲区大小32M
spring.kafka.producer.buffer-memory=33554432
# 失败重试次数
spring.kafka.producer.retries=3
# 提交延时,接收到主broker消息后延迟1s发送下一个消息
spring.kafka.producer.properties.linger.ms=1000
# 单条消息最大字节数10M
spring.kafka.producer.properties.max.request.size=10485760
3 介绍
介绍1:
说到这里我们就必须要知道线程池的运行机制,它里面是支持corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler(jdk1.8-帮助文档搜ThreadPoolExecutor)这七大参数的;
但是呢,在这里我们只介绍corePoolSize、maximumPoolSize、workQueue、handler这四个,它们四个之间的在有界工作队列的联动机制:
1 线程池收到任务后,首先判断此时是否存在空闲的核心线程,如果有就启动核心线程然后处理。如果没有就把任务放到工作队列中;
2 如果工作队列没有满就继续插入,如果满了就需要判断是否存在非核心线程,如果存在就启动,然后执行任务。如果不存在就执行拒绝策略;
说到这里,这句话就非常容易理解了;