那么你想得太少了,我个人认为Redis的快是基于多方面的:不但是单线程和内存,还有底层的数据结构设计,网络通信的设计,主从、哨兵和集群等等方面的设计~
下面,我将360°为你揭开RedisQPS达到10万/秒的神秘面纱。
首先值得称赞的第一点:Redis底层使用的数据结构很多,但是却没有直接使用这些数据结构来实现键值对数据库,而是基于数据结构创建了一个对象(redisObject)系统。(是不是觉得有点面向对象编程的意思~)
对象系统里面包括了字符串对象,列表对象,哈希对象、集合对象和有序集合对象。
使用对象的好处:
一个对象怎么设置不同的数据结构实现?
在讲解前,我们必须要了解Redis对象的结构。
它三个重要的部分:type属性、encoding属性,和ptr属性。
我们用字符串对象为例:
我们都知道,Redis的SET命令其实是针对字符串的,但是它也可以设置数值。那底层是怎么做的呢?
它会将String对象的encoding属性标识为REDIS_ENCODING_INT,表示这个键对应的值是Long类型的整数。
而当我们利用APPEND命令往值后面添加字符串呢?
此时会将String对象的encoding属性的标识为REDIS_ENCODING_RAW,表示这个值此时是简单动态字符串。
正是因为使用对象,通过type、encoding和prt属性,使得同一个对象可以适应在不同的场景下,使得不同的改变不需要创建新的键值对,这样使得Redis的对象使用效率非常的高。
Redis的字符串对象采用三种编码:int、embstr和raw。
int编码就不用说了,就是为了兼容SET命令可以设置数值。
而embstr和raw最大的区别就是内存分配操作次数:
Redis中字符串对象的底层是使用SDS(SimpleDynamicString)实现的。
SDS有三部分:
首先介绍一下使用len属性和free属性的好处:
得益于SDS有len属性,获取字符串长度的复杂度为O(1);
得益于SDS有free属性,可以杜绝缓冲区溢出,字符串扩展前可以根据free属性来判断是否满足直接扩展,不满足则需要先执行内存重分配操作,然后再扩展字符串。
我们都知道修改字符串长度很有可能导致触发内存重分配操作,但是Redis对于内存重分配有两个优化策略:
空间预分配:
惰性空间释放:
目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
当然了,我们还可以通过修改redis.h/REDIS_SHARED_INTEGERS常量来修改创建共享字符串对象的数量。
我们都知道Redis是使用C语言开发的,所以SDS一样遵循C字符串以空字符结尾的惯例,所以SDS可以重用很多
简单介绍一下ziplist的结构:
压缩列表是一种为节约内存而开发的顺序型数据结构,所以在Redis里面压缩列表被用做列表键和哈希键的底层实现之一。
正是利用压缩列表,不但使得数据非常紧凑而节约内存,而且还可以利用它的结构来做到非常简单的顺序遍历、逆序遍历,O(1)复杂度的获取长度和所占内存大小等等。
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
我们先看看整数集合的结构:
intset一开始不会直接使用最大类型来定义数组,而是利用升级操作,当元素的值达到一定长度时,会重新为数组分配内存空间,并将数组里的旧元素的类型进行升级。
这样做好处:
因为整数集合没有降级操作,所以从另外一个角度看,升级操作其实也会浪费内存:如果整数集合里只有一个数值是int64_t,而其他数值都是小于它的,但是整数集合的编码将还是保持INTSET_ENC_INT64,就是说,小于int64_t的整数还是会用int64_t的空间来保存。
每当别人问Redis为啥这么快?脱口而出的不是基于内存就是基于单线程。
Redis使用基于Reactor模式实现的网络通信,它使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器就会调用套接字之前关联好的事件处理器来处理这些事件。
因为Redis是单线程的,所以I/O多路复用程序会利用队列来控制产生事件的套接字的并发;队列中的套接字以有序、同步、每次一个的方式分派给文件事件分派器。
多种I/O复用机制:
常见的I/O复用机制有很多种,例如select、epoll、evport和kqueue等等。
Redis对上面的多种I/O复用机制都进行了各自的封装,在程序编译时会自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现。
我们都知道,文件事件的发生都是随机的,因为Redis服务器永远不可能知道客户端下次发送命令是什么时候,所以程序也不可能一直阻塞着直到发生文件事件。
Redis2.8前的复制功能:
缺点:
假设主从服务器断开连接,当从服务器重新连接上后,又要重新执行一遍同步(sync)操作;但是其实,从服务器重新连接时,数据库状态和主服务器大致是一样的,缺少的只是断开连接过程中,主服务器接收到的写命令;每次断线后都需要重新执行一遍完整的同步操作,这样会很浪费主服务器的性能,毕竟BGSAVE命令要读取此时主服务器完整的数据库状态。
Redis2.8后对复制算法进行了很大的优化:
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONFACK
心跳检测的三大作用:
min-slaves-to-write3min-slaves-max-lag10解释:那么在从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令,这里的延迟值就是上面提到的INFOreplication命令的lag值。3、哨兵模式的订阅连接设计Sentinel不但会与主从服务器建立命令连接,还会建立订阅连接。
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送PUBLISH命令,命令附带的是Sentinel本身的信息和所监听的主服务器的信息;接着接收到此命令的主从服务器会向_sentinel_:hello**频道发送这些信息。
而其他所有都是监听此主从服务器的Sentinel可以通过订阅连接获取到上面的信息。
这也就是说,对于每个与Sentinel的服务器,Sentinel既通过命令连接向服务器的sentinel:hello频道发送信息(PUBLISH),又通过订阅连接从服务器的sentinel:hello频道接收信息(SUBSCRIBE)。
通过这种方式,监听同一个主服务器的Sentinel们可以互相知道彼此的存在,并且可以根据频道消息更新主服务器实例结构(sentinelRedisInstance)的sentinels字典,还可借此与其他Sentinel建立命令连接,方便之后关于主服务器下线检查、选举领头Sentinel等等的通信。
Redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由两个cluster.h/clusterMsgDataGossip结构组成。
利用Gossip协议,可以使得集群中节点更新的信息像病毒一样扩散,这样不但扩散速度快,而且不需要每个节点之间都发送一次消息才能同步集群中最新的信息。
至此,我自己能想到的使得Redis性能优越的设计都在这里了。当然了,它的厉害之处远远不止这些~
大家都知道,使用Redis是非常简单的,来来去去就几个命令,但是当你深入Redis底层的设计和实现,你会发现,这真的是一个非常值得大家深究的开源中间件!!!