從零單排學Redis【黃金】

前言

只有光頭才能變強java

好的,今天咱們要上黃金段位了,若是還沒經歷過青銅和白銀階段的,能夠先去蹭蹭經驗再回來:git

看過相關Redis基礎的同窗能夠知道Redis是單線程的,不少面試題也極可能會問到「爲何Redis是單線程的還那麼快」。程序員

這篇文章來說講單線程的內部的原理github

文本力求簡單講清每一個知識點,但願你們看完能有所收穫面試

1、基礎鋪墊

在講解Redis以前,咱們先來一些基礎的鋪墊,有更好的閱讀體驗。redis

1.1網路編程

咱們在初學Java的時候確定會學過網絡編程這一章節的,當時學完寫的應用可能就是「網絡聊天室」。數據庫

寫出來的效果可能就是在console噼裏啪啦的輸入數據,而後噼裏啪啦的返回數據,就完事了..(扎心了)編程

網絡編程可簡單分爲TCP和UPD兩種,通常咱們更多關注的是TCP。TCP網絡編程在Java中封裝成Socket和SocketServer,咱們來回顧一下最簡單的TCP網絡編程吧:數組

TCP客戶端服務器

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        //建立發送端的Socket對象
        Socket s = new Socket("192.168.1.106",8888);
        
        //Socket對象能夠獲取輸出流
        OutputStream os = s.getOutputStream();
        os.write("hello,tcp,我來了".getBytes());

        s.close();
    }
}

TCP服務端:

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        //建立接收端的Socket對象
        ServerSocket ss = new ServerSocket(8888);

        //監聽客戶端鏈接,返回一個對應的Socket對象
        //偵聽並接受到此套接字的鏈接,此方法會阻塞
        Socket s = ss.accept();

        //獲取輸入流,讀取數據
        InputStream is = s.getInputStream();

        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String str = new String (bys,0,len);

        String ip = s.getInetAddress().getHostAddress();
        System.out.println(ip + "    ---" +str);

        //釋放資源
        s.close();
        //ss.close();  

    }
}

上面的代碼就能夠實現:客戶端向服務器發送數據,服務端可以接收客戶端發送過來的數據

1.2IO多路複用

以前我已經寫過Java NIO的文章了,Java的NIO也是基於IO多路複用模型的,建議先去看一下再回來,文章寫得挺詳細和通俗的了:JDK10都發布了,nio你瞭解多少?

這裏就簡單回顧一下吧:

  • I/O多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符其中的任意一個進入讀就緒狀態、等等select()函數就能夠返回。
  • select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接

說白了,使用IO多路複用機制的,通常本身會有一套事件機制,使用一個線程或者進程監聽這些事件,若是這些事件被觸發了,則調用對應的函數來處理。

2、Redis事件

Redis服務器是一個事件驅動程序,主要處理如下兩類事件:

  • 文件事件:文件事件其實就是對Socket操做的抽象,Redis服務器與Redis客戶端的通訊會產生文件事件,服務器經過監聽並處理這些事件來完成一系列的網絡操做
  • 時間事件:時間事件其實就是對定時操做的抽象,前面咱們已經講了RDB、AOF、定時刪除鍵這些操做均可以由服務端去定時或者週期去完成,底層就是經過觸發時間事件來實現的!

2.1文件事件

Redis開發了本身的網絡事件處理器,這個處理器被稱爲文件事件處理器

文件事件處理器由四部分組成:

文件事件處理器組成

文件事件處理器使用I/O多路複用程序來同時監聽多個Socket。當被監聽的Socket準備好執行鏈接應答(accept)、讀取(read)等等操做時,與操做相對應的文件事件就會產生,根據文件事件來爲Socket關聯對應的事件處理器,從而實現功能。

要值得注意的是:Redis中的I/O多路複用程序會將全部產生事件的Socket放到一個隊列裏邊,而後經過這個隊列以有序、同步、每次一個Socket的方式向文件事件分派器傳送套接字。也就是說:當上一個Socket處理完畢後,I/O多路複用程序纔會向文件事件分派器傳送下一個Socket。

首先,IO多路複用程序首先會監聽着Socket的AE_READABLE事件,該事件對應着鏈接應答處理器

  • 能夠理解簡單成SocketServet.accpet()

監聽着Socket的AE_READABLE事件

此時,一個名字叫作3y的Socket要鏈接服務器啦。服務器會用鏈接應答處理器處理。建立出客戶端的Socket,並將客戶端的Socket與命令請求處理器進行關聯,使得客戶端能夠向服務器發送命令請求。

  • 至關於Socket s = ss.accept();,建立出客戶端的Socket,而後將該Socket關聯命令請求處理器
  • 此時客戶端就能夠向主服務器發送命令請求了

客戶端請求鏈接,服務器建立出客戶端Scoket,關聯命令請求處理器

假設如今客戶端發送一個命令請求set Java3y "關注、點贊、評論" ,客戶端Socket將產生AE_READABLE事件,引起命令請求處理器執行。處理器讀取客戶端的命令內容,而後傳給對應的程序去執行。

客戶端發送完命令請求後,服務端總得給客戶端迴應的。此時服務端會將客戶端的Scoket的AE_WRITABLE事件與命令回覆處理器關聯。

客戶端的Scoket的AE_WRITABLE事件與命令回覆處理器關聯

最後客戶端嘗試讀取命令回覆時,客戶端Socket產生AE_WRITABLE事件,觸發命令回覆處理器執行。當把全部的回覆數據寫入到Socket以後,服務器就會解除客戶端Socket的AE_WRITABLE事件與命令回覆處理器的關聯。

最後以《Redis設計與實現》的一張圖來歸納:

Redis事件交互過程

2.2時間事件

持續運行的Redis服務器會按期對自身的資源和狀態進行檢查和調整,這些按期的操做由serverCron函數負責執行,它的主要工做包括:

  • 更新服務器的統計信息(時間、內存佔用、數據庫佔用)
  • 清理數據庫的過時鍵值對
  • AOF、RDB持久化
  • 若是是主從服務器,對從服務器進行按期同步
  • 若是是集羣模式,對進羣進行按期同步和鏈接
  • ...

Redis服務器將時間事件放在一個鏈表中,當時間事件執行器運行時,會遍歷整個鏈表。時間事件包括:

  • 週期性事件(Redis通常只執行serverCron時間事件,serverCron時間事件是週期性的)
  • 定時事件

2.3時間事件和文件事件

  • 文件事件和時間事件之間是合做關係,服務器會輪流處理這兩種事件,而且處理事件的過程當中不會發生搶佔。
  • 時間事件的實際處理事件一般會比設定的到達時間一些

3、Redis多線程爲何快?

  • 1)純內存操做
  • 2)核心是基於非阻塞的IO多路複用機制
  • 3)單線程避免了多線程的頻繁上下文切換問題

4、客戶端與服務器

在《Redis設計與實現》中各用了一章節來寫客戶端與服務器,我看完以爲比較底層的東西,也很難記得住,因此我決定總結一下比較重要的知識。若是之後真的遇到了,再來補坑~

服務器使用clints鏈表鏈接多個客戶端狀態,新添加的客戶端狀態會被放到鏈表的末尾

客戶端--鏈表

  • 一個服務器能夠與多個客戶端創建網絡鏈接,每一個客戶端能夠向服務器發送命令請求,而服務器則接收並處理客戶端發送的命令請求,並向客戶端返回命令回覆。
  • Redis服務器使用單線程單進程的方式處理命令請求。在數據庫中保存客戶端執行命令所產生的數據,並經過資源管理來維持服務器自身的運轉。

4.1客戶端

客戶端章節中主要講解了Redis客戶端的屬性(客戶端狀態、輸入/輸出緩衝區、命令參數、命令函數等等)

typedef struct redisClient{
    
    //客戶端狀態的輸入緩衝區用於保存客戶端發送的命令請求,最大1GB,不然服務器將關閉這個客戶端
    sds querybuf;  
    
    
    //負責記錄argv數組的長度。
    int argc;   
	
    // 命令的參數
    robj **argv;  
	
    // 客戶端要執行命令的實現函數
    struct redisCommand *cmd, *lastcmd;  


    //記錄了客戶端的角色(role),以及客戶端所處的狀態。 (REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI) 
    int flags;             
    
    //記錄客戶端是否經過了身份驗證
    int authenticated;     
    
    //時間相關的屬性
    time_t ctime;           /* Client creation time */       
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    
    
    //固定大小的緩衝區用於保存那些長度比較小的回覆
    /* Response buffer */
    int bufpos;
    char buf[REDIS_REPLY_CHUNK_BYTES];
    
    //可變大小的緩衝區用於保存那些長度比較大的回覆
    list *reply; //可變大小緩衝區由reply 鏈表和一個或多個字符串對象組成
    //...
}

4.2服務端

服務器章節中主要講解了Redis服務器讀取客戶端發送過來的命令是如何解析,以及初始化的過程。

服務器從啓動到可以處理客戶端的命令請求須要執行如下的步驟:

  • 初始化服務器狀態
  • 載入服務器配置
  • 初始化服務器的數據結構
  • 還原數據庫狀態
  • 執行事件循環

總的來講是這樣子的:

def main():

	init_server();

	while server_is_not_shutdown();
		aeProcessEvents()

	clean_server();

從客戶端發送命令道完成主要包括的步驟:

  • 客戶端將命令請求發送給服務器
  • 服務器讀取命令請求,分析出命令參數
  • 命令執行器根據參數查找命令的實現函數,執行實現函數並得出命令回覆
  • 服務器將命令回覆返回給客戶端

5、最後

如今臨近雙十一買阿里雲服務器就特別省錢!以前我買學生機也要9.8塊錢一個月,如今最低價只須要8.3一個月!

不管是Nginx/Elasticsearch/Redis這些技術都是在Linux下完美運行的,若是仍是程序員新手,買一個學習Linux基礎命令,學習搭建環境也是不錯的選擇。

若是有要買服務器的同窗可經過個人連接直接享受最低價https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli


原本也想把「複製」(主從)在這邊一塊兒寫的,但寫完可能就很長了,因此留到下一篇吧。

若是你們有更好的理解方式或者文章有錯誤的地方還請你們不吝在評論區留言,你們互相學習交流~~~

參考資料:

  • 《Redis設計與實現》
  • 《Redis實戰》

一個堅持原創的Java技術公衆號:Java3y,歡迎你們關注

3y全部的原創文章:

相關文章
相關標籤/搜索