面試準備之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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章