如何编写高效的Java通信客户端

有很多机会需要编写自己的Java通信客户端,例如redis、memcache等的访问,怎么样编写一个高效的Java通信客户端呢,主要有以下几点:连接、序列化/反序列化、减少上下文切换。

1. 连接
如何高效的使用连接是网络通信中一个很关键的点,像通常用数据库我们都会用到数据库连接池,但其实数据库连接池在连接的使用上效率是不高的,数据库连接池的作用只是降低了建连接本身的消耗,但在获取/归还连接时的并发竞争还是比较严重的,在拿不到连接的时候的等待就影响更明显了。

数据库连接也许是因为各种各样的历史原因所以导致了这样的现象,但在自己可编写的其他的网络通信中,则可以采用更高效的方式,数据库连接池这种的主要问题在于连接获取/归还时的并发竞争,最好的解决方法自然是不要产生竞争。

要做到连接获取/归还不产生竞争,比较靠谱的方法就是连接不独占,也就是多个请求可以用同一个连接来同时发送(专业点说就是连接复用),要做到连接复用,需要有两点支持:NIO或AIO、request ID透传。

NIO或AIO可以实现无需像BIO一样,要占用连接来等响应,而是可以通过异步的回调来实现,这样每个请求就无需独占一个连接,而很多数据库连接目前仍然是采用bio的,so也不可能做成连接复用。

在没有request ID透传的支持下,如果多个请求共用一个连接,会导致的问题是响应回来的时候不知道是哪个请求的响应回来了,因此在连接复用的场景中,都需要增加一个request ID,在发送请求时带上这个id,响应回来的时候也把这个ID原样返回,这样客户端在接到后就可以知道是哪个请求的响应了。

在有了上面两点的支持后,就可以做到连接复用了,连接复用后如果有需要在连接上保持状态信息的,也需要做一些改造,否则仍然会导致连接无法复用。

在连接复用后,是不是还需要数据库连接池那样的创建限定个数的连接呢,通常来说是没有必要的,原因是在像mina/netty/grizzly的这些nio框架的实现下,在发送请求/接收响应的过程中,连接本身在整个处理过程中会需要独占的主要是在写发送缓冲区/接响应包上,写发送缓冲区这点本来就是串行的,所以即使多个连接也不会有什么改善,而接响应包这点上,只要包不是太大,单个连接和多个连接差距基本可以忽略,但如果包太大的话,确实会有影响,因此通常都建议在大多数的场景下采用单个长连接即可。

长连在连接是经过负载均衡设备的场景下会产生一个典型的问题,例如服务端集群有10台机器,期望的肯定是连接均衡的分散在这10台服务器上,但其实是会出现这样的现象:假设服务端其中几台重启了,这个时候客户端请求又发过来了,那么这个时候就会出现连接都建到存活的机器上了,因此最终可能会导致几台机器上有很多的连接,而另外几台完全没有连接,对于这种现象,有个龌龊的解决方法是每个连接在发送了X个请求后主动关闭掉,靠强制的这种重建连接来避免这个问题,最好的方法是连接完全不经过负载均衡设备,但这个通常只适合内部网络场景。

2. 反序列化
在第1点上提到了单个连接不可并发的地方主要是接响应包上,通常很多的通信的实现是在接响应包的动作上进行反序列化,更高效的做法是不要在接响应包的时候就去做反序列化的动作,而是改为在将响应包交给业务线程时,在业务线程里进行反序列化,这样可以尽可能的减少独占连接的时间。

另外一点不用说,就是序列化/反序列化尽可能选用高效的,例如google pb这些,不过也一定要考虑开发易用性。

3. 减少上下文切换
通常来说,在接到请求包或响应包时,由于业务端的处理可能不是纯cpu型的(例如会有io/锁等动作),因此通常都会将包交给另外的线程来处理,通常的情况下实现都会是每接到一个包,就通知下处理的线程,但在nio这类场景中,如果请求/响应包不是很大的话,很多时候接到的一个流中可能会包含多个包,如果每个包都通知一下处理的线程,就意味着一次上下文切换,优化点的做法就是如果流中有多个包,可以在把包都接好后,扔一个List给处理的线程,这在高并发的场景下会有明显的优化效果。

在编写一个高效的通信客户端时,差不多主要就是上面这3点,如果有疑问或更好的建议,欢迎发微信给我,期待!

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。