大批量数据导出处理

场景:最近接到领导的任务,导出近百万的车辆数据,其中要经过三个接口的处理,导致一辆车最少需要1.17秒,按这种情况推算,1000000 * 1.17 / 3600 = 325 小时。。

思路 :当数据库查询结果集数量过大时,必使用多线程,并且根据手头服务器的配置,规划多线程的线程数。考虑到写入文件数据过多,可以选择分批导出多个文件。

实现:代码里配的线程数为16,线程数 = CPU核数 + 1(其实根据情况可以更多,没敢太多),且 数组切割为10000一批次。如此处理,20小时之内完成百万导出。

注意:一定要注意内存释放,省的full GC OMM

           如写入同一个文件加锁;

           记得打日志;

           代码执行性能优化。(速度多优化 1mm,对于整个执行时间就会优化好几个小时)

代码如下:

 

package com.foton.m2m.iov.standard.controller.openapi;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.basic.annotation.DisplayName;
import org.springframework.basic.domain.Pair;
import org.springframework.basic.domain.map.Address;
import org.springframework.basic.orm.DataAccessException;
import org.springframework.basic.service.ServiceException;
import org.springframework.basic.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.foton.m2m.basic.annotation.IgnoreHibernateSession;
import com.foton.m2m.basic.domain.vo.ApiResponse;
import com.foton.m2m.basic.map.component.MapService;
import com.foton.m2m.basic.utils.ContextUtil;
import com.foton.m2m.iov.basic.component.CapacityStatsService;
import com.foton.m2m.iov.basic.component.FenceRecordService;
import com.foton.m2m.iov.basic.controller.openapi.OpenApiBaseController;
import com.foton.m2m.iov.basic.domain.SimpleDevice;
import com.foton.m2m.iov.basic.model.CapacityStats;
import com.foton.m2m.iov.standard.Constants;
import com.google.common.collect.Lists;

/**
 * 平台报警信息
 */
@DisplayName("平台报警信息")
@Controller(Constants.STANDARD_BEAN_NAME_PREFIX + "OpenAPIPlatformAlarmController")
@RequestMapping("/openapi/iov/business/platformAlarm")
@IgnoreHibernateSession
public class PlatformAlarmController extends OpenApiBaseController {
    @Autowired
    @Qualifier(com.foton.m2m.cbm.manage.Constants.JDBC_TEMPLATE)
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private MapService mapService;
    @Autowired
    private transient CapacityStatsService capacityStatsService;
    @Autowired
    private transient FenceRecordService fenceRecordService;
    @Autowired
    @Qualifier(com.foton.m2m.iov.basic.Constants.TASK_EXECUTOR)
    private Executor executor;

    @RequestMapping(value = "/demo" + Constants.SPRING_MVC_JSON_SUFFIX, method = RequestMethod.GET)
    public @ResponseBody ApiResponse<Map<String, Object>> demo(@RequestParam(value = "startTime", required = false) String startTime, @RequestParam(value = "endTime", required = false) String endTime, @RequestParam(value = "companyId") Long companyId, HttpServletRequest request) {
        String sql = "select d.did from iov_biz_car_info c join iov_biz_device d on d.iov_biz_car_info_id = c.id and d.is_primary = 1 where d.cbm_mag_company_id = " + companyId + " order by d.lasttime desc";
        List<String> dids = jdbcTemplate.queryForList(sql, String.class);

        Date start = new Date();
        Date end = new Date();

        if (StringUtils.isNotEmpty(startTime)) {
            start = DateUtils.parseDateQuietly(startTime, DateUtils.DEFAULT_DATE_FORMAT);
        }

        if (StringUtils.isNotEmpty(endTime)) {
            end = DateUtils.parseDateQuietly(endTime, DateUtils.DEFAULT_DATE_FORMAT);
        }

        Date startDate = DateUtils.truncateDayOfMonth(start);
        Date endDate = DateUtils.setEndMonth(end);

        Object[] headers = { "vin", "车牌号", "月份", "行车路线Top1", "行车路线Top2", "行车路线Top3", "停车地点Top1", "停车地点Top2", "停车地点Top3", "车辆足迹Top1", "车辆足迹Top2", "车辆足迹Top3", "加油站Top1", "加油站Top2", "加油站Top3" };
        List<Object> headerData = Arrays.asList(headers);

        String filePath = "/data-nfs/iov/upload/temp/zzz/";
        //filePath = "attached/zzz/";
        String lineName = "TRAVEL_ROUTE_ANALYSIS_";
        write2TempFile(dids, startDate, endDate,  headerData, filePath, lineName);

        return ApiResponse.success("ok");
    }

    private void write2TempFile(List<String> dids,Date startDate,Date endDate, List<Object> headerData, String filePath, String lineName) {
        try {
            Semaphore semaphore = new Semaphore(16, true);
            List<List<String>> list = Lists.partition(dids, 10000);// 10000个数据分组执行
            semaphore.acquire();
            for (int i = 0; i < list.size(); i++) {
                List<String> batch = list.get(i);
                executor.execute(() -> {
                List<List<Object>> datalist = new ArrayList<>();
                batch.stream().forEach(did -> {
                    long start1 = System.currentTimeMillis();
                    // 按月获取车辆画像数据、故障数据
                    Iterator<Pair<Date, Date>> iter = DateUtils.getIterator(startDate, endDate, DateUtils.DateType.MONTHLY, false);
                    SimpleDevice deviceInfo = deviceMapService.findOne(did);
                    String vin = deviceInfo.getFramenum();
                    while (iter.hasNext()) {
                        Pair<Date, Date> current = iter.next();
                        // 封装运营时间
                        String run_month = DateUtils.toString(current.getKey(), DateUtils.DEFAULT_DATE_FORMAT);
                        // 车辆画像数据(前五的停车地点、前五的经常行驶路线、前五的车辆足迹)
                        CapacityStats capacityStats = capacityStatsService.findAll(Lists.newArrayList(did), current.getKey(), current.getValue());

                        if (capacityStats != null) {
                            List<Pair<String, Integer>> stops = capacityStats.getStops();// 停车地点
                            List<Pair<String, Integer>> cities = capacityStats.getCities();// 车辆足迹
                            List<Pair<String, Integer>> streets = capacityStats.getStreets();// 经常行驶路线

                            List<Object> data = new ArrayList<>();
                            data.add(StringUtils.isBlank(deviceInfo.getFramenum()) ? "-" : deviceInfo.getFramenum());// vin
                            data.add(StringUtils.isBlank(deviceInfo.getLpn()) ? "-" : deviceInfo.getLpn());// lpn
                            data.add(StringUtils.isBlank(run_month) ? "-" : run_month);// 月份
                            buildMapByTop3(streets, data, false);// 行车路线
                            buildMapByTop3(stops, data, true);// 停车地点
                            buildMapByTop3(cities, data, false);// 车辆足迹
                            buildGasMapByTop3(did, current.getKey(), current.getValue(), data);

                            datalist.add(data);
                        }
                    }

                    logger.info("task:{}=====Completed car vin {} ========= totalTime:{}.",Thread.currentThread().getName(), vin, System.currentTimeMillis() - start1);
                });

                File file = createCSVFile(headerData, datalist, filePath, lineName+"("+datalist.size()+")"+ System.currentTimeMillis());
                });
            }

            semaphore.acquire(16);
            return;
        } catch (DataAccessException | InterruptedException e) {
            logger.error(e.getMessage(), e);
            throw new ServiceException("please check template!",e);
        }
    }

    private void buildGasMapByTop3(String did, Date start, Date end, List<Object> data) {
        List<Map<String, Object>> mapList = fenceRecordService.findTopFenceByDids(Lists.newArrayList(did), (short) 21, start, end);
        if (mapList != null) {
            if (mapList.size() == 0) {
                data.add("-");// Top1
                data.add("-");// Top2
                data.add("-");// Top3
            } else if (mapList.size() == 1) {
                data.add(MapUtils.getString(mapList.get(0), "name","-"));// Top1
                data.add("-");// Top2
                data.add("-");// Top3
            } else if (mapList.size() == 2) {
                data.add(MapUtils.getString(mapList.get(0), "name","-"));// Top1
                data.add(MapUtils.getString(mapList.get(1), "name","-"));// Top2
                data.add("-");// Top3
            } else if (mapList.size() >= 3) {
                data.add(MapUtils.getString(mapList.get(0), "name","-"));// Top1
                data.add(MapUtils.getString(mapList.get(1), "name","-"));// Top2
                data.add(MapUtils.getString(mapList.get(2), "name","-"));// Top3
            }
        }else{
            data.add("-");// Top1
            data.add("-");// Top2
            data.add("-");// Top3
        }
    }

    private void buildMapByTop3(List<Pair<String, Integer>> list, List<Object> data, Boolean isStops) {
        if (isStops) {
            for (Pair<String, Integer> pair : list) {
                String lnglats = pair.getKey();
                if (lnglats == null || !lnglats.contains("-")) {
                    continue;
                }
                String[] arr = pair.getKey().split("-");
                Address addr = mapService.inverseGeocoding(arr[0], arr[1]);
                String address = "-";
                if (addr != null) {
                    address = addr.getFormattedAddress();
                }
                pair.setKey(address);
            }
        }
        if (list.size() == 0) {
            data.add("-");// Top1
            data.add("-");// Top2
            data.add("-");// Top3
        } else if (list.size() == 1) {
            data.add(StringUtils.isBlank(list.get(0).getKey()) ? "-" : list.get(0).getKey());// Top1
            data.add("-");// Top2
            data.add("-");// Top3
        } else if (list.size() == 2) {
            data.add(StringUtils.isBlank(list.get(0).getKey()) ? "-" : list.get(0).getKey());// Top1
            data.add(StringUtils.isBlank(list.get(1).getKey()) ? "-" : list.get(1).getKey());// Top2
            data.add("-");// Top3
        } else if (list.size() >= 3) {
            data.add(StringUtils.isBlank(list.get(0).getKey()) ? "-" : list.get(0).getKey());// Top1
            data.add(StringUtils.isBlank(list.get(1).getKey()) ? "-" : list.get(1).getKey());// Top2
            data.add(StringUtils.isBlank(list.get(2).getKey()) ? "-" : list.get(2).getKey());// Top3
        }
    }

    /**
     * CSV文件生成方法
     *
     * @param head 文件头
     * @param dataList 数据列表
     * @param outPutPath 文件输出路径
     * @param filename 文件名
     * @return
     */
    public static File createCSVFile(List<Object> head, List<List<Object>> dataList, String outPutPath, String filename) {

        File csvFile = null;
        BufferedWriter csvWtriter = null;
        try {
            //ContextUtil.getWebRootPath() +
            csvFile = new File(outPutPath + File.separator + filename + ".csv");
            File parent = csvFile.getParentFile();
            if (parent != null && !parent.exists()) {
                parent.mkdirs();
            }
            csvFile.createNewFile();

            // GB2312使正确读取分隔符","
            csvWtriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(csvFile), "GB2312"), 1024);
            // 写入文件头部
            writeRow(head, csvWtriter);

            // 写入文件内容
            for (List<Object> row : dataList) {
                writeRow(row, csvWtriter);
            }
            csvWtriter.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                csvWtriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return csvFile;
    }

    /**
     * 写一行数据方法
     *
     * @param row
     * @param csvWriter
     * @throws IOException
     */
    private static void writeRow(List<Object> row, BufferedWriter csvWriter) throws IOException {
        // 写入文件头部
        for (Object data : row) {
            StringBuffer sb = new StringBuffer();
            if (data == null) {
                continue;
            }
            String rowStr = sb.append("\"").append(String.valueOf(data).replaceAll("\"", "'").replaceAll(",", ",")).append("\",").toString();
            csvWriter.write(rowStr);
        }
        csvWriter.newLine();
    }
}

 

posted @ 2021-06-04 09:40  蔡徐坤1987  阅读(358)  评论(0编辑  收藏  举报