当前位置:

首页 > 编程开发 > Java如何实现多线程大批量同步数据

Java如何实现多线程大批量同步数据

背景最近遇到个功能,两个月有300w+的数据,之后还在累加,因一开始该数据就全部存储在mysql表,现需要展示在页面,还需要关联另一张表的数据,而且产品要求页面的查询条件多达20个条件,最终,这个功能卡的要死,基本查不出来数据。最后是打算把这两张表的数据同时存储到MongoDB中去,以提高查询效率。一开始同步的时候,采用单线程,循环以分页的模式去同步这两张表数据,结果是…一晚上,只同步了30w数据,特慢!!!最后,改造了一番,2小时,就成功同步了300w+数据。以下是主要逻辑。线程的个数请根

本文目录

    背景

    最近遇到个功能,两个月有300w+的数据,之后还在累加,因一开始该数据就全部存储在mysql表,现需要展示在页面,还需要关联另一张表的数据,而且产品要求页面的查询条件多达20个条件,最终,这个功能卡的要死,基本查不出来数据。

    最后是打算把这两张表的数据同时存储到MongoDB中去,以提高查询效率。

    一开始同步的时候,采用单线程,循环以分页的模式去同步这两张表数据,结果是…一晚上,只同步了30w数据,特慢!!!

    最后,改造了一番,2小时,就成功同步了300w+数据。

    以下是主要逻辑。

    线程的个数请根据你自己的服务器性能酌情设置。

    思路

    先通过count查出结果集的总条数,设置每个线程分页查询的条数,通过总条数和单次条数得到线程数量,通过改变limit的下标实现分批查询。

    代码实现

    package com.github.admin.controller.loans;
    
    import com.baomidou.mybatisplus.mapper.EntityWrapper;
    import com.github.admin.model.entity.CaseCheckCallRecord;
    import com.github.admin.model.entity.duyan.DuyanCallRecordDetail;
    import com.github.admin.model.entity.loans.CaseCallRemarkRecord;
    import com.github.admin.service.duyan.DuyanCallRecordDetailService;
    import com.github.admin.service.loans.CaseCallRemarkRecordService;
    import com.github.common.constant.MongodbConstant;
    import com.github.common.util.DingDingMsgSendUtils;
    import com.github.common.util.ListUtils;
    import com.github.common.util.Response;
    import com.github.common.util.concurrent.Executors;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.BeanUtils;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.mongodb.core.MongoTemplate;
    import org.springframework.data.mongodb.core.query.Criteria;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Future;
    
    /**
     * 多线程同步历史数据
     * @author songfayuan
     * @date 2019-09-26 15:38
     */
    @Slf4j
    @RestController
    @RequestMapping("/demo")
    public class SynchronizeHistoricalDataController implements DisposableBean {
    
        private ExecutorService executor = Executors.newFixedThreadPool(10, "SynchronizeHistoricalDataController");  //newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    
        @Value("${spring.profiles.active}")
        private String profile;
        @Autowired
        private DuyanCallRecordDetailService duyanCallRecordDetailService;
        @Autowired
        private MongoTemplate mongoTemplate;
        @Autowired
        private CaseCallRemarkRecordService caseCallRemarkRecordService;
    
        /**
         * 多线程同步通话记录历史数据
         * @param params
         * @return
         * @throws Exception
         */
        @GetMapping("/syncHistoryData")
        public Response syncHistoryData(Map params) throws Exception {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        logicHandler(params);
                    } catch (Exception e) {
                        log.warn("多线程同步稽查通话记录历史数据才处理异常,errMsg = {}", e);
                        DingDingMsgSendUtils.sendDingDingGroupMsg("【系统消息】" + profile + "环境,多线程同步稽查通话记录历史数据才处理异常,errMsg = "+e);
                    }
                }
            });
            return Response.success("请求成功");
        }
    
        /**
         * 处理数据逻辑
         * @param params
         * @throws Exception
         */
        private void logicHandler(Map params) throws Exception {
            /******返回结果:多线程处理完的最终数据******/
            List result = new ArrayList<>();
    
            /******查询数据库总的数据条数******/
            int count = this.duyanCallRecordDetailService.selectCount(new EntityWrapper()
                    .eq("is_delete", 0)
                    .eq("platform_type", 1));
            DingDingMsgSendUtils.sendDingDingGroupMsg("【系统消息】" + profile + "环境,本次需要同步" + count + "条历史稽查通话记录数据。");
    
    //        int count = 2620266;
            /******限制每次查询的条数******/
            int num = 1000;
    
            /******计算需要查询的次数******/
            int times = count / num;
            if (count % num != 0) {
                times = times + 1;
            }
    
            /******每个线程开始查询的行数******/
            int offset = 0;
    
            /******添加任务******/
            List>> tasks = new ArrayList<>();
            for (int i = 0; i < times; i++) {
                Callable> qfe = new ThredQuery(duyanCallRecordDetailService, params, offset, num);
                tasks.add(qfe);
                offset = offset + num;
            }
    
            /******为避免太多任务的最终数据全部存在list导致内存溢出,故将任务再次拆分单独处理******/
            List>>> smallList = ListUtils.partition(tasks, 10);
            for (List>> callableList : smallList) {
                if (CollectionUtils.isNotEmpty(callableList)) {
    //                executor.execute(new Runnable() {
    //                    @Override
    //                    public void run() {
    //                        log.info("任务拆分执行开始:线程{}拆分处理开始...", Thread.currentThread().getName());
    //
    //                        log.info("任务拆分执行结束:线程{}拆分处理开始...", Thread.currentThread().getName());
    //                    }
    //                });
    
                    try {
                        List>> futures = executor.invokeAll(callableList);
                        /******处理线程返回结果******/
                        if (futures != null && futures.size() > 0) {
                            for (Future> future : futures) {
                                List duyanCallRecordDetailList = future.get();
                                if (CollectionUtils.isNotEmpty(duyanCallRecordDetailList)){
                                    executor.execute(new Runnable() {
                                        @Override
                                        public void run() {
                                            /******异步存储******/
                                            log.info("异步存储MongoDB开始:线程{}拆分处理开始...", Thread.currentThread().getName());
                                            saveMongoDB(duyanCallRecordDetailList);
                                            log.info("异步存储MongoDB结束:线程{}拆分处理开始...", Thread.currentThread().getName());
                                        }
                                    });
                                }
                                //result.addAll(future.get());
                            }
                        }
                    } catch (Exception e) {
                        log.warn("任务拆分执行异常,errMsg = {}", e);
                        DingDingMsgSendUtils.sendDingDingGroupMsg("【系统消息】" + profile + "环境,任务拆分执行异常,errMsg = "+e);
                    }
                }
            }
        }
    
        /**
         * 数据存储MongoDB
         * @param duyanCallRecordDetailList
         */
        private void saveMongoDB(List duyanCallRecordDetailList) {
            for (DuyanCallRecordDetail duyanCallRecordDetail : duyanCallRecordDetailList) {
                /******重复数据不同步MongoDB******/
                org.springframework.data.mongodb.core.query.Query query = new org.springframework.data.mongodb.core.query.Query();
                query.addCriteria(Criteria.where("callUuid").is(duyanCallRecordDetail.getCallUuid()));
                List caseCheckCallRecordList = mongoTemplate.find(query, CaseCheckCallRecord.class, MongodbConstant.CASE_CHECK_CALL_RECORD);
                if (CollectionUtils.isNotEmpty(caseCheckCallRecordList)) {
                    log.warn("call_uuid = {}在MongoDB已经存在数据,后面数据将被舍弃...", duyanCallRecordDetail.getCallUuid());
                    continue;
                }
    
                /******关联填写的记录******/
                CaseCallRemarkRecord caseCallRemarkRecord = this.caseCallRemarkRecordService.selectOne(new EntityWrapper()
                        .eq("is_delete", 0)
                        .eq("call_uuid", duyanCallRecordDetail.getCallUuid()));
    
                CaseCheckCallRecord caseCheckCallRecord = new CaseCheckCallRecord();
                BeanUtils.copyProperties(duyanCallRecordDetail, caseCheckCallRecord);
                //补充
                caseCheckCallRecord.setCollectorUserId(duyanCallRecordDetail.getUserId());
                
                if (caseCallRemarkRecord != null) {
                    //补充
                    caseCheckCallRecord.setCalleeName(caseCallRemarkRecord.getContactName());            
                }
                log.info("正在存储数据到MongoDB:{}", caseCheckCallRecord.toString());
                this.mongoTemplate.save(caseCheckCallRecord, MongodbConstant.CASE_CHECK_CALL_RECORD);
            }
        }
    
        @Override
        public void destroy() throws Exception {
            executor.shutdown();
        }
    }
    
    
    class ThredQuery implements Callable> {
        /******需要通过构造方法把对应的业务service传进来 实际用的时候把类型变为对应的类型******/
        private DuyanCallRecordDetailService myService;
        /******查询条件 根据条件来定义该类的属性******/
        private Map params;
    
        /******分页index******/
        private int offset;
        /******数量******/
        private int num;
    
        public ThredQuery(DuyanCallRecordDetailService myService, Map params, int offset, int num) {
            this.myService = myService;
            this.params = params;
            this.offset = offset;
            this.num = num;
        }
    
        @Override
        public List call() throws Exception {
            /******通过service查询得到对应结果******/
            List duyanCallRecordDetailList = myService.selectList(new EntityWrapper()
                    .eq("is_delete", 0)
                    .eq("platform_type", 1)
                    .last("limit "+offset+", "+num));
            return duyanCallRecordDetailList;
        }
    }

    ListUtils工具

    package com.github.common.util;
    
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 描述:List工具类
     * @author songfayuan
     * 2018年7月22日下午2:23:22
     */
    @Slf4j
    public class ListUtils {
        
        /**
         * 描述:list集合深拷贝
         * @param src
         * @return
         * @throws IOException
         * @throws ClassNotFoundException
         * @author songfayuan
         * 2018年7月22日下午2:35:23
         */
        public static  List deepCopy(List src) {
            try {
                ByteArrayOutputStream byteout = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(byteout);
                out.writeObject(src);
                ByteArrayInputStream bytein = new ByteArrayInputStream(byteout.toByteArray());
                ObjectInputStream in = new ObjectInputStream(bytein);
                @SuppressWarnings("unchecked")
                List dest = (List) in.readObject();
                return dest;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 描述:对象深拷贝
         * @param src
         * @return
         * @throws IOException
         * @throws ClassNotFoundException
         * @author songfayuan
         * 2018年12月14日
         */
        public static  T objDeepCopy(T src) {
            try {
                ByteArrayOutputStream byteout = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(byteout);
                out.writeObject(src);
                ByteArrayInputStream bytein = new ByteArrayInputStream(byteout.toByteArray());
                ObjectInputStream in = new ObjectInputStream(bytein);
                @SuppressWarnings("unchecked")
                T dest = (T) in.readObject();
                return dest;
            } catch (ClassNotFoundException e) {
                log.error("errMsg = {}", e);
                return null;
            } catch (IOException e) {
                log.error("errMsg = {}", e);
                return null;
            }
        }
    
        /**
         * 将一个list均分成n个list,主要通过偏移量来实现的
         * @author songfayuan
         * 2018年12月14日
         */
        public static  List> averageAssign(List source, int n) {
            List> result = new ArrayList>();
            int remaider = source.size() % n;  //(先计算出余数)
            int number = source.size() / n;  //然后是商
            int offset = 0;//偏移量
            for (int i = 0; i < n; i++) {
                List value = null;
                if (remaider > 0) {
                    value = source.subList(i * number + offset, (i + 1) * number + offset + 1);
                    remaider--;
                    offset++;
                } else {
                    value = source.subList(i * number + offset, (i + 1) * number + offset);
                }
                result.add(value);
            }
            return result;
        }
    
        /**
         * List按指定长度分割
         * @param list the list to return consecutive sublists of (需要分隔的list)
         * @param size the desired size of each sublist (the last may be smaller) (分隔的长度)
         * @author songfayuan
         * @date 2019-07-07 21:37
         */
        public static  List> partition(List list, int size){
            return  Lists.partition(list, size); // 使用guava
        }
    
        /**
         * 测试
         * @param args
         */
        public static void main(String[] args) {
            List bigList = new ArrayList<>();
            for (int i = 0; i < 101; i++){
                bigList.add(i);
            }
            log.info("bigList长度为:{}", bigList.size());
            log.info("bigList为:{}", bigList);
            List> smallists = partition(bigList, 20);
            log.info("smallists长度为:{}", smallists.size());
            for (List smallist : smallists) {
                log.info("拆分结果:{},长度为:{}", smallist, smallist.size());
            }
        }
    
    }
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。