前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单线程的Redis为什么辣么快?

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

作者头像
ImportSource
发布2018-08-14 17:22:42
3800
发布2018-08-14 17:22:42
举报
文章被收录于专栏:ImportSourceImportSource
相信你经常听到说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多路复用来实现的响应模型,也就是基于文件描述符,这是一种比多线程模型性能更好的服务端响应实现。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-08-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档