面试官:redis和memcached有什么区别。我:redis是单线程的,memcaced是多线程的。好,那么redis真的是单线程的吗?
网络IO、线程模型
localhost bin]# ps -T -p 3081
PID SPID TTY TIME CMD
3081 3081 ? 00:00:00 redis-server
3081 3088 ? 00:00:00 redis-server
3081 3089 ? 00:00:00 redis-server
3081 3090 ? 00:00:00 redis-server
纳尼?说好的单线程呢?
从网络IO模型讲起
同步模型(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路复用IO(multiplexing IO)
信号驱动式IO(signal-driven IO)
大家可以了解一下Reactor模式(单线程、多线程),其实Reactor模式中就是利用epoll或者select多路复用函数库来实现。
异步IO(asynchronous IO)
Proactor模式利用异步IO来实现,区别于Reactor,就读事件这个点而言,在于Reactor执行实际的读取操作,而Proactor是操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。
有没有想起Spring中的IOC依赖反转,将对象生命周期控制权讲给Spring容器,那这里我把它形容成读写反转,真正的读写操作交给操作系统。
我们的redis用的是哪种网络IO模型呢,是不是经常听到多路复用,对,没错,redis用的就多路复用的网络模型(I/O多路复用程序的所有功能是通过包装select、epoll、evport和kqueue这些I/O多路复用函数库来实现的),也是我们常提到nio网络模型。
好,的确有点抽象,我们来看一下java里面如何实现的这个模型的。
服务端逻辑代码
public class MultiplexerTimeServerHandler implements Runnable {
private Selector selector = null;
private ServerSocketChannel servChannel = null;
private int port;
private boolean stop;
/**
* 初始化多路复用器,绑定监听端口
*
* @param port
*/
public MultiplexerTimeServerHandler(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
//非阻塞
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
//在通道上注册selector,感兴趣事件是OP_ACCEPT
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器监听" + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
public void run() {
while (!stop) {
//循环监听就绪时间
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (IOException e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册的channel和pipe等资源都会自动去注册并关闭
// ,所有不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//处理事件
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuff = ByteBuffer.allocate(1024);
//上面设置了这里是非阻塞的
int read = sc.read(readBuff);
if (read > 0) {
readBuff.flip();
byte[] bytes = new byte[readBuff.remaining()];
readBuff.get(bytes);
String body = new String(bytes, "utf-8");
System.out.println("服务收到命令:" + body);
String currentTime = "time".equals(body) ? new Date(System.currentTimeMillis()).toString() : "无效命令";
doWrite(sc, currentTime);
} else if (read < 0) {
//对端链路关闭
key.cancel();
sc.close();
} else {
//读到0字节
}
}
}
}
private void doWrite(SocketChannel sc, String content) throws IOException {
if (content != null && content.trim().length() > 0) {
byte[] bytes = content.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
//可能存在写半包情况
sc.write(byteBuffer);
}
}
}
服务端启动代码
public class TimeServer {
public static void main(String... args) throws IOException {
int port = 8888;
MultiplexerTimeServerHandler server = new MultiplexerTimeServerHandler(port);
new Thread(server,"nio-MultiplexerTimeServerHandler-001").start();
}
}
这里我是用个口令记得,“兴趣事件Selector控,可读可行可连接,实际操作交用户(线程用户态)”。
JAVA NIO 的核心三部分是Channel(通道),Buffer(缓冲区), Selector,想学习的可以自行查阅相关资料。
redis的线程模型
文件事件处理器(file event handler)
1.Redis 基于 Reactor 模式
开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)
2.文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字
, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
3.当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)
等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
4.文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
Redis是单线程模型为什么效率还这么高?
1.纯内存访问
:数据基本存放在内存中,这是10万QPS级别访问的重要基础。
2.非阻塞I/O
:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,都转换为了时间,不在I/O上浪费过多的时间。
3.单线程
避免了线程切换和竞态产生的消耗。
4.Redis采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞,对于Redis这种高性能服务是致命的,所以Redis是面向高速执行的数据库。
那多出的几个线程是什么呢
初始化三类线程. 这三类线程被认为是后台执行不影响主线程
BIO_CLOSE_FILE
关闭重写之前的aof文件。
BIO_AOF_FSYNC
定时刷新数据到磁盘上。
BIO_LAZY_FREE
惰性删除过期时间数据。
redis为了保证其高效.一些比较耗时的动作会起线程或者进程来完成.不会阻塞在业务主线程上。