1、基于内存实现
Redis将所有数据存储在内存中,因此它可以非常快速地读取和写入数据,而无需像传统数据库那样将数据从磁盘读取和写入磁盘,这样也就不受I/O限制。
2、I/O多路复用
多路指的是多个socket连接;复用指的是复用一个线程。多路复用主要有三种技术:select、 poll、epoll。epoll是最新的也是目前最好的多路复用技术。Redis使用IO多路复用的原理图如下所示:
Redis内核会一直监听socket上的连接请求,一旦有请求到达就交给Redis线程处理,这就实现了一个Redis线程处理多个IO流的效果。
Redis线程不会阻塞在某一个特定的客户端请求处理上,所以Redis可以同时和多个客户端连接并处理请求。select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器,因此Redis一直在处理事件,提升了Redis 的响应性能。
3、高效的数据结构
Redis中有多种数据类型,每种数据类型的底层都由一种或多种数据结构来支持。正是因为有了这些数据结构,所以大大的提升了Redis存储与读取速度。下图整理了Redis中常见数据类型与底层的数据结构:
3.1 简单动态字符串
Redis的字符串类型底层使用的是简单动态字符串(SDS),它是用来处理字符串的,C语言中用到SDS来处理字符串,C语言中的字符串展示如下图所示:
想要获取字符串“REDIS”的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止。Redis是C语言实现的,那么Redis中字符串是怎么使用SDS的呢?如下图所示:
Redis中用一个len字段记录当前字符串的长度,想要获取长度只需要获取len字段即可。
Redis中SDS修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。具体的操作是如果len长度小于1M,那么将会额外分配与len相同长度的未使用空间,如果修改后长度大于1M,那么将分配1M 的使用空间。
Redis中存在惰性的空间释放,即是SDS缩短时并不会回收多余的内存空间,而是使用free字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free中记录的空间,减少了内存的分配。
3.2 双端链表
Redis的List数据结构更多是被当作队列或栈来使用的,队列和栈的特性一个先进先出,一个先进后出,那么使用双端链表很好实现这些特性,双端链表的如下图所示:
头节点里同时还有一个参数len,用来记录链表长度的,因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)。
3.3 压缩链表
如果在双端链表中,一个链表节点中存储一个小数据(如一个字节)。那么对应的就要保存头节点,前后指针等额外的数据,这样就浪费了空间,同时由于反复申请与释放操作也容易导致内存碎片化,内存的使用效率就太低了,于是就出现了压缩链表,如下图所示:
压缩列表的内存是连续分配的,遍历的速度很快。
3.4 跳跃表
Redis中跳跃表是在链表的基础上增加了多级索引来提升查找效率。如下图所示的跳跃表原理图:
跳跃表每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。
在Redis中跳表真实的存储结构是在头节点里记录了相应的信息,减少了一些不必要的系统开销,如下图所示:
3.5 hashTable
哈希表本质上是一个数组,每一个元素叫做哈希桶,每个桶中的entry保存着键值指针(key和value)。Redis其实就是一个全局哈希表,哈希表的时间复杂度是O(1),通过key的哈希值和哈希函数,就可以定位对应哈希桶的位置,从而确定桶中entry,然后找到对应数据,如下如所示
Redis中存储的数据越来越多的时候,会出现哈希冲突,所以Redis采用链式哈希的方式解决冲突,即同一个桶里面的元素使用链表保存,如下图所示:
随着数据量的增加,哈希冲突越来越多,链表也随之越来越长,进而导致查找性能变差。因此Redis使用了两个全局哈希表,通过rehash操作,增加现有的哈希桶数量,分散单桶元素数量,从而在减少哈希冲突的同时缩短链表长度,提高Redis的查询效率。
4、单线程模型
Redis的单线程指的是Redis的网络IO及键值对指令读写是由一个线程来执行的,对于Redis的持久化、集群数据同步、异步删除等都是其他线程执行。
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。Redis使用单线程的优势如下所示:
(1)不会因为线程创建导致的性能消耗
(2)避免上下文切换引起的CPU消耗,没有多线程切换的开销
(3)避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题。
5、合理的数据编码
对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。Redis中不同的数据类型是如何进行编码转化,如下所示:
(1)String:存储数字时候采用 int类型的编码,如果是非数字的话,采用raw编码。
(2)List:字符串长度及元素个数小于一定范围使用ziplist编码,任意条件不满足,则转化为linkedlist编码。
(3)Hash:hash对象保存的键值对内的键和值字符串长度小于一定值及键值对。
(4)Set:保存元素为整数及元素个数小于一定范围使用intset编码,任意条件不满足,则使用hashtable编码。
(5)Zset:zset对象中保存的元素个数小于及成员长度小于一定值使用 ziplist编码,任意条件不满足,则使用skiplist编码。