文章目录
- 一.业务场景
- 二.拆分流程
- 三. 简单上个List拆分的demo
- 四.测试结果
- 五.小结
一.业务场景
节假日后第一天,上完班有点累,回到家稍微写点简单的东西。
- 我们项目里面有这样一业务场景,要计算全公司所有人某几个月内每天的考勤机打卡加班工时结果,如果直接查询出所有人的打卡记录(动不动几十万上百万条)记录,再以人为维度分组计算,这样程序要跑特别特别久。
- 每次业务如果需要我们重新跑一次结果,要等这么久的话有点浪费彼此的时间,所以有没有办法来提高效率让程序跑快一点呢?
没错,那就是集合拆分使用多线程把串行变成并行处理 + 统一收集结果再用Mybatis的批处理,这里先只介绍集合拆分串行变成并行处理,不详细介绍其他的了。
当然在线程里面执行批量插入效率也可以,但是得看分的线程数量,效率则不同,具体看文章【Mybatis批处理 vs 集合拆分多线程批量插入 性能对比】
二.拆分流程
我们业务里面我是这样来拆分的:
- 先查出所有的人,大概5W多个人员id
- 把5W多个人员id集合拆分子List,每个子List大小设置为200
- 然后在线程里面去根据人员id查询打卡记录表,得到每个人的每天打卡记录,这样sql查询也快些了,最终再去计算每个人的结果。
当然我还做了其他很多优化:
1.比如利用TransmittableThreadLocal主线程传个空的并发安全List(Collections.synchronizedList适用于写多读少)进子线程,每个子线程满足特定条件都加锁再收集结果在这集合中。
2.最终等所有子线程都收集完了结果,再通过MyBatis的批处理操作统一插入该结果集合。
补充:在MyBatis中,批处理操作是一种高效执行多条语句的方式,特别是当你需要在一个事务中插入、更新或删除多条记录时。批处理可以显著减少与数据库的交互次数,从而提高性能。
执行批处理的基本步骤:
开启批处理模式:在获取SqlSession时,需要指定执行器(Executor)类型为ExecutorType.BATCH。
执行SQL语句:执行需要批处理的SQL语句,此时语句并不会立即执行,而是被添加到批处理队列中。
提交事务:调用SqlSession.commit()方法,此时MyBatis会将批处理队列中的语句一次性发送给数据库执行。
处理批处理结果:提交事务后,可以通过批处理结果进行后续处理。
3.用统一收集结果再通过Mybatis批处理进行整个改造后,我们项目里面这个任务由原来的6-7分钟跑完只需要1-2分钟。 亲测有效!绝没有吹牛逼!
三. 简单上个List拆分的demo
这里只举测试例子(项目代码不便贴)。List拆分成子List,然后根据子List的人员id去查询表,再做逻辑计算处理,跟我业务场景是一样的。
public void parallelSubList() {List<Integer> list = new ArrayList<>();for (int i = 1; i <= 1000; i++) {list.add(i);}int totalCount = list.size();int pageSize = 50;int threadCount = totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1;log.info("线程拆分数量:{}",threadCount);CountDownLatch countDownLatch = new CountDownLatch(threadCount);for (int index = 0; index < threadCount; index++){List<Integer> subList = list.subList(index * pageSize, index == threadCount - 1 ? totalCount : (index + 1) * pageSize);tulingThreadPoolExecutor.submit(()->{try {log.info("当前执行线程名称:{},subList集合范围:{}",Thread.currentThread().getName(), JSONUtil.toJsonStr(subList.get(0) + "-" + subList.get(subList.size()-1)));}catch (Exception e){log.info("当前任务执行失败:{}",e.getMessage());}finally {// 请注意,这里无论成功或者失败,必须countDown,不然会造成计数器归不了零,从而造成程序一直会阻塞在await方法countDownLatch.countDown();}});}try {countDownLatch.await();}catch (InterruptedException e){log.info("任务出错了,中断异常:"+e.getMessage());e.printStackTrace();}}
四.测试结果
五.小结
这里先简单写下,主要是觉得List拆分这块算是个模板,也是个思路,可以拿来即用的,在咱们之后业务场景里遇到大的集合拆成小集合这是一个复制粘贴的思路。