业务背景
公司在做客服迁移迁移系统,下来的需求就是将公司的用户信息全量同步到那边的数据库,总数据量大概300w+,将无效数据过滤掉以及不需要进行同步的用户(手机号码不存在),剩下大概200w。
开始采用的方案就是单线程分页查询,每批100条的数据量,定时任务写好放到测试环境里面跑了一下,预估大概每天能跑50w的数据量(两天反正很难完成任务)。
为什么这么慢呢,有个原因是因为那边没有提供批量插入的接口,只能单条插入,并且每秒有阈值,大概是十条左右。
另外就是数据量比较大,会导致分页查询越来越慢(其实我觉得也还好),因为每次查询都需要扫描整个结果集并跳过前面的记录以获取请求的页数。
优化思路
改动多线程将大任务分成子任务,并且子任务之间没有关联。
另外就是核心线程数的设置,我在我自己机器上测,核心数是4,我设置线程数10速度是比较快的,
和单线程相比较,大概能提升2.5倍,查询大概10分钟(仅仅是查询)。
代码实现
@Slf4j
@RestController
@RequestMapping("/demo")
public class SynchronizeHistoricalDataController implements DisposableBean {private ExecutorService executor = Executors.newFixedThreadPool(10); //newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。@Autowiredprivate ITest1Service test1Service;/*** 多线程同步通话记录历史数据** @param params* @return* @throws Exception*/@GetMapping("/syncHistoryData")public Response syncHistoryData(Map<String, Object> params) throws Exception {executor.execute(new Runnable() {@Overridepublic void run() {try {logicHandler(params);} catch (Exception e) {log.warn("多线程同步用户信息历史数据才处理异常,errMsg={}", e.getMessage());
// DingDingMsgSendUtils.sendDingDingGroupMsg("【系统消息】" + profile + "环境,多线程同步稽查通话记录历史数据才处理异常,errMsg = " + e);}}});return Response.success("请求成功");}/*** 处理数据逻辑** @param params* @throws Exception*/private void logicHandler(Map<String, Object> params) throws Exception {/******返回结果:多线程处理完的最终数据******/List<Test1> result = new ArrayList<>();/******查询数据库总的数据条数******/int count = this.test1Service.count(new QueryWrapper<Test1>());/******限制每次查询的条数******/int num = 1000;/******计算需要查询的次数******/int times = count / num;if (count % num != 0) {times = times + 1;}/******每个线程开始查询的行数******/int offset = 0;/******添加任务******/List<Callable<List<Test1>>> tasks = new ArrayList<>();for (int i = 0; i < times; i++) {Callable<List<Test1>> qfe = new ThredQuery(test1Service, params, offset, num);tasks.add(qfe);offset = offset + num;}/******为避免太多任务的最终数据全部存在list导致内存溢出,故将任务再次拆分单独处理******///将x页的数据分成10份,每一份是x/10页的数据量//也就是将x/10份任务交给10个线程来处理,每个List<List<Callable<List<Test1>>>> smallList = ListUtils.partition(tasks, 10);//遍历任务for (List<Callable<List<Test1>>> callableList : smallList) {if (CollectionUtils.isNotEmpty(callableList)) {try {List<Future<List<Test1>>> futures = executor.invokeAll(callableList);/******处理线程返回结果******/if (!futures.isEmpty()) {for (Future<List<Test1>> future : futures) {List<Test1> test1List = future.get();//将数据多线程发送到MongoDBif (CollectionUtils.isNotEmpty(test1List)) {executor.execute(new Runnable() {@Overridepublic 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);}}}System.out.println(System.currentTimeMillis());}@Overridepublic void destroy() throws Exception {executor.shutdown();}}class ThredQuery implements Callable<List<Test1>> {/******需要通过构造方法把对应的业务service传进来 实际用的时候把类型变为对应的类型******/private ITest1Service myService;/******查询条件 根据条件来定义该类的属性******/private Map<String, Object> params;/******分页index******/private int offset;/******数量******/private int num;public ThredQuery(ITest1Service myService, Map<String, Object> params, int offset, int num) {this.myService = myService;this.params = params;this.offset = offset;this.num = num;}//分页查询@Overridepublic List<Test1> call() throws Exception {/******通过service查询得到对应结果******/List<Test1> test1s = myService.list(new QueryWrapper<Test1>().last("limit " + offset + ", " + num));return test1s;}}
内存风险
为避免太多任务的最终数据全部存在list导致内存溢出,故将任务再次拆分单独处理。