前言
熟悉 Netty 的同学都知道,不能在 Netty 中做耗时的,不可预料的操作,比如数据库,网络请求,这将会严重影响 Netty 对 Socket 的处理速度。而解决方法就是将耗时任务添加到异步线程池中。但就添加线程池这步操作来讲,可以有2种方式,而且这2种方式的区别也蛮大的。今天就好好讲一讲。
1. 处理耗时业务的第一种方式-------handler 种加入线程池
以我们之前的 Netty 的 demo 源码例子,在 channelRead 方法种进行异步:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ThreadPoolExecutor executorPool = ThreadPoolFactoryUtils.CreateThreadPoolIfAbsent(threadPoolConfig,"service-handler-pool",false); ThreadPoolFactoryUtils.printThreadPoolStatus(executorPool); executorPool.execute(new Runnable() { @SneakyThrows @Override public void run() { String byteBuf = (String) msg; logger.info(Thread.currentThread().getName() + " server recvied: " +byteBuf); StudentReq studentReq = (StudentReq) S_UtilsXml.getEntity(byteBuf,StudentReq.class); logger.info(Thread.currentThread().getName() + " request data: "+studentReq.toString()); Thread.sleep(1000); Student stu = new Student(); RepTransaction transaction = new RepTransaction(); String repXml = ""; if (studentReq != null){ stu = studentService.getStudentById(studentReq.getStudentId()); if (stu!=null){ logger.info(Thread.currentThread().getName() + " response data: " + stu.toString()); transaction = S_UtilsXml.getRepTransaction(byteBuf); transaction.getHeader().getStatus().setRetCd("000000"); transaction.getHeader().getStatus().setDesc("success"); repXml = S_UtilsXml.getRepXml(transaction,stu); }else { logger.info(Thread.currentThread().getName() + " response data is null"); transaction = S_UtilsXml.getRepTransaction(byteBuf); transaction.getHeader().getStatus().setRetCd("000000"); transaction.getHeader().getStatus().setDesc("数据不存在"); repXml = S_UtilsXml.getNoDataRepXml(transaction); } }else { logger.info(Thread.currentThread().getName() + " 请求报文数据内容为null"); transaction = S_UtilsXml.getRepTransaction(byteBuf); transaction.getHeader().getStatus().setRetCd("999999"); transaction.getHeader().getStatus().setDesc("false"); repXml = S_UtilsXml.getNoDataRepXml(transaction); } logger.info(Thread.currentThread().getName() + " 应答报文: " +repXml); ctx.writeAndFlush(repXml); } }); }
上图中的 channelRead 方法,我们模拟了一个耗时 10 秒的操作,于是,我们将这个任务提交到了一个自定义的业务线程池中,这样,就不会阻塞 Netty 的 IO 线程。
这样操作之后,整个程序的逻辑是这样的:
解释一下上图,当 IO 线程轮询到一个 socket 事件,然后,IO 线程开始处理,当走到耗时 handler 的时候,将耗时任务交给业务线程池。当耗时任务执行完毕再执行 pipeline write 方法的时候(代码中使用的是 context 的 write 方法,上图画的是执行 pipeline 方法),会将任务这个任务交给 IO 线程。
下面是 write 方法的源码:
当判定下个 outbound 的 executor 线程不是当前线程的时候,会将当前的工作封装成 task ,然后放入 mpsc 队列中,等待 IO 任务执行完毕后执行队列中的任务。很明显,下个任务的线程肯定是 IO 线程,因为我们没有设置。
2. 处理耗时业务的第二种方式-------Context 中添加线程池
第二种方式是 Netty 建议的方式,在添加 pipeline 中的 handler 时候,添加一个线程池:
而 handler 中的代码不用做任何修改。
当我们在调用 addLast 方法添加线程池后,handler 将优先使用这个线程池,如果不添加,将使用 IO 线程。
所以,当走到 AbstractChannelHandlerContext 的 invokeChannelRead 方法的时候,executor.inEventLoop() 是不会通过的,因为当前线程是 IO 线程,Context(也就是 Handler) 的 executor 是业务线程,所以会异步执行,如下:
总结: 两种方式的对比和思考
有什么区别呢?第一种方式在 handler 中添加异步,可能更加的自由,比如如果需要访问数据库,那我就异步,如果不需要,就不异步,异步会拖长接口响应时间。因为需要将任务放进 mpscTask 中。如果不凑巧,IO 时间很短,task 很多,可能一个循环下来,都没时间执行整个 task,导致响应时间达不到指标。
第二种方式是 Netty 建议的,但是,这么做会将整个 handler 都交给业务线程池。不论耗时不耗时,都加入到队列里,不够灵活。
再回顾一下我们刚开始的图吧: