redis单线程为什么快,Redis 为什么那么快 ?

1、Redis 为什么那么快 ?前段时间面试被问到这个问题了? 我的回答也是很简单,总结大概就有两点 :
事后我我下去又翻了许多资料,主要总结出一下内容 :
 因为Redis是基于内存的操作 , CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽 。既然单线程容易实现 , 而且CPU不会成为瓶颈,所以 Redis 是单线程的 。
IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu 。多路是指网络连接,复用指的是同一个线程 。
这里可能有不理解的地方,我尝试说一下我的理解 :
进程执行都是顺序执行的,当我们在执行一个操作的时候,进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作 。有没有什么解决办法呢 ? 有! 就是上面说的 IO多路复用。IO多路复用本质上是在同一个线程或进程中 , 通过拨动开关的方式来执行多个IO操作 。注意实际上每个IO操作都是独立进行的 。只是由原来的一对一变成了多对多 。
在 Linux 主要有 3 种实现 : select、poll、epoll
select 、poll : 开启一个线程 , 隔一段时间去询问你是否有完成操作 ,如果完成了,你就去执行 。
epoll : 当你可以使用的时候,你发消息去通知,然后执行。
Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据) 。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘 。这样就可以避免因为内存不足而造成访问速度下降的问题 。Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个Redis Server上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上 。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现 。
Redis为了保证查找的速度,只会将value交换出去,而在内存中保留所有的Key 。所以它非常适合Key很小 , Value很大的存储结构 。如果Key很大 , value很小 , 那么vm可能还是无法满足需求 。
相关配置 :

redis单线程为什么快,Redis 为什么那么快 ?

文章插图
2、redis是个单线程的程序,为什么会这么快呢?redis数据库实现原理
首先redis数据库的功能强大一些,因为不像memcached只支持保存字符串,redis支持string, list,set,sorted set,hash table 5种数据结构 。例如存储一个人的信息就可以使用hash table,用人的名字做key , 然后name super ,  age 24,通过key 和 name,就可以取到名字super,或者通过key和age,就可以取到年龄24 。这样 , 当只需要取得age的时候,不需要把人的整个信息取回来,然后从里面找age,直接获取age即可,高效方便 。
为了实现这些数据结构,redis定义了抽象的对象redis object,如下图 。每一个对象有类型 , 一共5种:字符串,链表,集合,有序集合,哈希表 。同时,为了提高效率,redis为每种类型准备了多种实现方式,根据特定的场景来选择合适的实现方式,encoding就是表示对象的实现方式的 。然后还有记录了对象的lru,即上次被访问的时间,同时在redis 服务器中会记录一个当前的时间(近似值,因为这个时间只是每隔一定时间 , 服务器进行自动维护的时候才更新),它们两个只差就可以计算出对象多久没有被访问了 。然后redis object中还有引用计数 , 这是为了共享对象 , 然后确定对象的删除时间用的 。最后使用一个void*指针来指向对象的真正内容 。正式由于使用了抽象redis object,使得数据库操作数据时方便很多,全部统一使用redis object对象即可,需要区分对象类型的时候,再根据type来判断 。而且正式由于采用了这种面向对象的方法,让redis的代码看起来很像c++代码,其实全是用c写的 。
说到底redis还是一个key-value的数据库,不管它支持多少种数据结构,最终存储的还是以key-value的方式,只不过value可以是链表,set , sorted set,hash table等 。和memcached一样,所有的key都是string,而set,sorted set,hash table等具体存储的时候也用到了string 。而c没有现成的string,所以redis的首要任务就是实现一个string , 取名叫sds(simple dynamic string),如下的代码,非常简单的一个结构体,len存储改string的内存总长度 , free表示还有多少字节没有使用,而buf存储具体的数据,显然len-free就是目前字符串的长度 。
字符串解决了,所有的key都存成sds就行了 , 那么key和value怎么关联呢?key-value的格式在脚本语言中很好处理,直接使用字典即可,C没有字典,怎么办呢?自己写一个呗(redis十分热衷于造轮子) 。看下面的代码,privdata存额外信息,用的很少,至少我们发现 。dictht是具体的哈希表,一个dict对应两张哈希表,这是为了扩容(包括rehashidx也是为了扩容) 。dictType存储了哈希表的属性 。redis还为dict实现了迭代器(所以说看起来像c++代码) 。
哈希表的具体实现是和mc类似的做法 , 也是使用开链法来解决冲突,不过里面用到了一些小技巧 。比如使用dictType存储函数指针 , 可以动态配置桶里面元素的操作方法 。又比如dictht中保存的sizemask取size(桶的数量)-1,用它与key做&操作来代替取余运算,加快速度等等 。总的来看 , dict里面有两个哈希表,每个哈希表的桶里面存储dictEntry链表,dictEntry存储具体的key和value 。
前面说过 , 一个dict对于两个dictht , 是为了扩容(其实还有缩容) 。正常的时候,dict只使用dictht[0],当dict[0]中已有entry的数量与桶的数量达到一定的比例后,就会触发扩容和缩容操作,我们统称为rehash,这时 , 为dictht[1]申请rehash后的大小的内存,然后把dictht[0]里的数据往dictht[1]里面移动,并用rehashidx记录当前已经移动万的桶的数量,当所有桶都移完后 , rehash完成 , 这时将dictht[1]变成dictht[0], 将原来的dictht[0]变成dictht[1] , 并变为null即可 。不同于memcached , 这里不用开一个后台线程来做,而是就在event loop中完成,并且rehash不是一次性完成,而是分成多次,每次用户操作dict之前,redis移动一个桶的数据,直到rehash完成 。这样就把移动分成多个小移动完成,把rehash的时间开销均分到用户每个操作上,这样避免了用户一个请求导致rehash的时候,需要等待很长时间,直到rehash完成才有返回的情况 。不过在rehash期间,每个操作都变慢了点,而且用户还不知道redis在他的请求中间添加了移动数据的操作,感觉redis太贱了 。
纯内存数据库 , 如果只是简单的 key-value,内存不是瓶颈 。一般情况下,hash 查找可以达到每秒数百万次的数量级 。
瓶颈在于网络 IO 上 。
根据你测的的 10000/s 来看,客户端和 redis 应该是部署在两台不同的机器 , 并且是使用同步的方式请求 redis. 每次请求需要通过网络把请求发送到 redis 所在的机器,然后等待 redis 返回数据 。时间大部分消耗在网络传输中 。
如果把 redis 和客户端放在同一台机器 , 网络延迟会更小 , 一般情况下可以打到 60000 次每秒甚至更高,取决于机器性能 。
锁不是影响性能的主要因素 。线程锁 (mutex_lock) 只有在遇到冲突的情况下性能会下降 , 而正常情况下,遇到冲突的概率很低 。如果只是简单的加锁、释放锁速度是非常快的,每秒钟上千万次没问题 。memcache 内部用到了大量的锁,并没有见到性能降低 。
线程也不是影响吞吐量的重要因素 。如第一点来说 , 一般情况下,程序处理内存数据的速度远高于网卡接收的速度 。使用线程好处是可以同时处理多条连接,在极端情况下 , 可能会提高响应速度 。
使用 epoll 或 libevent 等因为异步非阻塞 IO 编程只能这么做 。与之对应的是同步阻塞 IO 编程,使用多进程或多线程实现多条连接的处理,比如 apache 。一般情况下,异步非阻塞 IO 模型性能是远高于同步阻塞 IO 模型的,可以参考 nginx 与 apache 性能的对比 。
libevent 并不比 redis 自己实现的 ae_event 慢,代码多是应为 ae_event 只实现了 redis 需要的功能,而 libevent 则具有更多的功能,比如更快的定时器、buffer event 模型,甚至自带了 DNS、HTTP 协议的处理 。并且 libevent 更通用,而 redis 只专注于 linux 平台 。
最后回答问题,快在哪? 1、纯内存操作 2、异步非阻塞 IO
目前想到的原因有这几方面 。
Libevent 。和Memcached不同 , Redis并没有选择libevent 。Libevent为了迎合通用性造成代码庞大(目前Redis代码还不到libevent的1/3)及牺牲了在特定平台的不少性能 。Redis用libevent中两个文件修改实现了自己的epoll event loop(4) 。业界不少开发者也建议Redis使用另外一个libevent高性能替代libev , 但是作者还是坚持Redis应该小巧并去依赖的思路 。一个印象深刻的细节是编译Redis之前并不需要执行./configure 。
CAS问题 。CAS是Memcached中比较方便的一种防止竞争修改资源的方法 。CAS实现需要为每个cache key设置一个隐藏的cas token,cas相当value版本号,每次set会token需要递增,因此带来CPU和内存的双重开销,虽然这些开销很小,但是到单机10G+ cache以及QPS上万之后这些开销就会给双方相对带来一些细微性能差别 。
【redis单线程为什么快,Redis 为什么那么快 ?】单线程有时候比多线程 或多进程更快,比需要考虑并发、锁,也不会增加上下文切换等开销,也即代码更加简洁,执行效率更高~~~
Redis是用内部是快的原因之一,要比较的化,可以同MemCache比较下 。
redis单线程为什么快,Redis 为什么那么快 ?

文章插图
3、redis原理,单线程怎么做到高并发的书本上有 , 我就引用一下吧:
Redis是个单线程程序!这点必须铭记 。
也许你会怀疑高并发的Redis 中间件怎么可能是单线程 。很抱歉,它就是单线程,你的怀疑暴露了你基础知识的不足 。莫要瞧不起单线程 , 除了Redis 之外,Node.js 也是单线程 , Nginx也是单线程,但是它们都是服务器高性能的典范 。
Redis单线程为什么还能这么快?
因为它所有的数据都在内存中 , 所有的运算都是内存级别的运算 。正因为Redis是单线程 , 所以要小心使用Redis 指令 , 对于那些时间复杂度为O(n) 级别的指令,- -定要谨慎使用,一不小心就可能会导致Redis 卡顿 。
Redis单线程如何处理那么多的并发客户端连接?
这个问题 , 有很多中高级程序员都无法回答,因为他们没听过多路复用这个词汇 , 不知
道select 系列的事件轮询API, 没用过非阻塞IO 。
非阻塞IO
但线程 , 只能靠单个处理器速度,内存速度,处理器上的缓存速度 , 总线传输速度 。余下的是你的网络IO 。但线程高并发完全依赖程序的运行速度 。redis这种东西肯定不是但线程的 。一个连接就是一个线程,你这样理解应该不准确 。
redis单线程为什么快,Redis 为什么那么快 ?

文章插图
4、为什么redis比mysql快 Redis将所有数据放在内存中 , 非数据同步正常工作中 , 是不需要从磁盘读取数据的,0次IO 。内存响应时间大约为100纳秒,这是Redis速度快的重要基础 。
mysql是持久化存储,存放在磁盘里面,检索的话 , 会涉及到一定的IO,为了解决这个瓶颈 , 于是出现了缓存 , 比如现在用的最多的 memcached(简称mc) 。
首先,用户访问mc,如果未命中,就去访问mysql,之后像内存和硬盘一样 , 把数据复制到mc一部分redis和mc都是缓存,并且都是驻留在内存中运行的,这大大提升了高数据量web访问的访问速度 。(推荐学习:Redis视频教程)
然而mc只是提供了简单的数据结构,比如 string存储;redis却提供了大量的数据结构,比如string、list、set、hashset、sorted set这些 , 这使得用户方便了好多 , 毕竟封装了一层实用的功能,同时实现了同样的效果,当然用redis而慢慢舍弃mc 。
Redis是单线程 。简化算法的实现,并发的数据结构实现不但困难且测试也麻烦 。第二,单线程避免了线程切换以及加锁释放锁带来的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手 。当然了,单线程也会有它的缺点,也是Redis的噩梦:阻塞 。如果执行一个命令过长,那么会造成其他命令的阻塞,对于Redis是十分致命的,所以Redis是面向快速执行场景的数据库 。
Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll的read、write、close等都转换成事件,不在网络I/O上浪费过多的时间 。实现对多个FD读写的监控,提高性能 。
mysql用于持久化的存储数据到硬盘 , 功能强大,但是速度较慢
redis用于存储使用较为频繁的数据到缓存中,读取速度快
更多Redis相关技术文章 , 请访问Redis数据库使用入门教程栏目进行学习!
redis单线程为什么快,Redis 为什么那么快 ?

文章插图
5、Redis单线程为何可以处理大量请求?这得益于linux的IO多路复用
应用层可以把多个socket连接注册给操作系统,让操作系统帮忙盯着这些socket有没有数据过来(可读/可写) 。
注册完成之后,应用层就可以去干别的事了 。当socket有数据过来时 , 操作系统会通知应用层,应用层再去处理 。这样的优势在于应用层1个线程 , 就可以服务多个网络请求 , 即 IO 多路复用 。
IO多路复用的具体实现模型有 select/poll/epoll,目前epoll是性能最好的 。
一个网络程序,客户端A给服务端B发数据,A编写socket程序,调用write API向这个socket fd写数据 。
写完之后,数据怎么发给B呢?
是需要经过操作系统、网卡、网线发过去的 。
A的操作系统把数据按照网络协议包装好,通过网卡发出去,经过网络线路,最终都到B了 。
B谁先来处理?肯定是操作系统先拿到这些数据,它会先放在内核态,然后通知上层的应用,你的数据来了 , 你来读取吧 。
epoll是操作系统提供的API , 应用层把socket提前注册给操作系统,先调epoll的注册方法,这就托管给操作系统了 。
然后应用层再调用epoll的 wait 方法 , 有fd有数据过来/可写,就返回,返回指的就是wait方法return了,应用层就拿到这个fd可以操作了 。不返回说明,注册的这些fd都不能读写,那应用层就阻塞在wait上等着,除非wait方法传了个timeout参数,那就到了timeout也给应用层返回 。
这是读的情况 。除了读 , 还有写 。写对应的是发数据,fd可以写了,就通知应用层,应用就写这个fd 。
还是拿刚才A->B传输数据来说 。刚才说的是读,是B要读A的数据 。现在看A这一侧,A要写这个fd给B发数据 。
一开始写,fd对应的是操作系统的缓冲区,A写fd,会先写到操作系统的缓冲区里 , 然后由操作系统把缓冲区的数据发给B 。
但如果A一直写一直写,B那边读的慢 , 那A这边的缓冲区就会满了,满了之后应用层还在调write , 肯定就不可以写了 , 那啥时再可以写?A操作系统能知道B那边有没有把缓冲区的数据读走,如果读走了,缓冲区空出来了,那A的操作系统就通知应用层说,缓冲区空出来了,你继续可以写了 。
fd是操作系统封装的数据结构 , 但凡一个应用层要做网络交互 , 必须经过操作系统,所以操作系统就提供了很多高效的办法,比如缓冲区和这些通知机制 。
说白了就是,操作系统是一个大管家,其目的就是为了更好地服务上层应用,所以它做了很多事,这些IO多路复用,都是操作系统在帮应用层干活 。