前题
刚入职的公司,要求接口压测要通过,发现在业务中需要接口需要使用多线程的使用,由于入参动态输入会员id数组,我们需要创建数组size的线程数,来请求db(其实应该用一个sql来写的,但是公司分片数据库不支持)。
需求:多线程中,每个线程执行的时间都不能超过一个固定值,如果返回不了,可丢弃
方案一
使用CompletableFuture
的allOf
join
来实现,但又缺点,我们需要在规定时间200ms内获取所有Future的结果,但是好像这样的代码并不适合
// multiple task
List<Integer> taskList = new ArrayList<Integer>() {{
add(1);
add(2);
add(3);
add(4);
}};
// collect completeFutureList
List<CompletableFuture<String>> completableFutureList = new ArrayList<>();
for (Integer task : taskList) {
CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return task.toString() + "-----";
});
completableFutureList.add(completableFuture);
}
// all task complete
CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[completableFutureList.size()])).join();
// get result
for (CompletableFuture completableFuture : completableFutureList) {
try {
System.out.println(completableFuture.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
方案二
使用线程池+callable+CountDownLatch来实现 (当前方法有并发问题)
final static ExecutorService executor = Executors.newCachedThreadPool();
public static void main(String[] args) {
// multiple task
List<Integer> taskList = new ArrayList<Integer>() {{
add(1);
add(2);
add(3);
add(4);
}};
// create countDownLatch
CountDownLatch countDownLatch = new CountDownLatch(taskList.size());
// future list
List<Future<String>> futureList = new ArrayList<>();
for (Integer taskId:taskList) {
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(taskId);
countDownLatch.countDown();
return String.valueOf(taskId);
}
});
futureList.add(future);
}
// wait 2 seconds
try {
countDownLatch.await(2,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// get future result right now
List<String> result = new ArrayList<>();
for (Future<String> future : futureList){
String s = null;
try {
s = future.get(0, TimeUnit.SECONDS);
result.add(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
System.out.println(result);
}
下面贴 测试出来的bug
// 模拟1000并发请求,会发现下面的get时 会有几率报错。在压测的时候,10w请求,大概1000+个错误
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
CountDownLatch latch = new CountDownLatch(1);
Future<String> submit = executor.submit(() -> {
latch.countDown();
/**
* 猜测,这里出现了cpu切换
* */
return "abc";
});
try {
latch.await(10, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println( submit.get(0, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
});
}
方案三
使用 invokeAll 加时间的形式
for (int i = 0; i < 100000; i++) {
// 模拟请求
executor.execute(() -> {
//
List<Callable<Integer>> taskList = new ArrayList<>();
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
try {
// 模拟sql
TimeUnit.MILLISECONDS.sleep(20);
return 1;
//
} catch (InterruptedException e) {
e.printStackTrace();
}
return -1025;
}
});
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
try {
// 模拟sql
TimeUnit.MILLISECONDS.sleep(10);
return 1;
} catch (InterruptedException e) {
System.out.println("error");
}
return -1024;
}
});
//
List<Future<Integer>> futures = null;
try {
// 问题所在 1 ?
futures = executor.invokeAll(taskList,200,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<Integer> result = new ArrayList<>();
for (Future<Integer> future : futures) {
Integer integer = 0;
try {
// 问题所在 2 ?
future.cancel(true);
if (!future.isCancelled()) {
integer = future.get();
}
} catch (Exception e) {
e.printStackTrace();
}
result.add(integer);
}
});
}
这个使用触发一个druid的异常
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
// 省略
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("interrupt", e);
}
// 省略,数据库连接池配置的大小参数等检查
}
1.在于future.cancel(true)会触发interrupt
2.invokeAll(taskList,200,TimeUnit.MILLISECONDS)里面的finally也会调用future.cancel(true);会触发interrupt
第四种方案 (最终版本)
就使用invokeAll,不带时间的形式。
主要是我们看,线上的接口响应时间大多符合要求,于是去掉
for (int i = 0; i < 100000; i++) {
// 模拟请求
executor.execute(() -> {
//
List<Callable<Integer>> taskList = new ArrayList<>();
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
try {
// 模拟sql
TimeUnit.MILLISECONDS.sleep(20);
return 1;
//
} catch (InterruptedException e) {
e.printStackTrace();
}
return -1025;
}
});
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
try {
// 模拟sql
TimeUnit.MILLISECONDS.sleep(10);
return 1;
} catch (InterruptedException e) {
System.out.println("error");
}
return -1024;
}
});
//
List<Future<Integer>> futures = null;
try {
futures = executor.invokeAll(taskList);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<Integer> result = new ArrayList<>();
for (Future<Integer> future : futures) {
Integer integer = 0;
try {
if (!future.isCancelled()) {
integer = future.get();
}
} catch (Exception e) {
e.printStackTrace();
}
result.add(integer);
}
});
}