Memcached(简称为:MC)在互联网广泛使用,是最基础的架构。但MC的mget(即一次获取多个值)一直是一个难题,我们的要求是mget性能上要尽量接近普通memcache get。下面通过一段伪代码介绍了如何以接近get single value的性能实现mget,并且就该架构在实际环境中遇到的一些问题加以讨论。
场景
在开始这个话题之前先考虑一个问题,为什么需要MC mget?Redis不是已经很好的实现了list,hashset,hashtable,zset等等丰富的数据结构吗?这个问题需要从本厂的应用场景开始。用户登陆之后会修改自己的状态,同时获得自己关注人的状态。修改自己的状态是一次MC set过程。自己的关注人列表可以从Redis中获得,此时key是用户的uid,value是关注任的list。获得自己关注人的状态则是根据关注人uid的一次MC get,时间复杂度是O(1)。可以这样做,在程序中执行一个for循环,依次从MC中get关注人状态,这个get过程的时间复杂度是O(n)。当关注人列表扩展到2000时,每次MC get平均耗时2~5ms,这种线性循环获取好友状态的办法要耗时10s,是完全无法接受的。怎么解决这个问题呢?
通过NIO实现mget,并发的执行MC get
danga.memcached2.0.1已经使用NIO框架来实现mget,但是它的实现有些问题,参考:http://blog.csdn.net/e_wsq/article/details/7876801。mget伪代码如下:
private final class Conn {
public ByteBuffer outgoing;
// 使用一个ByteBuffer list来存储从MC读出的内容
public List<ByteBuffer> incoming = new ArrayList<ByteBuffer>();
public Conn(Selector selector) {
channel = getSock().getChannel();
channel.configureBlocking( false );
channel.register( selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this );
outgoing = ByteBuffer.wrap( request.append( "\r\n" ).toString().getBytes() );
}
public boolean isFinished() {
// judge if get "END\r\n"
}
public ByteBuffer getBuffer() {
int last = incoming.size()-1;
if ( last >= 0 && incoming.get( last ).hasRemaining() ) {
return incoming.get( last );
}
else {
ByteBuffer newBuf = ByteBuffer.allocate( 8192 );
incoming.add( newBuf );
return newBuf;
}
}
}
public Object getMulti() throws Exception {
selector = Selector.open();
Conn conn = new Conn(selector);
try {
while(timeRemaining) {
int n = selector.select(timeout));
if ( n > 0 ) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while ( it.hasNext() ) {
SelectionKey key = it.next();
it.remove();
if ( key.isReadable() )
readResponse( key );
else if ( key.isWritable() )
writeRequest( key );
}
}
else {
// error...
}
timeRemaining = timeout - (SystemTimer.currentTimeMillis() - startTime);
}
}
finally {
selector.close();
}
}
public void writeRequest( SelectionKey key ) throws IOException {
ByteBuffer buf = ((Conn) key.attachment()).outgoing;
SocketChannel sc = (SocketChannel)key.channel();
if ( buf.hasRemaining() ) {
sc.write( buf );
}
if ( !buf.hasRemaining() ) {
// switching to read mode for server
key.interestOps( SelectionKey.OP_READ );
}
}
public void readResponse( SelectionKey key ) throws IOException {
Conn conn = (Conn)key.attachment();
ByteBuffer buf = conn.getBuffer();
int count = conn.channel.read( buf );
if ( count > 0 ) {
if ( log.isDebugEnabled() )
log.debug( "read " + count + " from " + conn.channel.socket().getInetAddress() );
if ( conn.isFinished() ) {
...
return;
}
}
}
伪代码中主要给出了NIO中的一些逻辑。并发mget的好处是非常明显的,但这段代码有几个明显的坑。
mget伪代码的几个坑
1. Too many open files的坑
每次getMulti都执行Selector.open()?? Linux系统中,执行Selector.open()打开一对pipe(参考:http://blog.csdn.net/haoel/article/details/2224055),当后续IO慢时,Selector就不能及时关闭。造成大量pipe被创建,导致Too many open files错误。一般NIO的逻辑是只有一个全局selector,新channel注册后只需selector.wakeup() 即可。
2. 死循环的坑
Java6 NIO有两个众所周知的坑:http://bugs.sun.com/view_bug.do?bug_id=6693490和http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933。简单的说,就是Selector应该只在2种情况有返回值,即有网络事件发生或者超时。但是Selector有时却会在没有获得任何selectionKey的情况返回,这是一个Java6 NIO的bug。上面这段mget的伪代码中没有相关处理,容易造成死循环。我们可以参考MINA的解决方法,伪代码如下:
long t0 = System.currentTimeMillis();
int selected = select(1000L);
long t1 = System.currentTimeMillis();
long delta = (t1 - t0);
if ((selected == 0) && !wakeupCalled.get() && (delta < 100)) {
// Last chance : the select() may have been
// interrupted because we have had an closed channel.
if (isBrokenConnection()) {
LOG.warn("Broken connection");
// we can reselect immediately
// set back the flag to false
wakeupCalled.getAndSet(false);
continue;
} else {
LOG.warn("Create a new selector. Selected is 0, delta = " + (t1 - t0));
// Ok, we are hit by the nasty epoll
// spinning.
// Basically, there is a race condition
// which causes a closing file descriptor not to be
// considered as available as a selected channel, but
// it stopped the select. The next time we will
// call select(), it will exit immediately for the same
// reason, and do so forever, consuming 100%
// CPU.
// We have to destroy the selector, and
// register all the socket on a new one.
registerNewSelector();
}
// Set back the flag to false
wakeupCalled.getAndSet(false);
// and continue the loop
continue;
}
这段代码非常清晰,触发条件是selector返回值为0,网络没有断开,并且时间<100ms就认为是触发了Java NIO的bug。处理的方法就是重建一个selector。另外一个可以参考的例子是Jetty的处理方法:http://wiki.eclipse.org/Jetty/Feature/JVM_NIO_Bug
分享到:
相关推荐
java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现Reactor模型源码java基于NIO实现...
基于Java NIO实现五子棋游戏.zip基于Java NIO实现五子棋游戏.zip 基于Java NIO实现五子棋游戏.zip基于Java NIO实现五子棋游戏.zip 基于Java NIO实现五子棋游戏.zip基于Java NIO实现五子棋游戏.zip 基于Java NIO实现...
### Java NIO 实现Socket通信详解 #### 一、NIO与传统IO的区别及优势 ...通过以上步骤,我们可以看到Java NIO是如何实现高效的Socket通信的。这种方式不仅减少了线程的开销,还极大地提高了系统的并发处理能力。
在这个场景下,Java NIO能够帮助服务器有效地处理多个客户端连接,并实现客户端之间消息的互发。 首先,我们要理解Java NIO的基本组件。主要包括通道(Channels)、缓冲区(Buffers)和选择器(Selectors)。通道是...
本项目"基于nio的简易聊天室"旨在通过NIO技术实现一个简单的聊天室服务端和客户端,其特点是有图形用户界面(GUI)供用户交互。 NIO的核心组件包括通道(Channels)、缓冲区(Buffers)和选择器(Selectors)。在这...
本主题“基于nio实现的多文件上传源码”探讨的是如何利用Java NIO来实现高效的多文件上传功能,尤其对于小文件数量较大的情况。 首先,理解NIO的基本概念是必要的。NIO中的“非阻塞”意味着当数据不可用时,读写...
本篇将详细讲解如何使用NIO实现邮件接收,主要涉及JavaMail API和NIO的核心概念。 首先,JavaMail API是Java平台上的一个用于处理邮件的开放源码库,它提供了创建、发送、接收和管理邮件的一系列接口和类。而NIO则...
标题中的“NIO实现telnet”指的是使用Java的非阻塞I/O(New Input/Output,简称NIO)来构建一个telnet服务器。在Java中,NIO是一种替代传统阻塞I/O(BIO)的I/O模型,它提供了更高效的数据传输方式,尤其适合处理...
在本文中,我们将深入探讨如何使用NIO实现客户端之间的通信,并通过一个中心服务器进行消息的转发。 首先,我们来看通道。通道是NIO中的数据传输路径,它可以连接到硬件设备、文件系统或其他服务。例如,...
数据备份与恢复通常通过`mongodump`和`mongorestore`命令来实现,而分片和复制集则是实现高可用性和水平扩展的关键技术。 NIO在MongoDB的实现中主要体现在网络通信层面。MongoDB的Java驱动程序使用NIO进行网络通信...
鹊桥,又称为MagpieBridge,是一款基于Java的内网穿透工具,利用先进的异步I/O模型(AIO/NIO)来实现高效的网络通信。它为开发者提供了在内网环境中进行开发并允许外部进行调试的强大功能,对于远程协作、云服务测试...
本项目"基于Spring Boot + NIO实现的电商平台见证宝服务"旨在利用Spring Boot的便捷性与NIO(非阻塞I/O)的效率,来打造一个高效、稳定且可扩展的服务。下面将详细阐述其中涉及的关键技术点。 首先,Spring Boot是...
设计思想: 每次通过nio读取字节到 fbb中 然后对fbb自己中的内容进行行判断即 10 回车 13 行号 0 文件结束 这样字节的判断,然后 返回行 如果 到达 fbb的结尾 还没有结束,就再通过nio读取一段字节,继续处理。 ...
本篇将基于给定的标题“采用NIO实现一个Socket服务器”来详细阐述如何使用NIO进行Socket服务端的开发。 首先,理解NIO的核心概念: 1. **通道(Channel)**:通道是连接到I/O设备的途径,可以读取或写入数据。常见的...
NIO实现的Http正向代理
总之,使用Java NIO实现异步连接池,不仅优化了多服务访问的性能瓶颈,还通过非阻塞I/O技术显著提升了系统的并发处理能力和健壮性。在高负载、多服务交互的应用场景中,异步连接池成为了提升系统性能的关键技术。
缓冲区具有容量、位置和限制等概念,可以通过get和put方法读写数据。 3. **选择器(Selector)**:选择器是NIO中用于多路复用的关键组件。通过向选择器注册通道,可以监控多个通道的事件,如读、写、连接和接受事件...
压缩包中的文件可能包含了实现这三种通信模型的Java代码示例,通过学习和理解这些代码,你可以更好地掌握Java I/O的运用,并根据需求选择合适的模型。建议从简单的BIO开始,逐步过渡到更复杂的NIO和AIO,理解其工作...