用Java做后端开发不可避免的会用到线程池,比如我们启动一个web server,指定用作处理请求的线程数量,此server便可以并发处理请求,以netty为例,代码如下
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup(4);
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast( new HttpServerCodec());
ch.pipeline().addLast(new HttpObjectAggregator(1024 * 1024));
ch.pipeline().addLast(new HttpServerExpectContinueHandler());
ch.pipeline().addLast(new MainRequestHandler());
}
});
ChannelFuture future = serverBootstrap.bind(1234).sync();
future.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
我们为这个netty web server指定两个线程池,boss 和 worker,boss线程池用作处理接收网络连接;worker线程池用作处理连接上的读写,于是这个web server便有了并发处理请求的能力。
MainRequestHandler类是这个web server的处理程序,我们会把业务代码写在里面
static class MainRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
sendResponse(ctx,"hello world");
}
private void sendResponse(ChannelHandlerContext ctx, String responseText ) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(responseText, CharsetUtil.UTF_8));
response.headers().set("Content-Type", "text/html");
response.headers().set("Content-Length", response.content().readableBytes());
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
示例程序会输出hello world。然而,实际开发项目时逻辑肯定不会这么简单,我们假设代码如下
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
String url = msg.uri();
String result = "";
if( url.equals("/user_list")) {
result = getUserListFromMySql();
} if(url.equals("news_list")) {
result = getNewsListFromMySql();
}
sendResponse(ctx, result);
}
处理程序需要根据不同的url去mysql中获得对应的数据,而从mysql中取数据不可避免的会有等待,在这个过程中当前线程会阻塞,无法继续处理其他任务。假设server的线程池大小为6,某个mysql查询等待时间为200毫秒,假如在这200毫秒之内,来了7个请求,那么前6个任务占满线程池,第7个任务将无法及时被处理程序处理,也就是说没有办法一下子到达 channelRead0 方法内,只有前面6个任务中有任务处理完毕了,才轮到第7个任务。
表面上看这么做没什么问题,如果服务器实在处理不过来,大不了报502嘛,反正这都是家常便饭。但是,假如有这样的需求:当服务器的任务处理不过来时,需要将这些请求的具体信息记录下来供后续分析优化。显然在程序中无法满足这种需求,因为请求根本没有到达程序,谈何记录呢。当然,也可以给这个web server再加上一个前端,比如nginx,通过分析nginx失败日志也能得到结果,但是显然和我们的程序不在同一个层面,没有浑然一体的感觉,更不能精准的控制请求。
一种解决方法是将web server的网络处理线程池和业务处理线程池分开,Java自从有了NIO,通常服务器阻塞的原因肯定不是socket读写上的性能问题造成,性能的问题总是在业务处理上,比如数据的存取,通过将线程池分离,我们总能保证请求一定会到达程序,至于请求有没有响应都是业务处理程序的事了,跟web server上的socket读写无关。
我们将示例代码修改如下
private static ThreadPoolExecutor executor = null;
private static BlockingQueue taskList = null;
static {
taskList = new LinkedBlockingQueue<>(100);
executor = new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,taskList);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
try{
executor.execute(()->{
String url = msg.uri();
String result = "";
if( url.equals("/user_list")) {
result = getUserListFromMySql();
} if(url.equals("news_list")) {
result = getNewsListFromMySql();
}
sendResponse(ctx, result);
});
}catch (RejectedExecutionException e) {
System.out.println("任务堆积数量过多");
}
}
我们给业务处理程序单独建立一个线程池,线程大小为5,对应的任务队列是一个有界队列,容量限制为100, 与此同时我们给线程池指定一个饱和策略,当待处理任务数量大于任务队列容量时,放弃任务的执行并抛出异常,这也是线程池的默认饱和策略。那么,当任务数量超限时,RejectedExecutionException异常被捕获,并打印日志,当然,我们也可以在catch块中做一些其他的事情,比如记录这个请求,或者返回异常提示消息给客户端。
这种做法更容易受程序精确的控制,因为请求已经抵达了程序,我们的程序可以精准的处理每个请求,包括无法被处理的请求。如果是传统的方法,socket读写线程和业务处理线程混在一块,那么当业务负载过高时,新进的请求根本无法到达处理程序,nginx 502没商量,比如传统php就是这种德行。