SpringBoot のスループットを向上させる 7 つの方法
一、 非同期実行
1、実装方法は二種
非同期アノテーション @Async を使用し、起動クラスに @EnableAsync アノテーションを追加します。
2、JDK 8 には非常に便利な Future クラスである CompletableFuture が備わっています
@AllArgsConstructor
public class AskThread implements Runnable{
private CompletableFuture<Integer> re = null;
public void run() {
int myRe = 0;
try {
myRe = re.get() * re.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(myRe);
}
public static void main(String[] args) throws InterruptedException {
final CompletableFuture<Integer> future = new CompletableFuture<>();
new Thread(new AskThread(future)).start();
// 長時間の計算プロセスをシミュレートする
Thread.sleep(1000);
// 完了結果を通知する
future.complete(60);
}
}
この例では、スレッドを開始しますが、その時点で AskThread オブジェクトは必要なデータを取得しておらず、myRe = re.get() * re.get() の実行でブロックされます。
1秒間スリープして長時間の計算プロセスをシミュレートし、計算結果を future に通知すると、AskThread スレッドは続行します。
public class Calc {
public static Integer calc(Integer para) {
try {
// 1秒間
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return para * para;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> calc(50))
.thenApply((i) -> Integer.toString(i))
.thenApply((str) -> "\"" + str + "\"")
.thenAccept(System.out::println);
future.get();
}
}
CompletableFuture.supplyAsync メソッドは、CompletableFuture インスタンスを構築します。supplyAsync() メソッド内では、新しいスレッドで引数が実行されます。
ここでは calc() メソッドが実行されますが、このメソッドは比較的遅い可能性があります。しかし、これは CompletableFuture インスタンスの構築速度には影響しません。supplyAsync() は即座に戻ります。
戻り値の CompletableFuture インスタンスは、今回の呼び出しの契約として機能し、将来的にどの場面でも最終的な計算結果を取得するために使用できます。
supplyAsync は返り値を提供する場合に使用されますが、CompletableFuture には返り値を必要としない非同期呼び出しメソッド runAsync(Runnable runnable) もあります。一般的に、コントローラーの最適化時にこのメソッドを多く使用します。
これらの2つのメソッドは、スレッドプールを指定しない場合、ForkJoinPool.common スレッドプールで実行されます。このスレッドプール内のすべてのスレッドはデーモン(守護)スレッドであるため、メインスレッドが終了すると、これらのスレッドは実行が完了してもシステムから退出します。
CompletableFuture.runAsync(() ->
this.afterBetProcessor(betRequest,betDetailResult,appUser,id)
);
非同期呼び出しは Callable を使用して実装します。
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
private HelloService hello;
@GetMapping("/helloworld")
public String helloWorldController() {
return hello.sayHello();
}
/**
* 非同期呼び出し restful
* コントローラーの戻り値が Callable の場合、Spring MVC はスレッドを起動して Callable を TaskExecutor に渡します。
* その後、DispatcherServlet およびすべての Spring インターセプターはメインスレッドから退出し、response をオープン状態に保ちます。
* Callable の実行が終了すると、Spring MVC は新たに request を割り当て、DispatcherServlet が再び呼び出し、Callable の非同期実行の戻り値を処理し、ビューを返します。
* @return
*/
@GetMapping("/hello")
public Callable<String> helloController() {
logger.info(Thread.currentThread().getName() + " helloController");
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " call");
String say = hello.sayHello();
logger.info(Thread.currentThread().getName() + " helloService");
return say;
}
};
logger.info(Thread.currentThread().getName() + " helloController");
return callable;
}
}
非同期呼び出し WebAsyncTask
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
private HelloService hello;
/**
* タイムアウト付きの非同期リクエストは、WebAsyncTask を使用してクライアントのタイムアウトをカスタマイズします。
* @return
*/
@GetMapping("/world")
public WebAsyncTask<String> worldController() {
logger.info(Thread.currentThread().getName() + " helloController");
// 3秒間返答がない場合は、タイムアウトと見なします。
WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " call");
String say = hello.sayHello();
logger.info(Thread.currentThread().getName() + " helloService");
return say;
}
});
logger.info(Thread.currentThread().getName() + " helloController");
webAsyncTask.onCompletion(new Runnable() {
@Override
public void run() {
logger.info(Thread.currentThread().getName() + " OK);
}
});
webAsyncTask.onTimeout(new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " onTimeout");
// タイムアウト時に直接例外をスローし、外側で統一してタイムアウト例外を処理させます。
throw new TimeoutException("调用超时");
}
});
return webAsyncTask;
}
/**
* 非同期呼び出し、例外処理、詳細な処理フローは MyExceptionHandler クラスを参照してください。
* @return
*/
@GetMapping("/exception")
public WebAsyncTask<String> exceptionController() {
logger.info(Thread.currentThread().getName() + " helloController");
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " call");
throw new TimeoutException("调用超时!");
}
};
logger.info(Thread.currentThread().getName() + "helloController");
return new WebAsyncTask<>(20000, callable);
}
}
二、埋め込み Tomcat の最大接続数を増加させる。
@Configuration
public class TomcatConfig {
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory();
tomcatFactory.addConnectorCustomizers(new MyTomcatConnectorCustomizer());
tomcatFactory.setPort(8005);
tomcatFactory.setContextPath("/api-g");
return tomcatFactory;
}
class MyTomcatConnectorCustomizer implements TomcatConnectorCustomizer {
public void customize(Connector connector) {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
// 最大接続数を設定する
protocol.setMaxConnections(20000);
// 最大スレッド数を設定する
protocol.setMaxThreads(2000);
protocol.setConnectionTimeout(30000);
}
}
}
三、@ComponentScan() を使用する。
@ComponentScan() を使用してパッケージをスキャンする方が、@SpringBootApplication を使用するよりも速いです。
四、デフォルトの Tomcat コンテナを Undertow に変更する
デフォルトの Tomcat コンテナを Undertow に変更する(Jboss サーバーで、Tomcat のスループットは 5000、Undertow のスループットは 8000)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
``
五、BufferedWriter を使用してバッファリングする。
六、Deferred を使用して非同期呼び出しを実装する
```javascript
@RestController
public class AsyncDeferredController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final LongTimeTask taskService;
@Autowired
public AsyncDeferredController(LongTimeTask taskService) {
this.taskService = taskService;
}
@GetMapping("/deferred")
public DeferredResult<String> executeSlowTask() {
logger.info(Thread.currentThread().getName() + "executeSlowTask");
DeferredResult<String> deferredResult = new DeferredResult<>();
// 長時間実行されるタスクを呼び出す
taskService.execute(deferredResult);
// 長時間タスク内で deferred.setResult("world"); メソッドを使用すると、長時間タスクから戻り、コントローラー内のプロセスが続行されます
logger.info(Thread.currentThread().getName() + "executeSlowTask");
// タイムアウトのコールバックメソッド
deferredResult.onTimeout(new Runnable(){
@Override
public void run() {
logger.info(Thread.currentThread().getName() + " onTimeout");
// タイムアウト情報を返す
deferredResult.setErrorResult("time out!");
}
});
// 処理完了のコールバックメソッドは、タイムアウトでも処理成功でも、このコールバックメソッドに入ります
deferredResult.onCompletion(new Runnable(){
@Override
public void run() {
logger.info(Thread.currentThread().getName() + " onCompletion");
}
});
return deferredResult;
}
}
七、非同期呼び出しは AsyncHandlerInterceptor を使用してインターセプトできます
@Component
public class MyAsyncHandlerInterceptor implements AsyncHandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MyAsyncHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// HandlerMethod handlerMethod = (HandlerMethod) handler;
logger.info(Thread.currentThread().getName()+ "サービス呼び出しが完了し、結果をクライアントに返します");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
if(null != ex){
System.out.println("例外が発生する:"+ex.getMessage());
}
}
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// インターセプトした後、データを書き戻し、元の hello world を以下の文字列に置き換えます
String resp = "my name is chhliu!";
response.setContentLength(resp.length());
response.getOutputStream().write(resp.getBytes());
logger.info(Thread.currentThread().getName() + " afterConcurrentHandlingStarted");
}
}