springboot 多线程查询

背景

  • 框架:mybatis-flex + springboot3.2
  • 原业务逻辑代码如下:
public class VehicleCountServiceImpl{
    ........
    @Override
    public List<GetMapTopCountVo> selectMapTopCount(GetMapTopCountDto mapTopCountDto) {
        ......
        // TODO 慢查询待优化
        // 未排序:key(键)为道路编号,value(值)为拥堵次数
        Map<Long, Long> unSortMap = new HashMap<>();
        List<VehicleCount> vehicleCountList = QueryChain.of(VehicleCount.class)
                .select(sum(VehicleCount::getUpCount).as(VehicleCount::getUpCount))
                .select(sum(VehicleCount::getDownCount).as(VehicleCount::getDownCount))
                .where(VehicleCount::getRoadId).in(roadIdList)
                .and(VehicleCount::getDatetime).between(startTime, endTime)
                .groupBy(VehicleCount::getRoadId)
                .orderBy(VehicleCount::getRoadId).asc()
                .list();
        for (int i = 0; i < roadIdList.size(); i++) {
            long upCount = ObjUtil.isNotEmpty(vehicleCountList) ? vehicleCountList.get(i).getUpCount() : 0L;
            long downCount = ObjUtil.isNotEmpty(vehicleCountList) ? vehicleCountList.get(i).getDownCount() : 0L;
            unSortMap.put(roadIdList.get(i), upCount + downCount);
        }
        ......
    }
    ......
}
  • 以上代码对于百万数据量、roadIdList.size()=10的查询需要10秒以上。sql逻辑较为简单,因此无法通过sql优化提高查询效率,故采用java多线程,为每个roadId运行一个线程进行查询。

代码优化

  • ThreadConfig
    添加@EnableAsync注解,否则@Async注解无效
@Slf4j
@EnableAsync
@Configuration
public class ThreadConfig implements AsyncConfigurer {
    ......
    /**
     * 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
     * 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
     * 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
     */

    // 核心线程池大小
    @Value("${async.executor.thread.core-pool-size:20}")
    private int corePoolSize;

    // 最大可创建的线程数
    @Value("${async.executor.thread.max-pool-size:50}")
    private int maxPoolSize;

    // 队列最大长度
    @Value("${async.executor.thread.queue-capacity:1024}")
    private int queueCapacity;

    // 线程池维护线程所允许的空闲时间
    @Value("${async.executor.thread.keep-alive-seconds:300}")
    private int keepAliveSeconds;

    //线程池名前缀
    @Value("${async.executor.thread.threadNamePrefix:vehicleCount-}")
    private String threadNamePrefix;


    @Bean(name = "vehicleCountThreadPool")
    public ThreadPoolTaskExecutor vehicleCountThreadPool()
    {
        // 阿里巴巴推荐使用 ThreadPoolTaskExecutor 而非 Executors 自定义线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务(无线程可用)的处理策略
        // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}
  • VehicleCountMapper
    之所以不将selectCount方法放到service层,是因为Spring AOP是基于动态代理的,而调用该方法的方法也在service层,spring同一类中的方法调用不走动态代理,这样会导致@Async注解失效
@Mapper
public interface VehicleCountMapper extends BaseMapper<VehicleCount> {

    // 异步查询,使用自定义线程池
    // 不能将该方法与调用该方法的方法放在同一个类中,
    // 否则无法异步执行,因为Spring AOP的原理是基于代理对象的
    @Async("vehicleCountThreadPool")
    default Future<VehicleCount> selectCount(Long roadId, LocalDateTime startTime, LocalDateTime endTime){
        VehicleCount vehicleCount = QueryChain.of(VehicleCount.class)
                .select(sum(VehicleCount::getUpCount).as(VehicleCount::getUpCount))
                .select(sum(VehicleCount::getDownCount).as(VehicleCount::getDownCount))
                .where(VehicleCount::getRoadId).eq(roadId)
                .and(VehicleCount::getDatetime).between(startTime, endTime)
                .one();
        System.out.println(Thread.currentThread());
        vehicleCount.setRoadId(roadId);
        return CompletableFuture.completedFuture(vehicleCount);
    }
}
  • VehicleCountServiceImpl
    异步调用Mapper层封装好的selectCount方法
public class VehicleCountServiceImpl{
    ........
    @Override
    public List<GetMapTopCountVo> selectMapTopCount(GetMapTopCountDto mapTopCountDto) {
        ......
        // 采用多线程并发查询,提高查询效率
        // 未排序:key(键)为道路编号,value(值)为拥堵次数
        Map<Long, Long> unSortMap = new HashMap<>();

        // 并发查询结果集
        List<Future<VehicleCount>> futureList = new ArrayList<>(roadIdList.size());

        for (long roadId : roadIdList) { // 每个道路并发查询
            Future<VehicleCount> vehicleCountFuture = mapper.selectCount(roadId, startTime, endTime);
            futureList.add(vehicleCountFuture);
        }

        for (Future<VehicleCount> future : futureList) {
            while (true) {
                // CPU高速轮询:每个future都并发轮循,判断完成状态然后获取结果,
                // 有多个future在高速轮询,完成一个future的获取结果,就关闭一个轮询
                try {
                    if (future.isDone() && !future.isCancelled()) {
                        // 获取future成功完成状态,如果想要限制每个任务的超时时间,
                        // 取消本行的状态判断+future.get(1000*1, TimeUnit.MILLISECONDS)+catch超时异常使用即可。

                        VehicleCount vehicleCount = future.get();//获取结果
                        long upCount = ObjUtil.isNotEmpty(vehicleCount) ? vehicleCount.getUpCount() : 0L;
                        long downCount = ObjUtil.isNotEmpty(vehicleCount) ? vehicleCount.getDownCount() : 0L;
                        unSortMap.put(vehicleCount.getRoadId(), upCount + downCount);
                        break; // 当前future获取结果完毕,跳出while
                    }
                    //每次轮询休息1毫秒(CPU纳秒级),避免CPU高速轮循耗空CPU
                    Thread.sleep(1);
                } catch (InterruptedException | ExecutionException e){
                    throw new RuntimeException(e);
                }
            }
        }
        ......
    }
    ......
}
  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值