這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然
這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、CRM 等等功能:
Boot 倉庫:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 倉庫:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 雙版本
Redis有多快
根據官方基準測試,在具有平均硬體的Linux機器上執行的單個Redis例項通常可以為簡單命令
(O(N)
或O(log(N)))
實現8w+的QPS,使用流水線批處理可以達到100w
。從效能角度來看,Redis可以稱為高效能的快取解決方案。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
Redis為什麼這麼快
面試時經常被問到Redis高效能的原因,典型回答是下面這些:
-
C語言實現,雖然C語言有助於Redis的效能,但語言並不是核心因素。 -
基於記憶體實現:僅記憶體I/O,相對於其他基於磁碟的資料庫(MySQL等),Redis具有純記憶體操作的自然效能優勢。 -
I/O複用模型,基於 epoll/select/kqueue
等I/O多路複用技術實現高吞吐量網路I/O。 -
單執行緒模型,單執行緒無法充分利用多核,但另一方面,它避免了多執行緒的頻繁上下文切換以及鎖等同步機制的開銷。
為什麼Redis選擇單執行緒?
上面回答了是單執行緒的,接著會問為啥採用單執行緒模型。
Redis的CPU通常不會成為效能瓶頸,因為通常情況下Redis要麼受到記憶體限制,要麼受到網路限制。例如,使用流水線技術,在平均Linux系統上執行的Redis甚至可以每秒處理100萬個請求,因此,如果應用程式主要使用
O(N)
或O(log(N))
命令,它幾乎不會使用太多CPU。這基本上意味著CPU通常不是資料庫的瓶頸,因為大多數請求不會佔用太多CPU資源,而是佔用I/O資源。特別是對於Redis來說,如果不考慮像RDB/AOF這樣的永續性方案,Redis是完全的記憶體操作,非常快速。Redis的真正效能瓶頸是網路I/O,即客戶端和伺服器之間的網路傳輸延遲,因此Redis選擇了單執行緒的I/O多路複用來實現其核心網路模型。
「實際上選擇單執行緒的更具體原因可以總結如下:」
-
避免過多的上下文切換開銷:在多執行緒排程過程中,需要在CPU之間切換執行緒上下文,並且上下文切換涉及一系列暫存器替換、程式堆疊重置,甚至包括程式計數器、堆疊指標和程式狀態字等快速表項的退休。因為單個程序內的多個執行緒共享程序地址空間,執行緒上下文要比程序上下文小得多,在跨程序排程的情況下,需要切換整個程序地址空間。 -
避免同步機制的開銷:如果Redis選擇多執行緒模型,因為Redis是一個數據庫,不可避免地涉及底層資料同步問題,這必然會引入一些同步機制,如鎖。我們知道Redis不僅提供簡單的鍵值資料結構,還提供列表、集合、雜湊等豐富的資料結構。不同的資料結構對於同步訪問的鎖定具有不同的粒度,這可能會在資料操作期間引入大量的鎖定和解鎖開銷,增加了程式的複雜性並降低了效能。 -
簡單和可維護性:Redis的作者 Salvatore Sanfilippo
(化名antirez
)在Redis的設計和程式碼中有一種近乎偏執的簡單哲學,當您閱讀Redis原始碼或向Redis提交PR時,您可以感受到這種偏執。因此,簡單且可維護的程式碼必然是Redis在早期的核心準則之一,引入多執行緒不可避免地導致了程式碼複雜性的增加和可維護性的降低。
Redis真的是單執行緒的嗎?
在回答這個問題之前,我們需要澄清“單執行緒”概念的範圍:它是否涵蓋了核心網路模型或整個Redis?如果是前者,答案是肯定的。Redis的網路模型在v6.0之前一直是單執行緒的;如果是後者,答案是不。Redis早在v4.0版本中就引入了多執行緒。
-
Redis v4.0
(引入多執行緒進行非同步任務) -
Redis v6.0
(正式在網路模型中實現I/O多執行緒)
單執行緒網路模型
從Redis v1.0到v6.0,Redis的核心網路模型一直是典型的單Reactor模型:使用
epoll/select/kqueue
等多路複用技術來處理事件(客戶端請求)在單執行緒事件迴圈中,最後將響應資料寫回客戶端。
在這裡有幾個核心概念需要了解。
-
客戶端:客戶端物件,Redis是典型的CS架構(客戶端<–>伺服器),客戶端透過套接字與伺服器建立網路通道,然後傳送請求的命令,伺服器執行請求的命令並回復。Redis使用client結構來儲存與客戶端相關的所有資訊,包括但不限於包裝套接字連線 -- *conn
,當前選擇的資料庫指標--*db
,讀緩衝區--querybuf
,寫緩衝區--buf
,寫資料鏈接列表--reply
等。 -
aeApiPoll
:I/O多路複用API,基於epoll_wait/select/kevent等系統呼叫封裝,監聽讀寫事件以觸發,然後進行處理,這是事件迴圈(Event Loop)中的核心函式,是事件驅動器執行的基礎。 -
acceptTcpHandler
:連線響應處理器,底層使用系統呼叫accept接受來自客戶端的新連線,並將新連線註冊繫結命令讀取處理器以進行後續的新客戶端TCP連線處理;除了此處理器外,還有相應的acceptUnixHandler用於處理Unix域套接字和acceptTLSHandler用於處理TLS加密連線。 -
readQueryFromClient
:命令讀取處理器,用於解析並執行客戶端請求的命令。 -
beforeSleep
:在事件迴圈進入aeApiPoll並等待事件到達之前執行的函式。它包含一些常規任務,如將來自client->buf或client->reply的響應寫回客戶端、將AOF緩衝區中的資料持久化到磁碟等。還有一個afterSleep函式,在aeApiPoll之後執行。 -
sendReplyToClient
:命令回覆處理器,當事件迴圈後仍然在寫緩衝區中有資料時,將註冊並繫結到相應連線的sendReplyToClient
命令,當連線觸發寫就緒事件時,將剩餘的寫緩衝區中的資料寫回客戶端。
Redis內部實現了一個高效能事件庫AE,基於
epoll/select/kqueue/evport
,用於為Linux/MacOS/FreeBSD/Solaris
實現高效能事件迴圈模型。Redis的核心網路模型正式構建在AE之上,包括I/O多路複用和各種處理器繫結的註冊,所有這些都是基於它實現的。
到這裡,我們可以描述一個客戶端從Redis請求命令的工作方式。
-
Redis伺服器啟動,開啟主執行緒事件迴圈,將 acceptTcpHandler
連線響應處理器註冊到使用者配置的監聽埠的檔案描述符上,等待新連線的到來。 -
客戶端與伺服器之間建立網路連線。 -
呼叫 acceptTcpHandler
,主執行緒使用AE的API將readQueryFromClient
命令讀取處理器繫結到新連線的檔案描述符上,並初始化一個client
以繫結此客戶端連線。 -
客戶端傳送請求命令,觸發讀就緒事件,主執行緒呼叫 readQueryFromClient
將客戶端透過套接字傳送的命令讀入客戶端->querybuf
讀緩衝區。 -
接下來呼叫 processInputBuffer
,在其中使用processInlineBuffer
或processMultibulkBuffer
來根據Redis協議解析命令,最後呼叫processCommand來執行命令。 -
根據請求命令的型別(SET、GET、DEL、EXEC等),分配適當的命令執行器來執行,最後呼叫 addReply
系列函式中的一系列函式將響應資料寫入到相應客戶端的寫緩衝區中:client->buf或client->reply,client->buf是首選的寫出緩衝區,具有固定大小的16KB,通常可以緩衝足夠的響應資料,但如果客戶端在時間視窗內需要非常大的響應,則它將自動切換到client->reply連結列表,理論上可以容納無限數量的資料(受機器物理記憶體限制)最後,將client新增到LIFO佇列clients_pending_write
。 -
在事件迴圈中,主執行緒執行 beforeSleep
->handleClientsWithPendingWrites
,遍歷clients_pending_write
佇列,並呼叫writeToClient
將客戶端寫緩衝區中的資料返回給客戶端,如果寫緩衝區中仍然有剩餘資料,則註冊sendReplyToClient
命令到連線的回覆處理器,等待客戶端寫入後繼續在事件迴圈中寫回剩餘的響應資料。
對於那些希望利用多核效能的人來說,官方的Redis解決方案簡單而直接:在同一臺機器上執行更多的Redis例項。事實上,為了保證高可用性,一個線上業務不太可能以獨立執行的方式存在。更常見的是使用Redis分散式叢集,具有多個節點和資料分片,以提高效能和確保高可用性。
多執行緒非同步任務
如前所述,Redis在v4.0版本中引入了多執行緒來執行一些非同步操作,主要用於非常耗時的命令。透過將這些命令的執行設定為非同步,可以避免阻塞單執行緒事件迴圈。
我們知道Redis的DEL命令用於刪除一個或多個鍵的儲存值,它是一個阻塞命令。在大多數情況下,要刪除的鍵不會儲存太多值,最多幾十個或幾百個物件,因此可以快速執行。但如果要刪除具有數百萬個物件的非常大的鍵值對,則此命令可能會阻塞至少幾秒鐘,由於事件迴圈是單執行緒的,它會阻塞隨後的其他事件,從而降低吞吐量。
Redis的作者
antirez
對解決這個問題進行了深思熟慮。起初,他提出了一個漸進式的解決方案:使用定時器和資料遊標,他將逐步刪除少量資料,例如1000個物件,最終清除所有資料。但這個解決方案存在一個致命的缺陷:如果其他客戶端繼續寫入正在逐步刪除的鍵,而且刪除速度跟不上寫入的資料,那麼記憶體將無休止地被消耗,這個問題透過一個巧妙的解決方案得以解決,但這個實現使Redis更加複雜。多執行緒似乎是一個牢不可破的解決方案:簡單且容易理解。因此,最終,antirez選擇引入多執行緒來執行這類非阻塞命令。antirez
在他的部落格中更多地思考了這個問題:懶惰的Redis是更好的Redis。因此,在Redis v4.0之後,已添加了一些非阻塞命令,如
UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC
等,它們會在後臺執行緒中執行,不會阻塞主執行緒事件迴圈。這使得Redis可以更好地應對一些特定情況下的命令處理。多執行緒非同步任務的主要特點:
-
後臺執行緒:這些非同步任務由一個或多個後臺執行緒負責執行,不影響主執行緒的事件迴圈,因此主執行緒可以繼續處理其他請求。 -
非阻塞:非同步任務是非阻塞的,因此它們不會阻止其他命令的執行,即使它們可能需要很長時間才能完成。 -
高可用性:透過將某些耗時操作轉移到後臺執行緒,Redis可以更好地保持高可用性。
總結 Redis的網路模型是單執行緒的,這意味著它使用單個事件迴圈來處理所有客戶端請求。這個設計的優點是簡單性和可維護性,但需要謹慎處理一些可能導致事件迴圈阻塞的命令。
為了處理一些非常耗時的命令,Redis v4.0引入了多執行緒非同步任務。這些非同步任務在後臺執行緒中執行,不會阻塞主執行緒的事件迴圈,從而提高了Redis的吞吐量和可用性。
總而言之,Redis的單執行緒事件迴圈和多執行緒非同步任務的設計是為了在效能和簡單性之間取得平衡,以滿足各種不同用例的需求。理解Redis的這些基本原理對於使用Redis進行高效能資料儲存和快取非常重要。
多執行緒網路模型
正如前面提到的,Redis最初選擇了單執行緒的網路模型,原因是CPU通常不是效能瓶頸,瓶頸往往是記憶體和網路,因此單執行緒足夠了。那麼為什麼Redis現在引入了多執行緒呢?簡單的事實是Redis的網路I/O瓶頸變得越來越明顯。
隨著網際網路的快速增長,網際網路業務系統處理越來越多的線上流量,而Redis的單執行緒模式導致系統在網路I/O上消耗了大量CPU時間,從而降低了吞吐量。提高Redis效能有兩種方式:
-
最佳化網路I/O模組 -
提高機器記憶體讀寫速度
後者依賴於硬體的發展,目前尚無法解決。因此,我們只能從前者入手,網路I/O的最佳化可以分為兩個方向:
-
零複製技術或DPDK技術 -
利用多核
零複製技術存在侷限性,無法完全適應像Redis這樣的複雜網路I/O場景。DPDK技術透過繞過核心棧來繞過NIC I/O,過於複雜,需要核心甚至硬體的支援。
因此,充分利用多個核心是最佳化網路I/O最具成本效益的方式。
在6.0版本之後,Redis正式將多執行緒引入核心網路模型中,也稱為I/O執行緒,現在Redis具有真正的多執行緒模型。在前面的部分中,我們瞭解了Redis 6.0之前的單執行緒事件迴圈模型,實際上是一個非常經典的反應器模型。

反應器模式在Linux平臺上的大多數主流高效能網路庫/框架中都有應用,比如
netty、libevent、libuv、POE(Perl)、Twisted(Python)
等。反應器模式實際上是指使用I/O多路複用(
I/O multiplexing
)+非阻塞I/O(non-blocking I/O
)模式。Redis的核心網路模型,直到6.0版本,都是單一的反應器模型:所有事件都在單一執行緒中處理,儘管在4.0版本中引入了多執行緒,但更多是用於特定場景的補丁(刪除超大鍵值等),不能被視為核心網路模型的多執行緒。
一般來說,單一反應器模型,在引入多執行緒後,會演變為多反應器模型,具有以下基本工作模型。

與單一執行緒事件迴圈不同,這種模式有多個執行緒(子反應器),每個執行緒維護一個獨立的事件迴圈,主反應器接收新連線並將其分發給子反應器進行獨立處理,而子反應器則將響應寫回客戶端。
多反應器模式通常可以等同於
Master-Workers
模式,比如Nginx
和Memcached
使用這種多執行緒模型,儘管專案之間的實現細節略有不同,但總體模式基本一致。Redis多執行緒網路模型設計
Redis也實現了多執行緒,但不是標準的多反應器/主工作模式。讓我們先看一下Redis多執行緒網路模型的一般設計。

-
Redis伺服器啟動,開啟主執行緒事件迴圈,將 acceptTcpHandler
連線答覆處理器註冊到與使用者配置的監聽埠對應的檔案描述符,並等待新連線的到來。 -
客戶端與伺服器之間建立網路連線。 -
呼叫 acceptTcpHandler
,主執行緒使用AE的API將readQueryFromClient
命令讀取處理器繫結到與新連線對應的檔案描述符上,並初始化一個客戶端以繫結這個客戶端連線。 -
客戶端傳送一個請求命令,觸發一個讀就緒事件。但不是透過套接字讀取客戶端的請求命令,而是伺服器的主執行緒首先將客戶端放入LIFO佇列 clients_pending_read
中。 -
在事件迴圈中,主執行緒執行beforeSleep –> handleClientsWithPendingReadsUsingThreads
,使用輪詢的負載均衡策略將clients_pending_read佇列中的連線均勻地分配給I/O執行緒。I/O執行緒透過套接字讀取客戶端的請求命令,將其儲存在client->querybuf中並解析第一個命令,但不執行它,同時主執行緒忙於輪詢並等待所有I/O執行緒完成讀取任務。 -
當主執行緒和所有I/O執行緒都完成讀取時,主執行緒結束忙碌的輪詢,遍歷 clients_pending_read
佇列,執行所有已連線客戶端的請求命令,首先呼叫processCommandResetClient來執行已解析的第一個命令,然後呼叫processInputBuffer
來解析和執行客戶端連線的所有命令,使用processInlineBuffer
或processMultibulkBuffer
根據Redis協議解析命令,最後呼叫processCommand
來執行命令。 -
根據所請求命令的型別( SET、GET、DEL、EXEC
等),分配相應的命令執行器來執行它,最後呼叫addReply系列函式中的一系列函式將響應資料寫入相應的客戶端寫出緩衝區:client->buf
或client->reply
,client->buf
是首選的寫出緩衝區,大小固定為16KB,通常足夠緩衝足夠的響應資料,但如果客戶端需要在時間視窗內響應大量資料,則會自動切換到client->reply連結串列,理論上可以容納無限量的資料(受到機器物理記憶體的限制),最後將客戶端新增到LIFO佇列clients_pending_write
中。 -
在事件迴圈中,主執行緒執行 beforeSleep –> handleClientsWithPendingWritesUsingThreads
,使用輪詢的負載均衡策略將clients_pending_write
佇列中的連線均勻地分配給I/O執行緒和主執行緒本身。I/O執行緒透過呼叫writeToClient
將客戶端寫緩衝區中的資料寫回客戶端,而主執行緒則忙於輪詢,等待所有I/O執行緒完成寫入任務。 -
當主執行緒和所有I/O執行緒都完成寫入時,主執行緒結束忙碌的輪詢,遍歷 clients_pending_write
佇列。如果客戶端寫緩衝區中還有資料,它將註冊sendReplyToClient
以等待連線的寫準備就緒事件,並等待客戶端寫入,然後繼續在事件迴圈中寫回剩餘的響應資料。
大部分邏輯與之前的單執行緒模型相同,唯一的改變是將讀取客戶端請求和寫回響應資料的邏輯非同步化到I/O執行緒中。這裡需要特別注意的是,I/O執行緒只負責讀取和解析客戶端命令,實際的命令執行最終是在主執行緒上完成的。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/yudao-cloud 影片教程:https://doc.iocoder.cn/video/
總結
當面試官再問Redis為啥這麼快時別傻傻再回答Redis是單執行緒了,否則只能回去等通知了。
Redis的多執行緒網路模型透過將讀取和寫回資料的任務非同步化,以及更好地利用多核CPU,從而提高了Redis在處理大量線上流量時的效能表現。
1.「多執行緒設計」 :
-
Redis多執行緒模型包括一個主執行緒(Main Reactor)和多個I/O執行緒(Sub Reactors)。 -
主執行緒負責接受新的連線,並將其分發到I/O執行緒進行獨立處理。 -
I/O執行緒負責讀取客戶端的請求命令,但不執行它們。 -
主執行緒負責執行客戶端的請求命令,包括解析和執行。 -
響應資料由I/O執行緒寫回客戶端。
2.「非同步讀寫」 :Redis的多執行緒模型非同步化了讀取客戶端請求和寫回響應資料的過程。客戶端請求首先被放入待讀取佇列,然後由I/O執行緒讀取。執行命令仍然在主執行緒上進行,但這種非同步化提高了系統的併發性和吞吐量。
歡迎加入我的知識星球,全面提升技術能力。

星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。





文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)