记一次springcloud多线程请求API的实践

前因

最近在做一些业务数据大屏统计,springcloud 架构,统计代码中需要调用别的服务获取数据,在传统的同步请求下获取所有的数据需要20-30s,有时候时间更长还会导致超时,故想到了使用mq和多线程2种解决方案。

由于任务比较急,而且别的服务并不是自己项目组负责,所以使用mq的方式就显得麻烦许多,最后评估后决定使用多线程的请求方式进行请求拆解优化,将一次请求分成多次请求,每次携带的一定个数的参数,使用多线程后,请求时间由20-30s降到了5-7s。

下面讲解大概的实现流程。

 

实现

1、避免在service层直接定义线程池,手动配置一个线程池。

package com.xxx.xxx.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 异步线程池配置
 * 可直接注入xxxExecutor,或者通过@Async("xxxExecutor")引用
 *
 * @author TianXieZuoMaiKong
 */
@Slf4j
@EnableAsync
@Configuration
public class ExecutorConfig implements AsyncConfigurer {
    /**
     * 阻塞因子
     */
    private static final double BLOCKING_COEFFICIENT = 0.9;

    private static final int CPU_PROCESSORS_COUNT = Runtime.getRuntime().availableProcessors();

    /**
     * 默认连接池
     * 用于CPU密集型任务
     */
    @Bean("taskExecutor")
    @Override
    public TaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("thread-async-");
        executor.setCorePoolSize(CPU_PROCESSORS_COUNT + 1);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.initialize();
        return executor;
    }

    /**
     * 耗时任务连接池
     * 用于IO密集型任务
     */
    @Bean("threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor timeConsumingExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("thread-io-");
        executor.setCorePoolSize(CPU_PROCESSORS_COUNT);
        executor.setMaxPoolSize((int) (CPU_PROCESSORS_COUNT / (1 - BLOCKING_COEFFICIENT)));
        executor.setQueueCapacity(200);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return ((throwable, method, objects) -> log.error("Occurring an exception during async task [{}] invocation!\n{}", method.getName(), throwable.getMessage()));
    }
}

2、定义一个任务包装类,实现Callable接口并重写其call方法,用于回调接收查询返回数据。

package com.xxx.xxx.pojo;

import com.xxx.xxx.api.OrderServiceApi;

import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 * 根据订单号获取订单信息 handler
 *
 * @author TianXieZuoMaiKong
 */
public class OrderInfoHandler implements Callable<List<Map<String, Object>>> {
    private Map<String, Object> paramMap;
    private OrderServiceApi serviceApi;

    /**
     * 多参构造函数
     *
     * @param paramMap      订单号map参数。一次查询2000个
     * @param serviceApi serviceApi,这里不想再通过反射去拿了,所以干脆当参数传进来
     */
    public OrderInfoHandler(Map<String, Object> paramMap, OrderServiceApi serviceApi) {
        this.paramMap = paramMap;
        this.serviceApi = serviceApi;
    }

    @Override
    public List<Map<String, Object>> call() {
        return serviceApi.findOrderInfo(paramMap);
    }
}

3、使用while循环拆解参数个数,一次传2000个订单号查询,订单号循环传完为止。

import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.xxx.xxx.api.OrderServiceApi;


/**
 * 业务处理类
 *
 * @author TianXieZuoMaiKong
 */
@Slf4j
@AllArgsConstructor
@Service
public class DataScreenService {
    private final OrderServiceApi orderServiceApi;
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
    

    /**
     * 批量查询订单信息
     *
     * @param orderIds 要查询的订单id
     */
	private List<Map<String, Object>> getOrderInfoList(List<String> orderIds) {
        List<Future<List<Map<String, Object>>>> futureList = new ArrayList<>();
 
        int size = orderIds.size();
        int orderNum = 0;
        while (orderNum < size) {
            // 分批查询,一次最多能查2000个元素,超过则in查询报错
            int temp = Math.min((orderNum + 2000), size);
            Map<String, Object> paramMap = MapBuilder.<String, Object>init("orderIds", orderIds.subList(orderNum, temp)).getMap();
            OrderInfoHandler  orderInfoListHandler = new OrderInfoHandler(paramMap, orderServiceApi);
            // 这里存一下Future 任务,用于下面接收回调数据。调用 submit 方法,让线程池安排去执行
            futureList.add(threadPoolTaskExecutor.submit(orderInfoListHandler));

            log.info("while 执行orderNum:[{}] 到 temp:[{}] 的查询。", orderNum, temp);
            orderNum = temp;
        }

        List<Map<String, Object>> orderInfoList = new ArrayList<>(size);
        for (Future<List<Map<String, Object>>> f : futureList) {
            List<Map<String, Object>> r = null;
            try {
                r = f.get(30, TimeUnit.SECONDS);
            } catch (InterruptedException | ExecutionException e) {
                log.error("#getorderInfoListList 多线程Future执行异常:{}", e.getMessage());
            } catch (TimeoutException e) {
                log.error("#getorderInfoListList 多线程Future超时。线程名:[{}]", Thread.currentThread().getName());
            }
            log.info("## get之后拿到结果size:[{}]", r.size());
            orderInfoList.addAll(r);
        }

        return orderInfoList;
    }

}

到这里,最后返回的 orderInfoList 就是多线程请求回来的数据集合,在自己的业务逻辑里去处理这个集合就可以了。

 

 

学无止境,生生不息。

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本帖最后由 Mruos 于 2019-2-22 13:53 编辑 bl-api-cloud,轻云服务端 为轻量级可扩展的API服务端框架,主要用于响应http请求,开发者可通过开发自己的功能插件(.dll)进行加载以达到扩展。 丨前言 之前发过帖子《【框架】bl-api-cloud,高性能可扩展的API服务器》https://bbs.125.la/forum.php?mod=viewthreadtid=14191272extra= 且获得了不少网友的认可,最近几天就行了大的优化、更新,并全部开源出来。丨用途 为其他程序、应用,提供便捷的http接口搭建功能,开发者从而不再需要使用大型、复杂框架。 (毕竟很多时候为了一个小接口不值得用主流语言的比如javaSpring或python的Flask去搭建服务端) 举个最简单的例子,有时在授权、防破解等应用中,我们需要获取准确的北京时间。一般我们为了方便会通过第三方来获取:找个提供北京时间的第三方web页面;写个模块或DLL,提取页面里的北京时间;弊端:如果第三方页面出现问题,或web页面源码有变动,那么获取就会失败,进而影响了所有使用了此模块/DLL的程序。其实,很简单,我们让自己的服务器自动同步了时间(一般都默认开启着),然后提供一个http接口即可,用自己的最稳定。网页_访问 (“http://192.168.1.1:6680/api/sup/bjtime/10 ”)我自己使用本框架已应用的领域: 如取北京时间等实用接口;搭建自己的网络验证系统(用户注册、登录、程序使用授权);搭建微信公众号后台程序(没错,完全没问题);web页面搭建,只要有能力,你可以搭建出任意页面;(当然搭建企业级或大型复杂些的建议使用主流语言的主流框架,毕竟使用这个费劲死了,需要自己建设的方面太多)丨特色 1、通讯组件使用的为HPsocket,强大、稳定 HP-Socket,是一套通用的高性能 TCP/UDP/HTTP 通信框架,包含服务端组件、客户端组件和Agent组件,广泛适用于各种不同应用场景的 TCP/UDP/HTTP 通信系统。 其Server 组件:基于IOCP / EPOLL通信模型,并结合缓存池、私有堆等技术实现高效内存管理,支持超大规模、高并发通信场景。 应用程序能够根据不同的容量要求、通信规模和资源状况等现实场景调整 HP-Socket 的各项性能参数(如:工作线程的数量、缓存池的大小、发送模式和接收模式等),优化资源配置,在满足应用需求的同时不必过度浪费资源。 2、双服务端支持(http、https) 服务端启动端口自定义,默认http服务端80端口,https服务端443。当然本框架出发定性小众领域使用,可以设置其他端口,以免占用重要的web框架接口。 3、扩展便捷 扩展(插件)为DLL文件,只需放入根目录下的/plugins 即可。DLL支持热加载与释放,无需终止服务端主程序即可进行DLL更新。 开发模板简单,一目了然,因为全部开源,开发者依然可以自主向插件传递更多可操作的主程序指针(通常模板自带功能足够使用了)。 每个插件都有自己的http访问请求处理域且可以是多个,插件之间不会互相冲突; 在两个示例demo中,对于/api/sup/bjtime 根址的http请求,服务端只push到了bjtime.dll插件,对于/web 的http请求,服务端只push到了web-demo.dll插件。 提供有2个扩展开发模板demo: (1)bjtime 示例如何返回Get请求,功能性代码不足20行即可实现; (2)web-demo 示例web页面返回,提供Get静态web目录文件回执和向服务端Post数据处理示例; 1、访问页面(http与https) 2、post数据 4、集成实用便捷功能 自带集成多色日志输出、debug消息模式、访问频率保护等功能; (1)多色日志输出 主程序的日志消息窗口,可以对应不同的日志显示不同的颜色,方便开发者一目了然的找到查看消息。如: 灰色(gray)的为debug消息; 红色(red)的为异常或错误信息; 绿色(green)的为收到的事件; 黄色(yellow)为重要系统消息; 当然可以自己设置其他颜色,以及如何输出。 注意: 多色输出使用的是超级编辑框组件,在高并发下是否对程序效率影响有待考证(组件可能拖累程序),请自行进行取舍、替换。 (2)日志录 主程序集成一个简单日志录模块,主程序运行每一次运行后都会在/log 目录创建一个日志文件(名称以运行开始时间-运行结束时间.txt为名,方便开发者查找时段消息)。 开发者也可以在自己开发的插件中加入独立的日志录。 (3)debug消息模式 主程序启动后通过输入sys debug on、sys d

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值