面试准备之redis知识点


 

一.redis为什么会这么快?

1.redis是基于内存进行操作的,没有对硬盘IO操作的瓶颈。

2.redis的数据结构相对简单,操作也相对简单,而且对redis的数据结构进行了专门的设计了的。

3.redis使用单线程多路IO复用,没有了多线程的上下切换,也没有锁竞争。

多路IO复用是指有多个网络请求,只用一个线程来处理。多路IO复用是利用select,poll,epoll模型来监控多个IO流,当没有IO流的时候,线程会阻塞,但是当有IO流事件进来的时候,系统会轮询所有的流(只有epoll会查询那些真正发生了事件的流),告诉线程需要处理就绪流了,这样可以避免无谓的等待。

其实这个问题和问redis为什么用单线程一样的,因为redis处理数据的速度更快,cppu不会成为redis的瓶颈,所以使用单线程就足够了。

二.对select,poll和epoll的理解


多路IO复用,为什么一个线程能调用多个socket,这是调用了linux系统上的select,poll或者epoll方法。
先看一段单线程调用select的伪代码。

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...)

int fds[] =  存放需要监听的socket

while(1){
    int n = select(..., fds, ...)//如果fds里面的socket都没有收到数据的时候当前线程会被挂起
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

当前线程在调用select函数的时候,会将自己监听的所有socket全部传给select方法,由select方法遍历所有的socket,查看socket的缓存是否由接收到数据,如果有,就可以调用这个socket执行相应的操作,然后接着再次调用select来查看有没有socket有接收到数据的;如果没有,这将当前线程挂起。这个方法就可以做到一个线程处理多个socket,而且当没有数据的时候不需要自己一直轮询,而是将自己挂起,然后当有数据进来的时候,再来由select通知自己。不过这里面每调用一次socket都需要将fds从用户核传递到内核空间,然后再遍历一遍所有的socket,这两个操作开销会很大,所以select对fds大小做了限制,也就是最大不超过1024。

poll和select最大的不同是去掉了fds大小的限制。

epoll调用的伪代码

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1){
    int n = epoll_wait(...)
    for(接收到数据的socket){
        //处理
    }
}

epoll和select,poll最大的不同是有三个方法
epoll_create方法会创建出一个epollevent对象,这个对象存储被监听的socked,可以动态的添加和删除要监听的socket,而且还有一个队列存放就绪的socket,也就是缓存区有数据的socket。
epoll_ctl方法将被监听的socket添加到epollevent对象里面去,因为有这个方法,epoll不必每次都将所有要监听的socket从用户空间复制到内核空间。
epoll_wait方法就是等待事件就绪,然后调用相应的线程执行,因为epollevent对象里面有一个列表存储的是就绪的socket,所以就不必遍历所有监听的socket,只需遍历那些有事件发生的socket。


总结一下,用我自己的理解总结一下。
socket有缓冲区用来存储接受收到的数据或者要发送的数据,拿接受数据缓冲区来说,缓冲区进入数据的时候被封装成一个有数据事件,当缓冲区没有数据的时候被封装成一个无数据事件。当有一个线程需要从socket缓冲区里面取数据的时候,缓冲区里面没有数据那个线程就会被阻塞,如果有多个socket,那么就有可能有多个线程被阻塞了,一个服务最多能开多少个线程,这样搞明显不太合适,如果搞一个线程来调用一组socket呢?线程遍历所有的socket,哪一个socket缓冲区里面有数据了,我就处理那个socket,当没有数据的时候,这个线程就一直空循环,这样搞好像也不好,所以在socket和调用线程加了一个代理,也就是select,poll,epoll,当所以socket缓冲区没有值的时候,将处理线程挂起来,然后由select,poll,epoll代理来确认socket里面是否有值,如果有值了,我会唤醒你,让后你来查看你监听的socket,看哪一个缓冲区里面有值,你就处理哪一个。
select,poll和epoll最大的区别select和poll每次被调用的时候也就是确认有没有数据的时候都会将这个线程监听的所有socket从用户空间复制到内核空间,然后遍历所有的sokcte,这两个操作开销很大。而epoll不一样的地方是epoll创建了一个epollevent对象,然后这个对象能动态的添加和删除要监听的socket,并且里面有一个列表用来存储有数据的socket。我觉得epollevnet不用这个对象是存储在内核空间的,所以不必每次都复制,然后epollevent有一个就绪列表,这样不用复制所有的socket,只需要复制就绪的socket就可以了。
fd:文件描述符,一个用于表述指向文件的引用的抽象化概念。我个人的理解就是指向socket缓冲区。以上只是自己的理解,一定会有很多错误的地方,如果看到参考参考就可以了。

三.redis底层的数据结构

redis的数据结构有五种:string,hash,list,set,zset。但是这五种数据结构下面却有各自实现的数据结构,而且每种至少有两种实现的数据结构。

当我们向redis设置key value的时候,不管value是何种类型,都会创建两个redisObject对象,其中一个对应key,一个对应value。

typedef struct redisObject{
     //类型
     unsigned type:4;
     //编码
     unsigned encoding:4;
     //指向底层数据结构的指针
     void *ptr;
     //引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
 
}robj

type代表着这个对象的类型,有string,hash,list,set,zset五种数据类型

可以通过 type  key   来获取存储的value的类型

encoding表示type的底层实现数据结构

  而每种类型的对象都至少使用了两种不同的编码:

说一下redis的底层是如何实现string的,它主要运用的是一个char数组,不过这个数组存储字符串的时候有预留的空间和直接记录字符串的长度。

struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}

字符串这样设计的好处:

1.可以直接获取字符串的长度,不需要去遍历。
2.当修改字符串的内容时候,不需要再次重新分配内存空间(c需要新释放内存空间再重新分配)
3.可以存储二进制文件(图片,视频等)因为二进制文件里面说不定存在空格,而c判断一个文件释放结束是用空格来判断的。

简单动态字符串和动态字符串的差别是简单字符串的redisobject和sds对象的存储空间是连续的,如果字节小于44个字节,则用简单动态字符串。如果对简单动态字符串修改了,无论是否小于44个字节,都会变成动态字符串。
 

list的实现类型有压缩列表和双端列表实现
压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。压缩列表最大的特点就是用一块连续的空间来存储值。
当列表保存元素个数小于512个,每个元素长度小于64字节的时候就选择压缩列表。

hash的实现类型有字典,这里的字典可以参考java的HashMap结构,和压缩列表。hash底层用压缩列表的条件和list类似。
利用压缩列表存储值的数据结构

hset profile name "Tom"
hset profile age 25
hset profile career "Programmer"

 

集合的实现类型有字典和整数集合,整数集合是一个存储整数的数组,这个数组里面的值不重复,而且是从小到大有序的。只有集合对象中所有元素都是整数,而且集合里面的元素不超过512个的时候才会用整数集合。

有序集合的实现方式有压缩列表和跳表加字典的组合方式实现


为什么要用跳表和字典的组合方式,跳表和字典里面的元素都是指向同一个元素的,不会造成空间浪费。
然后跳表保证数据的有序性,字典保证一个元素的唯一性和查找到这个元素的时间复杂度是O(1)。


跳表的数据结构:
1.跳表有多层数据结构,由多层链表组成,链表里面的每一个元素都有两个指针,分别指向下面的元素和右边的元素,下面元素值,就是这一列的都是一样的,同一层的右边的元素会比左边的要大。
2.跳表最底层的链表有所有的元素,元素按从小到大有序。然后当查找元素的时候,从最高的链表头节点开始查找,当要查找的元素比当前值要大,但是却比当前节点的右边的值要小,则向下寻值。
3.当插入元素的时候,会以抛硬币类似的方式,如果抛了三次正面向上,但第四次是向下的,那从底层开始数三层,这几层就都会有这个元素。

参考文章:https://www.cnblogs.com/ysocean/p/9102811.html

 

四.redis和memecached的不同点

1.reids支持的数据类型更加丰富,支持sring,list,hash,set,zset。而memcached只支持字符串。

2.redis支持持久化,而memcached不支持持久化,当服务宕机的时候,memcached数据会全部丢失,而redis可能只会丢失很少的一部分。

3.redis支持集群,有三种集群模式,主从,哨兵和集群,节点之间可以通信。如果是集群的话,实现方式是哈希槽,而memcached的集群方式需要客户端来实现,每个节点是不互相通信的,而且实现的方式一致性哈希。

4.redis处理数据是单线程来实现的,memcached是用多线程来实现的。

5.memcached的内存分配是由一组slab组成,slab里面是由一组chunk组成,同一个slab里面的chunk大小一样,不同的lsab里面的chunk大小可能就不一样,所以memcached存储值使用固定大小的chunk来存储,最大的value不能超过1M。而redis存储值是根据值的大小来分配空间的。

 

发布了89 篇原创文章 · 获赞 110 · 访问量 23万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章