Netty ChannelHandler 性能優(yōu)化
1. 前言
本節(jié)我們主要來(lái)繼續(xù)講解 ChannelHandler 的其它特性,主要講解如何去進(jìn)行 ChannelHandler 業(yè)務(wù)鏈表的常見(jiàn)性能優(yōu)化。
2. 優(yōu)化途徑
通常情況下為了提高自定義業(yè)務(wù) Handler 的性能需要進(jìn)行一定的優(yōu)化策略,常見(jiàn)的優(yōu)化方案分別是縮短傳播路徑、Handler 單利等。
- 傳播路徑: 如果業(yè)務(wù)很復(fù)雜的情況,由很多的 Handler 組成的時(shí)候,鏈條過(guò)長(zhǎng)會(huì)消耗性能,因此,一般都是動(dòng)態(tài)的刪除一些沒(méi)用的 Handler。
- Handler 單利: 每個(gè)客戶(hù)端進(jìn)來(lái),都會(huì)為每個(gè) Channel 創(chuàng)建一輪 Handler 并且加入到 Pipeline 進(jìn)行管理,new 的過(guò)程是消耗性能的。
3. 熱插拔
上節(jié)我們學(xué)習(xí)了 ChannelHandler 的生命周期,其中有一個(gè)關(guān)鍵的方法是 handlerRemoved (),在 handler 被移除的時(shí)候觸發(fā)該事件,針對(duì)該事件,其實(shí)我們可以靈活的擴(kuò)展自己的業(yè)務(wù)功能。
需求:客戶(hù)端和服務(wù)端之間通信,必須需要先認(rèn)證。
實(shí)例:
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//1.登錄認(rèn)證Handler
ch.pipeline().addLast(new LoginHandler());
//2.其他業(yè)務(wù)Handler
ch.pipeline().addLast(new OtherHandler());
}
});
通過(guò)以上的代碼,我們就能很好的解決了客戶(hù)端登錄認(rèn)證問(wèn)題,但是我們會(huì)發(fā)現(xiàn),在登錄認(rèn)證成功之后,客戶(hù)端發(fā)起其他類(lèi)型請(qǐng)求的時(shí)候,每次請(qǐng)求 LoginHandler 都會(huì)被執(zhí)行,那么應(yīng)該怎么去解決這個(gè)問(wèn)題呢?
解決思路:在客戶(hù)端第一次連接服務(wù)端時(shí),進(jìn)行賬號(hào)認(rèn)證,認(rèn)證成功之后,把 LoginHandler 給移除掉。
實(shí)例:
public class LoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.省略了部分代碼(轉(zhuǎn)換ByteBuf,對(duì)象流反序列化)
//2.獲取Map
Map<String,String> map=(Map<String,String>)iss.readObject();
//3.認(rèn)證賬號(hào)、密碼,并且響應(yīng)
String username=map.get("username");
String password=map.get("password");
if(username.equals("admin")&&password.equals("123456")){
//3.1.給客戶(hù)端響應(yīng)
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("success".getBytes()));
//3.2.移除該Handler,這樣下次請(qǐng)求就不會(huì)再執(zhí)行該Handler了
ctx.pipeline().remove(this);
}else{
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("error".getBytes()));
ctx.channel().closeFuture();
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
System.out.println("LoginHandler被移除");
}
}
總結(jié),動(dòng)態(tài)新增和移除 Handler,也稱(chēng)之為熱插拔,在真實(shí)項(xiàng)目開(kāi)發(fā)當(dāng)中其實(shí)非常的有用。
4. Handler 單利
4.1 @Shareable
ch.pipeline().addLast(new LoginHandler());
添加鏈表節(jié)點(diǎn)的時(shí)候,我們是手工 new 一個(gè)對(duì)象,其實(shí)也就是說(shuō),每個(gè)客戶(hù)端連接進(jìn)來(lái)的時(shí)候,都需要組建一條雙向鏈表,并且都是 new 每個(gè)節(jié)點(diǎn)的對(duì)象,我們都知道每次 new 性能肯定是不高。
Spring 的 IOC 其實(shí)就是解決手工 new 對(duì)象的,項(xiàng)目啟動(dòng)的時(shí)候把所有對(duì)象創(chuàng)建完放到 Spring 容器,后面每次使用的時(shí)候無(wú)需再創(chuàng)建,而是直接從容器里面獲取,這種方式可以提高性能。同樣道理,Netty 也提供類(lèi)似的功能,那就是 @Shareable
注解修飾的 Handler,只要用該注解修飾之后,那么該 Handler 就會(huì)變成共享,也就是說(shuō)被所有的客戶(hù)端所共享,無(wú)需每次都創(chuàng)建,自然性能會(huì)得到提升。
實(shí)例:
//使用注解修飾
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//提前創(chuàng)建好
final ServerLoginHandler serverLoginHandler=new ServerLoginHandler();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//這里無(wú)需再創(chuàng)建,只需要傳遞實(shí)例即可
ch.pipeline().addLast(serverLoginHandler);
}
});
serverBootstrap.bind(80);
}
}
4.2 @Shareable 線(xiàn)程不安全
對(duì)于共享的 Handler,很容易就會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題,多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)同一個(gè)對(duì)象不會(huì)出現(xiàn)任何的線(xiàn)程安全問(wèn)題,但是有讀有寫(xiě),則就會(huì)產(chǎn)生線(xiàn)程安全問(wèn)題,因此需要特別注意,因此,如果使用了 @Shareable 修飾了 Handler,那么千萬(wàn)不要包含全局變量、全局靜態(tài)變量,否則就會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題。
實(shí)例:
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//全局變量
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//遞增
count++;
}
}
疑問(wèn):為什么以上的代碼在并發(fā)情況下是不安全的呢?
原因是,每個(gè)線(xiàn)程內(nèi)部都會(huì)開(kāi)辟一個(gè)內(nèi)存空間,從主內(nèi)存中拷貝 count 值,在線(xiàn)程中遞增之后,再把結(jié)果寫(xiě)到主內(nèi)存當(dāng)中。并發(fā)情況下,多個(gè)線(xiàn)程之間可能取得的值是一樣,然后線(xiàn)程之間又不可見(jiàn)性,因此就會(huì)導(dǎo)致線(xiàn)程不安全。
解決:如果開(kāi)發(fā)過(guò)程中遇到類(lèi)似的問(wèn)題,應(yīng)該如何解決呢?
直接使用 AtomicXxx
去代替,AtomicXxx 是 J.U.C 下提供的工具類(lèi),底層是通過(guò) CAS 無(wú)鎖機(jī)制去控制,保證線(xiàn)程安全。
4.3 集成 Spring 容器
其實(shí),在真實(shí)開(kāi)發(fā)項(xiàng)目當(dāng)中,一般都是把 Handler 直接交給 Spring 容器進(jìn)行管理,也就是說(shuō)在 Handler 類(lèi)上添加 Spring 提供的 @Component 注解即可。
主要目的:
- 統(tǒng)一把 Handler 交給 Spring 來(lái)管理;
- Handler 一般都是需要和底層的數(shù)據(jù)庫(kù)進(jìn)行交互的,真實(shí)項(xiàng)目當(dāng)中一般都是使用 Spring 來(lái)管理 ORM 組件,如果 Handler 不交給 Spring 管理,那么操作數(shù)據(jù)庫(kù)的時(shí)候就會(huì)相對(duì)麻煩。
實(shí)例:
//交給Spring容器管理
@Component
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//注入dao
@Autowired
private UserDao userDao;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
@Autowired
private ServerLoginHandler serverLoginHandler;
//這里無(wú)需再創(chuàng)建,只需要傳遞實(shí)例即可
ch.pipeline().addLast(serverLoginHandler);
5. 小結(jié)
本內(nèi)容主要是從兩個(gè)方面去進(jìn)行業(yè)務(wù) Handler 性能上面的優(yōu)化,分別是
- 熱插拔: 在執(zhí)行過(guò)程中動(dòng)態(tài)的刪除無(wú)用的 Handler, 縮短 Handler 的傳播距離;
- 單例: 避免每個(gè)客戶(hù)端的連接進(jìn)來(lái)時(shí)都重復(fù)創(chuàng)建 Handler,使用單利的集中方式以及線(xiàn)程安全問(wèn)題。