動態

詳情 返回 返回

Redis-單線程模型 - 動態 詳情

大家好,我是半夏之沫 😁😁 一名金融科技領域的JAVA系統研發😊😊
我希望將自己工作和學習中的經驗以最樸實最嚴謹的方式分享給大家,共同進步👉💓👈
👉👉👉👉👉👉👉👉💓寫作不易,期待大家的關注和點贊💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓關注微信公眾號【技術探界】 💓👈👈👈👈👈👈👈👈

前言

Redis6.0引入了多線程模型,那麼在Redis6.0之前,Redis是單線程模型,那麼單線程模型的Redis的底層模型是什麼,為什麼單線程模型還能那麼快,本篇文章將對Redis的單線程模型進行學習。

注:Redis是單線程僅指Redis服務端的網絡IO是單線程的,Redis的集羣數據同步,持久化等都是多線程的。

正文

一. 準備知識

學習Redis中的單線程模型,會涉及一些操作系統相關的知識,所以先對這些知識進行一個簡單介紹。

1. linux操作系統中的I/O

linux操作系統中,一切事物對象都是文件,linux執行任何形式的I/O操作時,都是對一個文件描述符進行讀取或寫入,注意這裏的文件描述符不單會關聯傳統意義上的文件,還可能會關聯管道,鍵盤,顯示器或者網絡連接。

2. socket概念

既然linux操作系統中的任何形式的I/O都是對一個文件描述符的讀取或寫入,那麼網絡I/O也不例外,通過socket()函數可以創建網絡連接,其返回的socket就是文件描述符,通過socket就可以像操作文件那樣來操作網絡通信,例如使用read()函數來讀取對端計算機傳來的數據,使用write()函數來向對端計算機發送數據。

socket又叫套接字,其將不同主機上的進程或者相同主機上的不同進程進行通信做了一層抽象,也可以將socket理解為應用層到傳輸層的一層抽象。下表給出了兩種常用的socket

socket類型 説明
流式socket 用於TCP通信,提供可靠的,面向連接的通信。
數據報socket 用於UDP通信,提供不可靠,無連接的通信。

在進行網絡通信的時候,需要一對socket,一個運行於客户端,一個運行於服務端,下圖進行一個簡單示意。

那麼整個通信流程可以進行如下概括。

  • 服務端運行後,會在服務端創建listen-socketlisten-socket會綁定服務端的ipport,然後服務端進入監聽狀態;
  • 客户端請求服務端時,客户端創建connect-socketconnect-socket描述了其要連接的服務端的listen-socket,然後connect-socketlisten-socket發起連接請求;
  • connect-socketlisten-socket成功連接後(TCP三次握手成功),服務端會為已連接的客户端創建一個代表該客户端的client-socket,用於後續和客户端進行通信;
  • 客户端與服務端通過socket進行網絡I/O操作,此時就實現了客户端和服務端中的不同進程的通信。

3. 用户進程緩衝區與內核緩衝區

用户進程訪問系統資源(磁盤,網卡,鍵盤等)時,需要切換到內核態(Kernel Mode),訪問結束後,又需要從內核態切換為用户態(User Mode),這種切換十分耗時,所以用户進程會在用户進程空間中開闢一塊緩衝區域,叫做用户進程緩衝區,用户進程如果是讀系統資源,則會將讀到的系統資源寫入用户進程緩衝區,後續讀就讀用户進程緩衝區的內容,用户進程如果是寫數據到系統資源,則會將寫的數據先寫入用户進程緩衝區,然後再將用户進程緩衝區的內容寫到系統資源。所以用户進程緩存區會減少用户進程在用户態和內核態之間的切換次數,從而降低切換的時間。

用户進程訪問系統資源實際上需要藉助操作系統內核完成,所以與系統資源發生I/O的實際是操作系統內核,操作系統內核為了減少與系統資源實際的I/O的次數,也有一個緩衝區叫做內核緩衝區,如果是對系統資源的讀,則先將系統資源數據讀取並寫入內核緩衝區中,然後再將內核緩衝區的內容寫入用户進程緩衝區,如果是對系統資源的寫,則先將用户進程緩衝區的內容寫入內核緩衝區, 然後再將內核緩衝區的內容寫到系統資源。這樣可以有效降低操作系統內核與系統資源的實際I/O次數,降低I/O帶來的時間消耗。

下面以一個服務端處理一個客户端請求為例,對用户進程緩衝區與內核緩衝區進行一個更直觀的説明。(注:實際的網絡請求比下面的圖示更為複雜,下面的圖示只是一個大致流程的體現,目的是幫助體會用户進程緩衝區與內核緩衝區的作用)

那麼上述過程可以概括如下。

  • 操作系統內核通過與網卡(系統資源)進行網絡I/O讀取客户端請求數據到內核緩衝區中;
  • 服務端用户進程將內核緩衝區內容寫入用户進程緩衝區中,隨後用户進程讀取用户進程緩衝區的內容並處理業務;
  • 服務端用户進程將響應內容寫入用户進程緩衝區,然後再將用户進程緩衝區的內容寫入內核緩衝區;
  • 操作系統內核通過與網卡進行網絡I/O,將內核緩衝區的內容寫到網卡。

4. 同步阻塞I/O,同步非阻塞I/OI/O多路複用

同步阻塞I/O是用户進程調用read時發起的I/O操作,此時用户進程由用户態轉換到內核態,只有在內核態中將I/O操作執行完後,才會從內核態切換回用户態,這期間用户進程會一直阻塞。同步阻塞I/OBlocking IOBIO)示意圖如下。

同步非阻塞I/O是用户進程調用read時,用户進程由用户態轉換到內核態後,此時如果沒有系統資源數據能夠被讀取到內核緩衝區中,返回read失敗,並從內核態切換回用户態。也就是用户進程發起I/O操作後會立即得到一個操作結果,同時用户進程需要在read失敗時一直重複的發起read,直至read成功。同步非阻塞I/ONon-Blocking IO,NIO)示意圖如下。

I/O多路複用是一個用户進程中對多個文件描述符進行監控,一旦有文件描述符可以進行I/O操作,內核會通知用户進程對相應的文件描述符進行I/O操作。最簡單的實現是使用select操作來完成對多個文件描述符的監控,具體做法如下。

  • 在用户進程中將文件描述符註冊到select的文件描述符列表中;
  • 執行select操作,此時用户進程由用户態轉換到內核態,然後內核會查找出select的文件描述符列表中所有可以進行I/O操作的文件描述符,並返回,此時內核態轉換到用户態;
  • 用户進程在select操作返回前會一直阻塞,直至select操作返回,此時用户進程獲得了可以I/O的文件描述符列表;
  • 用户進程獲得了可以I/O操作的文件描述符列表後,會對列表中每個文件描述符發起I/O操作。

I/O多路複用(IO Multiplexing)可以用下圖進行示意。

5. 單Reactor單線程模型

有了上面1-4點的基礎,現在來介紹單Reactor單線程模型。已知在客户端與服務端通信的過程中,出現了三種socket,如下所示。

  • listen-socket,是服務端用於監聽客户端建立連接的socket
  • connect-socket,是客户端用於連接服務端的socket
  • client-socket,是服務端監聽到客户端連接請求後,在服務端生成的與客户端連接的socket

(注:上述中的socket,可以被稱為套接字,也可以被稱為文件描述符。)

那麼先看一下如下的客户端請求服務端的模型。

上圖中的Server主線程中創建了listen-socket用於監聽客户端的連接請求,當Client1創建connect1-socket併發起connect操作時,Server主線程會從accept操作返回並得到代表Client1client1-socket,隨後Server在主線程中處理Client1的請求,此時Client2創建connect2-socket併發起connect操作,由於Server主線程正在處理Client1的請求,所以Server此時不會立即與Client2建立連接,等到Server主線程中處理完了Client1的請求並斷開與Client1的連接後,此時Server才會再與Client2建立連接。上述的客户端請求服務端的模型,本質就是同步阻塞I/O模型,對於服務端來説,這種模型有兩個問題,如下所示。

  • 服務端是單線程的,同一時間只能在服務端主線程中監聽到一個客户端建立連接的請求,並且只會在處理完當前建立了連接的客户端的請求後,才會繼續與下一個客户端建立連接;
  • 服務端的listen-socketaccept操作是阻塞的,服務端與客户端建立連接後,client-socketreadwrite操作是阻塞的,換句話説,服務端要麼阻塞在listen-socketaccept操作上,要麼阻塞在client-socketreadwrite操作上。

那麼單Reactor單線程模型在引入了多路複用I/O後,對上面第二個問題進行了優化:服務端主線程中使用select或者epoll等操作,來同時監視listen-socketclient-socket。以select舉例,服務端主線程中一開始會在select的文件描述符列表中添加listen-socket,隨後調用select進入監視狀態(此時主線程阻塞在select上),此時如果客户端的connect-socket發起了connect操作,服務端主線程就會從select上返回,並且判斷是listen-socket準備就緒,所以會得到代表客户端的client-socket,該client-socket會被加入到select的文件描述符列表中,然後服務端主線程又調用select進入監視狀態,此時是同時監視listen-socketclient-socket,後續主線程從select返回後,判斷如果是listen-socket準備就緒,則將得到的client-socket加入select的文件描述符列表,如果是client-socket準備就緒,則處理對應的客户端的請求。單Reactor單線程模型可以用下圖進行示意。

Reactor單線程模型中,只有一個Reactor,負責調用select來監視listen-socketclient-socket,當有socket準備就緒時,稱有事件發生,如果是listen-socket準備就緒,則發生了連接事件,如果是client-socket準備就緒,則發生了讀寫事件,不同的事件由dispatch來分發到不同的模塊進行處理,連接事件由Acceptor來獲取client-socket並加入到select的文件描述符列表,讀寫事件由Handler來處理即執行客户端的請求並響應。

Reactor單線程模型的單線程體現在上述的操作均都是發生在主線程中,即當同時有連接事件和讀寫事件準備就緒時,單Reactor單線程模型會串行的處理連接事件和讀寫事件,該模型的優點就是簡單且沒有併發問題,缺點就是通常處理連接事件很快但是處理讀寫事件會較慢從而造成CPU資源被浪費,假若處理讀寫事件也很快,那麼單Reactor單線程模型會是一個優秀的選擇,恰好在Redis中,由於數據都是存儲在內存中,Redis服務端響應客户端的讀寫事件的速度是很快的,所以,Redis中的單線程模型,實際就是單Reactor單線程模型

二. Redis中的文件事件處理器

Redis服務端是通過listen-socket來獲取客户端連接,通過client-socket來處理客户端請求,listen-socketclient-socket可連接,可讀或者可寫時都會產生事件,稱為文件事件,即文件事件是Redis服務端對socket的操作的抽象。Redis有一個文件事件處理器來處理文件事件,示意圖如下所示。

Redis服務端會在I/O多路複用器中將socket準備就緒的操作入隊列,所以準備就緒的操作會作為文件事件有序的被文件事件分派器分派到事件處理器的不同模塊處理。 Redis中的文件事件處理器就是一個單Reactor單線程模型,並且單線程是體現在事件處理器處理不同的事件時是單線程的。

下面給出客户端請求Redis服務端的流程示意圖。

最後對上圖做如下幾點説明。

  • 如果客户端的connect-socket執行connect操作,或者客户端向Redis發起寫請求,那麼對應的socket會產生AE_READABLE事件;
  • 如果客户端向Redis發起讀請求,那麼對應的socket會產生 AE_WRITABLE事件;
  • 上述流程圖中,編號相同表示同一個請求的處理步驟。

總結

Redis的單線程模型,就是單Reactor單線程模型,Redis使用I/O多路複用,在單線程中輪詢socket,並將對Redis庫的建立連接,關閉連接,讀數據和寫數據請求都轉換成了文件事件,最後Redis還使用其實現的文件事件分派器和事件處理器來處理不同的事件,整體執行效率高,還節省了多線程的開銷。


大家好,我是半夏之沫 😁😁 一名金融科技領域的JAVA系統研發😊😊
我希望將自己工作和學習中的經驗以最樸實最嚴謹的方式分享給大家,共同進步👉💓👈
👉👉👉👉👉👉👉👉💓寫作不易,期待大家的關注和點贊💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓關注微信公眾號【技術探界】 💓👈👈👈👈👈👈👈👈
user avatar sean_5efd514dcd979 頭像 jdcdevloper 頭像
點贊 2 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.