一、前言
已经有好久没有更博了,上次也不知道是什么时候了,还是那句话,写博客就是为了记录,点点滴滴,点到为止!
最近一直在忙着组内产品上线发布和线下测试的事情,还有一系列的产品代码封版的事情,总之忙碌的时间过的那是相当的快;
自己也一直想抽点时间写写心得体会,当然是关于产品的;但无奈我都劝住了自己,因为还没有到合适的时间,我要确保自己在状态最佳的时间点,大脑能够完全空下来的时候,再做长篇大论;
切入本篇正题的最后一句话:“做项目容易,做产品难啊,做核心产品更难啊!”
放一张产品的其中一个功能截图, 激励下自己,相信往后的路,一切都会越来越好!
二、场景
回到标题,相信大家在做批量查询时,一定用到过in这个条件,可能你会说,我不用in,我用联合查询啊,本篇讨论的是如果你用in,所以,就不要纠结了;
比如,select * from xxxTable where id in(1,2,3,4......)
比如,select * from xxxTable where name in ('a','b','c','d'......)
如果括号里面的值少的话,比如50个还好,100个也还好,但是如果是1000个,10000万个呢,事实上业务系统不会一下子查询这么多的,不有分页控制着呢吗。但是如果你在sql语句中用了in这个条件,一但你参数的个数没有控制好,外界传了一个特别size大的数值过来了,一种情况是,查询响应的结果会很耗时,因为查询的数据量大不说且in本身效率也不高啊,第二个就是in后面跟的参数太长,可能直接在sql编译这关就被卡死了,idea会无情的告诉你:
Prepared or callable statement has more than 2000 parameter markers
遇到这种情况怎么办? 推荐一种方法,分批查,一次查询很少量,利用多线程并发查最后合并结果返回,以牺牲cpu切换的时间来换取查询性能上的快感,想必有些人一听到多线程就头大,难道就没有其他办法了吗?有啊,你可以从数据表结构本身调整、或者sql语句优化上做文章,当然这两种比较灵活就不在本篇的讨论范围内了;
多线的好处我就不用多说了吧,当然用多线程也是有风险的好吧,秉承着不浪费new太多线程消耗内存资源,所以,最好用线程池管理着线程,用完的放到池子中等待下次用,不够的池子在扩,好吧,下面上demo,演示一遍就OK了!
三、多线程创建的方式那么多,选哪个呢?
多线的创建方式有四种,一种继承Thread类,一种实现Runable接口,再一种就是实现Callable接口,最后一种是利用线程池启动多线程;
注意,select查询是要有结果返回的,二是继承Thread类和实现Runable接口效果是一样的,都要复写run方法,推荐使用实现的方式,因为继承在Java中是单线条的,实现是可以多实现的;但是这两种方式满足不了带结果返回的,因为线程执行完了,我要怎么拿到执行后的result呢,别急,我们来看下Callable接口是怎么定义的:
那我就用这种吧,再结合Excutor批量提交任务(第四种方式,创建多线程),最后实现大批量in的高效查询!
四、项目结构
话不多说,直接看关键代码片段:
@GetMapping("/query")
public ResponseEntity query(){
// 模拟10000个id
List<Long> ids = new ArrayList<>();
for (long i = 0; i <10000 ; i++) {
ids.add(i);
}
// 将ids按多少个进行拆分
List<List<Long>> parts = Lists.partition(ids, 50);
// 有多少份,就有多少次请求,有多少次请求就有多少个task
List<Callable<List<GxTask>>> tasks = new ArrayList<>();
for (List<Long> part : parts) {
GxTaskQuery taskQuery = new GxTaskQuery(taskService);
taskQuery.setIds(part);
tasks.add(taskQuery);
}
// 每一个任务(线程)执行的结果都是一个future对象,这个对象的数据就是List<GxTask>
List<Future<List<GxTask>>> futures = new ArrayList<>();
for (Callable<List<GxTask>> task : tasks) {
// 提交任务 == 注意这时候并没有触发线程执行,就绪状态
futures.add(executor.submit(task));
}
// 最终要查询的任务集
List<GxTask> allTasks = new ArrayList<>();
// 遍历执行结果,准备正式执行(get)
for (Future<List<GxTask>> future : futures) {
try {
// 查询任务超过3s的直接弃掉(cancel取消任务)
List<GxTask> taskList =future.get(3000,TimeUnit.MILLISECONDS );
if(taskList !=null && taskList.size() != 0){
allTasks.addAll(taskList);
}
}catch (InterruptedException | ExecutionException | TimeoutException e){
future.cancel(true);
}
}
return ResponseEntity.ok(allTasks);
}
需要注意的有三点:
(1)ids分段,比如10000个id,每50或100个分一批,这里利用的是google的guava包
(2)注意excutor提交的任务返回结果是Feature类型
(3)只有调用feature.get(x)方法才算真正执行任务,且这个执行的结果是可以设置等待时间的,如果超过设置的时间任务还没有返回结果,那么这个task就会被取消(中断cancel),这是必然啊,不能一个查询拖了全局查询的后腿吧,当然这种情况是很少会发生的。
五、测试
代码里没有真正去查数据库,但是模拟了一把in的查询:
直接项目启动,postman走一波测试:
测试结果:
六、GitHub项目地址
地址:https://github.com/kobeyk/MultiTask