最近新需求相对少一些,于是我又成了职业擦屁股人,于是吧,就成了千篇一律的排查sql,处理循环中的查询,大事务,这些常见问题,偶尔遇到一两个一眼看不出问题所在的,反而觉得有意思多了,比如下面这两个问题:
1:先上一个简单的,长时间卡顿问题,伪代码如下
//数据库查询数据
List a = service.findDataByCondition1();
//换个条件继续查询数据
List b = service.findDataByCondition2();
//剔除a中在b里存在的数据
a.removeAll(b);
简化成这个样子应该成猜到哪里出问题了,
当时排查,首先就是习惯性去排查了sql,发现sql挺简单,命中索引,虽然数据有点多,但是也不至于卡顿这么久,
(看上去很简单的sql,也被坑过:当时是mySql 5.x 版本,看上去是单表查询,实际上这个“单表”是个很复杂的视图,视图本身能用到索引, 但是加了where条件后,相当于在视图查询的结果集外再包一层然后加查询条件,直接卡爆)
然后打了日志,大概定位到这一段代码,定眼一看,幡然醒悟:
List 的 removeAll 方法,这个东西性能应该不太好吧,相当于两个循环,挨个调用equals(), 对象越复杂,equals 比较时间越长,数据越多嵌套循环性能越差。 进一步优化,这个查询可以根据一些条件尽量分批次去查询,节约内存
具体修改方法也特别简单,直接展示测试结果,
创建一个简单测试类
public static class Node {
int a;
String b;
int c;
String d;
public static Node createNode(int numer) {
return new Node(numer, "测试" + numer, numer + 100, "其他测试" + numer);
}
}
分别在 数组和HashSet中添加100000个 Node 对象,然后进行移除
2: 线上卡死问题
这次是在一个界面输入某个条件查询列表后,一直不出来数据,界面一直转“菊花”,还是老规矩,看了sql,一切正常。但是有一段看上去不太和谐的代码感觉有点问题,大概如下
CompletableFuture<String> completableFuture = new CompletableFuture<>();
//根据一些条件查询数据库
List list = service.find(query);
if (CollectionUtils.isNotEmpty(list)) {
completableFuture = CompletableFuture.supplyAsync(() -> {
//做一些耗时操作
return doSomeThing();
});
}
//这里只是举例只写了一个,allOf中还有其他的 completableFuture
CompletableFuture<Void> allOf = CompletableFuture.allOf(completableFuture);
allOf.get()
首先看上去写的还挺仔细,第一行代码先赋初始值,防止空指针,
把几个耗时操作异步进行,最后用 allOf.get(); 等待所有任务完成 (我习惯用whenComplete() 这个方法)
问题就出在 if 中的判断条件,如果数组为空,也就是界面的某个条件查不到数据时。allOf.get() 这行代码会一直阻塞!
查看源码,代码调用为
CompletableFuture#waitingGet -> ForkJoinPool.managedBlock(q);
然后循环中判断 blocker.block()
blocker.block() 中会进行阻塞,等待任务完成唤醒线程
源码里唤醒线程则是:任务完成会调用这个方法:
final void postComplete()
然后调用 tryFire 方法,这里就能唤醒阻塞线程
所以问题就出在,直接new 一个CompletableFuture,当list为空就回导致没有提交一个任务,就没有了任务完成唤醒线程的操作。导致页面表现为卡死
3:类似问题
这几天吧,外部系统调用我们的接口,老是超时,那么,是不是又是数据库出问题了呢?老规矩,挨个sql排查,老演员数据库欲哭无泪啊,老是我数据库大哥背锅,
然后呢,万千代码中相中了一个老熟人, CompletionService,这个工具类我自己还没用过,,,,但是长得和 CompletableFuture 有几分神似。就从它入手吧,然后大概还原的伪代码如下:
public static void main(String[] args) throws Exception {
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6);
CompletionService<Object> completionService = ThreadUtil.newCompletionService();
for (Integer number : list) {
if (number > 2) {
completionService.submit(() -> {
Thread.sleep(1000);
log.info("完成了!!");
return number + 100;
});
} else {
log.info("不符合条件,打个日志记录下");
}
}
list.forEach(number ->{
try {
completionService.take().get(10000, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("结束了");
}
这段代码看起来还挺靠谱,completionService.take().get(10000, TimeUnit.SECONDS) 方法中还加了等待时间,防止无线等待,但是跑起来,永远等不到System.out.println("结束了"); 这行代码执行。因为问题不是出在 .get(10000, TimeUnit.SECONDS) 方法,而是take()
原因其实和问题2类似, 6个元素的list,只放入了4个任务,然后又遍历整个list 6次,来获取任务的结果 ,那还有两次就永远等不到响应。这个工具类理解成 生产消费者问题就行。
CompletionService内部有一个阻塞队列,消费者是completionService.take(),就是去阻塞队列中获取 Future, 而生产者则是 completionService.submit,向队列放入Future,当生产者发放入的数量少于take()获取的数据量时,take()就会等待,直到有新的任务进入。
比如把上面的代码改一下,加一个异步线程丢入两个任务,completionService.take().get() 就能正常退出
public static void main(String[] args) throws Exception {
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6);
CompletionService<Object> completionService = ThreadUtil.newCompletionService();
for (Integer number : list) {
if (number > 2) {
completionService.submit(() -> {
Thread.sleep(1000);
log.info("完成了!!");
return number + 100;
});
} else {
log.info("不符合条件,打个日志记录下");
}
}
//等待一段时间后再放入两个任务
new Thread(() -> {
try {
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 2; i++) {
completionService.submit(() -> {
Thread.sleep(1000);
log.info("其他线程任务完成了!!");
return 0;
});
}
}).start();
list.forEach(number ->{
try {
completionService.take().get(10000, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("结束了");
}