单线程的Redis为什么辣么快?

相信你经常听到说redis是单线程的。那么接下来你就会疑问为什么单线程还这么快。

你之所以问这样的问题。是因为你认为只有多线程分别接收connection才可以更快,就像过去的tomcat那样,同时开多个线程来响应。

web container的多线程模型

然而多线程其实并不是最好的一种解决方案,多线程首先不能创建的太多,创建多了消耗很大。比如线程之间的上下文切换成本是非常高的。

其实还有一种消耗更少,可以完美替代多线程模型的io模型,那就是操作系统底层的IO多路复用。在这种情况下,只需要一个线程来响应请求。然后进来的IO被以文件描述符的方式来新建。也就是由多个文件描述符的这种方式来替代多线程的方式。

IO多路复用(IO Multiplexing)

redis就是使用的这种方式,就是使用IO多路复用的方式。所以你的观念需要更新,那就是除了多线程,还有一种更屌的方式,那就是多路复用。

说到多路复用。其实多路复用也有好几种,select、poll、epoll、evport、kqueue。

下面主要介绍三种:

1、select。

2、poll。

3、epoll。

以上三种是在内核机制上对文件描述符(file descriptor)集合进行轮询的三种方式(过去是多线程,现在流行多文件描述符,Linux/Unix系统不是有句名言吗?一切皆文件,说就是文件描述符。)。本质上就是三个函数,下面分别介绍。

Select

这种方式就是有一个叫select的函数,如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

读、写、异常fd_set

调用select函数后,便开始阻塞,也就是block了,一直到指定的文件描述符列表中有就绪的文件描述符(读取描述符、写入描述符、异常描述符)或超时,成功返回后,然后循环修改文件描述符列表,使其只包含准备就绪的文件描述符。理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

以下是官网的一个例子:

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <wait.h>

#include <signal.h>

#include <errno.h>

#include <sys/select.h>

#include <sys/time.h>

#include <unistd.h>

#define MAXBUF 256

void child_process(void)

{

sleep(2);

char msg[MAXBUF];

struct sockaddr_in addr = {0};

int n, sockfd,num=1;

srandom(getpid());

/* Create socket and connect to server */

sockfd = socket(AF_INET, SOCK_STREAM, 0);

addr.sin_family = AF_INET;

addr.sin_port = htons(2000);

addr.sin_addr.s_addr = inet_addr("127.0.0.1");

connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));

printf("child {%d} connected \n", getpid());

while(1){

int sl = (random() % 10 ) + 1;

num++;

sleep(sl);

sprintf (msg, "Test message %d from client %d", num, getpid());

n = write(sockfd, msg, strlen(msg)); /* Send message */

}

}

int main()

{

char buffer[MAXBUF];

int fds[5];

struct sockaddr_in addr;

struct sockaddr_in client;

int addrlen, n,i,max=0;;

int sockfd, commfd;

fd_set rset;

for(i=0;i<5;i++)

{

if(fork() == 0)

{

child_process();

exit(0);

}

}

sockfd = socket(AF_INET, SOCK_STREAM, 0);

memset(&addr, 0, sizeof (addr));

addr.sin_family = AF_INET;

addr.sin_port = htons(2000);

addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));

listen (sockfd, 5);

for (i=0;i<5;i++)

{

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);

if(fds[i] > max)

max = fds[i];

}

while(1){

FD_ZERO(&rset);

for (i = 0; i< 5; i++ ) {

FD_SET(fds[i],&rset);

}

puts("round again");

select(max+1, &rset, NULL, NULL, NULL);

for(i=0;i<5;i++) {

if (FD_ISSET(fds[i], &rset)){

memset(buffer,0,MAXBUF);

read(fds[i], buffer, MAXBUF);

puts(buffer);

}

}

}

return 0;

}

总之,select之后,就要进行循环就绪的文件描述符然后处理事情。

Poll

也是一种系统调用。和select不同的是poll函数的参数被包装了一下:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

改成直接传入一个结构pollfd数组,这个pollfd定义如下:

struct pollfd {

int fd;

short events;

short revents;

};

这样就是针对每个文件描述符,是包装成一个pollfd类型,然后把事件填充进去。成功返回后,然后循环检出revents字段。

把上面的例子改成基于poll的,如下:

for (i=0;i<5;i++)

{

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);

pollfds[i].events = POLLIN;

}

sleep(1);

while(1){

puts("round again");

poll(pollfds, 5, 50000);

for(i=0;i<5;i++) {

if (pollfds[i].revents & POLLIN){

pollfds[i].revents = 0;

memset(buffer,0,MAXBUF);

read(pollfds[i].fd, buffer, MAXBUF);

puts(buffer);

}

}

}

就和select一样,我们需要循环检查每个pollfd,去看对应的文件描述符是否准备就绪。但是已经不需要在每次迭代中都去构建集合。

Poll vs Select

1、poll不再限制最大量。你可以从poll函数的参数中看到,select里边是有最大量限制的。

2、poll()对于大值文件描述符更有效。 想象一下,通过select()观察值为900的单个文件描述符 - 内核必须检查每个传入集的每个bit位,直到第900位。poll是数组来管理fd,而select是用bit位来存放。

3、select的文件描述符数量是有最大量限制的。fd_set的最大size是1024。每个fd是1bit,所以fd_set就是一个32个整数数组(32 *32bit = 1024 bits)。

struct fd_set{

long int fd_sets[32];

}

select的fd_set

4、select的文件描述符集合每次迭代都要重新构建,而poll是输入的时候就提前分类好了,所以在迭代的时候就不需要重新分拣和构建了。

5、select更加的通用,而poll在有些Unix系统上是不支持的。

Epoll

这个词是event poll的意思。它是select和poll的增强版。就是更屌的。使用select和poll的时候,每次调用函数后,就无法再添加socket了。而epoll可以动态的添加和删除socket,也就是文件描述符。

epoll把自己的任务分成了三步走:

  1. 使用epoll_create在内核中创建上下文。
  2. 使用epoll_ctl向/从上下文添加和删除文件描述符。
  3. 使用epoll_wait等待上下文中的事件。

上面的例子可以改成epoll来实现:

struct epoll_event events[5];

int epfd = epoll_create(10);

...

...

for (i=0;i<5;i++)

{

static struct epoll_event ev;

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);

ev.events = EPOLLIN;

epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);

}

while(1){

puts("round again");

nfds = epoll_wait(epfd, events, 5, 10000);

for(i=0;i<nfds;i++) {

memset(buffer,0,MAXBUF);

read(events[i].data.fd, buffer, MAXBUF);

puts(buffer);

}

}

epoll的文件描述符都怎么存的呢?它用的是红黑树来存储。

epoll的文件描述符是存储在红黑树中,每个节点的结构叫做epitem(此图采自网上)

Poll vs EPoll

  1. 在epoll_wait调用后依然可以添加或删除文件描述符,也就是socket。
  2. epoll_wait成功返回后,返回直接就是准备就绪的文件描述符,不用你再去循环检查了。
  3. epoll有更好的性能。从O(N)到O(1)。
  4. epoll支持自动挡和手动挡,具体可以看到手册去。
  5. epoll没那么通用,是Linux专有的,有的系统不支持。

Redis支持四种多路复用

好,上面简单介绍了select、poll、epoll。

现在我们来看看redis的内部的多路复用实现。

可以看到ae_开头的就是redis针对多路复用的四种实现。分别是

epoll、evport、kqueue、select。除了epoll和select,还有evport和kqueue。

evport:Solaris 10的新特性。

kqueue:最初是在FreeBSD 4.1中被引入,后来支持了NetBSD, OpenBSD, DragonflyBSD和 macOS。

由此我们可以推断redis的这四种实现是为了满足不同的操作系统而开发的。

关于IO多路复用

IO多路复用这个词听着并不能让你有什么感觉,如果你是第一次听的话,还是回归到英文:IO Multiplex。就是“多个IO使用起来”。和本文开头对应起来就是以前是多个线程搞事情,现在是一个线程下,多个IO搞事情。

关于文件描述符

In Unix and related computer operating systems, a file descriptor (FD, less frequently fildes) is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket. File descriptors form part of the POSIX application programming interface.

根据英文原文,file decriptor就是一个抽象。是一个什么的抽象呢?是对访问一个file或者其他input 或output 资源的抽象,比如pipe或者网络socket。也就是说是对socket的抽象,那socket就是一种IO,所以IO多路复用使用的就是文件描述符来搞。

总结

select更通用,但有fd_set的1024限制,并且返回的是所有的fd,需要你自己去检查哪些是就绪的。poll不再限制size,并且参数传入也是一个数组,而且迭代时不需要每次都去重新构建fd_set,但却无法动态的添加和删除socket。而epoll是最强大的,可以动态添加删除socket,而且返回的fd_set是已经就绪了的fd,无须你再循环检查,但epoll是针对Linux的,部分操作系统不支持。redis内部提供了四种多路复用实现,除了select和epoll外,还实现了基于evport和kqueue的reactor模型。redis是单线程,却如此快,主要是因为它是基于操作系统底层的IO多路复用来实现的响应模型,也就是基于文件描述符,这是一种比多线程模型性能更好的服务端响应实现。

本文分享自微信公众号 - ImportSource(importsource)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏琯琯博客

PHP 操作 Redis

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

22210
来自专栏微信公众号:Java团长

购物车的原理及实现(仿京东实现原理)

1)用户没登陆用户名和密码,添加商品, 关闭浏览器再打开后 不登录用户名和密码 问:购物车商品还在吗?

37210
来自专栏battcn

一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis

Spring3.1 引入了激动人心的基于注解( annotation)的缓存( cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者...

25210
来自专栏微信公众号:Java团长

为什么分布式一定要有Redis?

考虑到绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操作,对 Redis 整体缺乏一个认知...

12710
来自专栏后台及大数据开发

springBoot系列教程03:redis的集成及使用

1.为了高可用,先安装redis集群 参考我的另一篇文章 http://www.cnblogs.com/xiaochangwei/p/7993065.html

16130
来自专栏微信公众号:Java团长

玩转Redis集群(上)

要想搭建一个最简单的Redis集群,那么至少需要6个节点:3个Master和3个Slave。为什么需要3个Master呢?如果你了解过Hadoop/Storm/...

9120
来自专栏微信公众号:Java团长

Java Web现代化开发:Spring Boot + Mybatis + Redis二级缓存

Spring-Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一。Mybatis是一个十分轻量好用的ORM框架。Red...

37820
来自专栏Java架构沉思录

Redis 深度历险:核心原理与应用实践

Redis 是互联网技术架构在存储系统中使用最为广泛的中间件,它也是中高级后端工程师技术面试中面试官最喜欢问的工程技能之一,特别是那些优秀的、竞争激烈的大型互联...

31420
来自专栏帘卷西风的专栏

关于luasocket的编译和部署

好了,luasocket的编译和部署就讲完了,做完上面这些步骤,就可以用luasocket来编写网络程序了。

32500
来自专栏纯洁的微笑

一次线上问题排查所引发的思考

之前或多或少分享过一些内存模型、对象创建之类的内容,其实大部分人看完都是懵懵懂懂,也不知道这些的实际意义。

14110

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励