文章目錄
- 面試題
- Redis為什么選擇單線程
- 為什么逐漸加入多線程特性
- Redis6、Redis7的多線程特性和IO多路復用入門
- Redis7多線程
面試題
- Redis到底是單線程還是多線程?
- IO多路復用聽說過嗎?
- Redis為什么這么快?
Redis為什么選擇單線程
- 其實Redis單線程這個問法并不嚴謹,為什么這么說呢?
- Redis的版本很多3.x、4.x、6.x,版本不同架構也是不同的,不限定版本問是否單線程也不太嚴謹。
- 版本3.x ,最早版本,也就是大家口口相傳的redis是單線程。
- 版本4.x,嚴格意義來說也不是單線程,而是負責處理客戶端請求的線程是單線程,但是開始加了點多線程的東西(異步刪除)。—貌似
- 2020年5月版本的6.0.x后及2022年出的7.0版本后,告別了大家印象中的單線程,用一種全新的多線程來解決問題。—實錘
- 有幾個里程碑式的重要版本:5.0版本是直接升級到6.0版本,對于這個激進的升級,Redis之父antirez表現得很有信心和興奮,所以第一時間發文來闡述6.0的一些重大功能 “Redis 6.0.0 GA is out!”。當然,Redis7.0后版本更加厲害。
- 為什么Redis會選擇單線程?通常說,Redis是單線程究竟何意?
- Redis是單線程主要是指Redis的網絡IO和鍵值對讀寫是由一個線程來完成的,Redis在處理客戶端的請求時包括獲取(socket 讀)、解析、執行、內容返回(socket 寫) 等都由一個順序串行的主線程處理,這就是所謂的“單線程”。這也是Redis對外提供鍵值存儲服務的主要流程。
- 但Redis的其他功能,比如持久化RDB、AOF、異步刪除、集群數據同步等等,其實是由額外的線程執行的。Redis 命令工作線程是單線程的,但是,整個Redis來說,是多線程的。
- Redis最核心的功能是對外提供鍵值存儲服務,也就是客戶端發請求,Redis執行請求,返回結果。
- 這個過程中的主要步驟是:
- 從 socket 讀取請求
- 解析協議
- 執行命令(比如 GET、SET、ZADD 等操作 Redis 內存中的數據結構)
- 把結果通過 socket 寫回客戶端
- 上述整個流程是由 一個線程(主線程)串行完成的,不會有多個線程同時操作內存中的數據結構,因此 Redis 命令執行過程天然是“原子性”的,不需要加鎖。
- 所以說 Redis 是單線程,特指這一條主流程(命令執行流程)是單線程。
- 請說說演進變化情況?
- Q:Redis3.X單線程時代但是性能依舊很快的主要原因?
- A:回答如下:
- 基于內存操作: Redis 的所有數據都存在內存中,因此所有的運算都是內存級別的,所以他的性能比較高;
- 數據結構簡單:Redis 的數據結構是專門設計的,而這些簡單的數據結構的查找和操作的時間大部分復雜度都是 0(1),因此性能比較高;
- 多路復用和非阻塞 I/O: Redis使用 I/O多路復用功能來監聽多個 socket連接客戶端,這樣就可以使用一個線程連接來處理多個請求,減少線程切換帶來的開銷,同時也避免了 I/O 阻塞操作;
- 避免上下文切換:因為是單線程模型,因此就避免了不必要的上下文切換和多線程競爭,這就省去了多線程切換帶來的時間和性能上的消耗,而且單線程不會導致死鎖問題的發生。
- 作者原話使用單線程原因,官網證據:https://redis.io/docs/getting-started/faq/
- 舊版本Redis官網說明,說Redis就是單線程:
- Redis是單線程的。
- 如何利用多個CPU /內核? CPU并不是您使用Redis的瓶頸,因為通常Redis要么受內存限制,要么受網絡限制。例如,使用在平均Linux系統上運行的流水線Redis每秒可以發送一百萬個請求,因此,如果您的應用程序主要使用O(N)或O(log(N) )命令,則幾乎不會使用過多的CPU。但是,為了最大程度地利用CPU,您可以在同一框中啟動多個Redis實例,并將它們視為不同的服務器。在某個時候,單個盒子可能還不夠,因此,如果您要使用多個CPU,則可以開始考慮更早地進行分片的某種方法。您可以在“分區”頁面中找到有關使用多個Redis實例的更多信息。但是,在Redis 4.0中,我們開始使Redis具有更多線程,目前,這僅限于在后臺刪除對象,以及阻正通過Redis模塊實現的命令。對于將來的版本,計劃是使Redis越來越線程化。
- 大體意思是說: Redis 是基于內存操作的,因此他的瓶頸可能是機器的內存或者網絡帶寬而并非 CPU,既然 CPU 不是瓶頸,那么自然就采用單線程的解決方案了,況且使用多線程比較麻煩。但是在 Redis 4.0 中開始支持多線程了,例如后臺刪除、備份等功能。
- 新版本Redis官網原話,去掉了單線程的:
- Redis4.0之前一直采用單線程的主要原因有以下三個:
- 使用單線程模型是 Redis 的開發和維護更簡單,因為單線程模型方便開發和調試;
- 即使使用單線程模型也并發的處理多客戶端的請求,主要使用的是IO多路復用和非阻塞IO;
- 對于Redis系統來說,主要的性能瓶頸是內存或者網絡帶寬而并非 CPU。
為什么逐漸加入多線程特性
- 單線程也有苦惱,舉個例子:
- 正常情況下使用 del 指令可以很快的刪除數據,而當被刪除的 key 是一個非常大的對象時,例如key包含了成千上萬個元素的 hash 集合時,那么 del 指令就會造成 Redis 主線程卡頓。
- 這就是redis3.x單線程時代最經典的故障,big key刪除的頭疼問題,由于由于redis是單線程的,del bigKey …等待很久這個線程才會釋放,類似加了一個synchronized鎖,你可以想象高并發下,程序堵成什么樣子?
- 如何解決?
- 使用惰性刪除可以有效的解決性能問題。
- 案例:比如當 Redis 需要刪除一個很大的數據時,因為是單線程原子命令操作,這就會導致 Redis 服務卡頓,于是在 Redis 4.0 中就新增了多線程的模塊,當然此版本中的多線程主要是為了解決刪除數據效率比較低的問題的。
unlink key
flushdb async
flushall async
- 上面三條指令都是 Redis 里用于刪除鍵或清空數據庫的命令,它們有一個共同特點:把刪除工作交給了后臺的子線程異步刪除數據,都可以異步刪除,不會阻塞主線程,大大減少對 Redis 性能的影響。
- 因為Redis是單個主線程處理,redis之父antirez一直強調 “Lazy Redis is better Redis”。
- 而 lazy free 的本質就是把某些cost(主要時間復雜度,占用主線程cpu時間片)較高刪除操作,從redis主線程剝離讓bio子線程來處理,極大地減少主線阻塞時間。從而減少刪除導致性能和穩定性問題。
- 什么是 Lazy Free?Lazy Free(延遲釋放/惰性釋放)是 Redis 4.0 引入的一個刪除策略優化。它不是改變“邏輯刪除”過程,主線程依然快速 取消引用,而是把真正釋放內存的過程放到后臺線程(bio 子線程)中完成。
- bio 是什么?bio = Background I/O。它是 Redis 內部實現的一個后臺 I/O 線程機制,專門用來做一些耗時、不可阻塞主線程的 I/O 操作,避免影響 Redis 主線程的實時性能。bio 不是操作系統的 BIO/Buffered I/O 概念,Redis 里的 bio 是一個模塊,管理一組后臺 worker 線程。
- 在Redis4.0就引入了多個線程來實現數據的異步惰性刪除等功能但是其處理讀寫請求的仍然只有一個線程,所以仍然算是狹義上的單線程。
Redis6、Redis7的多線程特性和IO多路復用入門
- 前面我們已經提到,對于Redis主要的性能瓶頸是內存或者網絡帶寬而并非 CPU。
- 所以Redis的瓶頸可以初步定為:網絡IO。
- Redis6/7,真正的多線程登場,在Redis6/7中,非常受關注的第一個新特性就是多線程。
- 這是因為,Redis一直被大家熟知的就是它的單線程架構,雖然有些命令操作可以用后臺線程或子進程執行(比如數據刪除、快照生成、AOF重寫),但是從網絡IO處理到實際的讀寫命令處理,都是由單個線程完成的。
- 隨著網絡硬件的性能提升,Redis的性能瓶頸有時會出現在網絡IO的處理上,也就是說,單個主線程處理網絡請求的速度跟不上底層網絡硬件的速度。
- 為了應對這個問題:采用多個IO線程來處理網絡請求,提高網絡請求處理的并行度,Redis6/7就是采用的這種方法。
- 但是,Redis的多IO線程只是用來處理網絡請求的,對于讀寫操作命今Redis仍然使用單線程來處理。這是因為,Redis處理請求時,網絡處理經常是瓶頸,通過多個IO線程并行處理網絡操作,可以提升實例的整體處理性能。而繼續使用單線程執行命今換作,就不用為了保證Lua腳本、事各的原子性,額外開發多線程互斥加鎖機制了(不管加鎖操作處理),這樣一來,Redis線程模型實現就簡單了。
- 主線程和IO線程怎么協作完成請求處理的——四個階段:
-
這張圖其實是一個典型的服務端 I/O 多線程處理流程圖,它分為主線程和IO線程兩部分,配合右側文字說明,其實描述的是一種請求處理模型,通常用在高性能服務器里。
-
當有客戶端請求到達時,主線程只是負責接收請求,然后把 Socket 交給 IO 線程來處理實際的讀寫,主線程避免被 IO 阻塞。
-
主線程做輕量工作 → IO 線程做重活(讀數據、業務處理)→ 主線程繼續執行其他請求。
-
主線程部分(左側):
- 接收連接請求:主線程監聽端口,有客戶端連接就 accept,拿到一個 Socket。
- 將 Socket 放入全局等待隊列:把新建立的 Socket 放到一個全局隊列里(防止處理不過來導致丟包或阻塞主線程)。
- 以輪詢方式將 Socket 連接分配給 IO 線程:主線程把隊列里的 Socket 按輪詢或者負載均衡分配給 IO 線程。
- 等待 IO 線程完成讀取和數據解析:分配完之后,主線程等待 IO 線程完成對這個 Socket 的數據讀取 + 解析。
- 執行請求命令操作:IO 線程解析出具體的請求(比如用戶要查數據庫),交給主線程來做業務處理。
- 請求命令操作執行完成:主線程完成業務處理,響應發回客戶端。
-
IO 線程部分(右側):
- 開始執行:主線程分配 Socket 后,IO 線程接管。
- 綁定線程:IO 線程“綁定”到這個 Socket,負責讀數據。
- 讀取 Socket 數據 + 解析:IO 線程讀取 Socket 中的數據,解析出請求內容。
- 并行/異步執行請求:根據需要,可能直接執行請求,也可能轉交回主線程執行。
- 請求完成:IO 線程標記請求處理完成,準備處理下一個。
-
階段一:主線程主要負責建立連接,拿到 Socket,放到等待隊列,交由 IO 線程處理。
-
階段二:IO 線程處理 Socket 數據讀取,避免主線程阻塞。
-
階段三:IO 線程完成解析后,主線程或者 IO 線程執行請求(通常是主線程做復雜業務邏輯)。
-
階段四:IO 線程完成請求處理后 Socket 解綁,準備下一個請求。
客戶端請求進來↓
主線程 accept → 得到 Socket↓
放入等待隊列 or 直接分配給 IO 線程↓
IO 線程綁定 Socket,開始 read↓
讀數據 → 解析請求 → 處理請求(可能交主線程/業務線程池處理)↓
寫響應 → 關閉/復用 Socket
-
主線程 接收連接 + 分發任務,不阻塞
-
IO 線程 處理 IO + 解析協議 + 執行請求
-
主線程 只負責高性能接入,避免自己被任何一個慢請求卡死
-
這個模型中,主線程只負責接收新連接并分發 Socket 給 IO 線程,主線程自身不做耗時 IO 操作;IO 線程負責綁定 Socket,完成數據的讀寫、請求解析和響應發送,通過這種分工實現高并發、高性能處理,主線程始終保持“輕盈”,不會被單個慢請求拖慢整體性能。
-
主線程負責接入和分發,IO 線程負責讀寫和處理,解耦接入與IO,提升系統吞吐量。
-
主線程只負責連接接入和 socket 分發,IO 線程獨立負責 socket 的讀寫和請求處理,主線程永遠不阻塞等待 IO 線程結果,IO 線程直接寫回客戶端響應。
-
Unix網絡編程中的五種IO模型:
- Blocking IO - 阻塞IO
- NoneBlocking IO - 非阻塞IO
- IO multiplexing - IO 多路復用
- Linux世界一切皆是文件
- 文件描述符,簡稱FD,句柄。FileDescriptor:文件描述符 (File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統
- 首次淺談IO多路復用,IO多路復用是什么?一種同步的IO模型,實現一個線程監視多個文件句柄,一旦某個文件句柄就緒就能夠通知到對應應用程序進行相應的讀寫操作,沒有文件句柄就緒時就會阻塞應用程序從而釋放CPU資源
- 概念:
- l/O:網絡I/O,尤其在操作系統層面指數據在內核態和用戶態之間的讀寫操作
- 多路:多個客戶端連接(連接就是套接字描述符,即 socket 或者 channel)
- 復用:復用一個或幾個線程
- lO多路復用:也就是說一個或一組線程處理多個TCP連接,使用單進程就能夠實現同時處理多個客戶端的連接,無需創建或者維護過多的進程/線程
- 一句話:一個服務端進程可以同時處理多個套接字描述符
- 實現IO多路復用的模型有3種: 可以分 select -> poll -> epoll 三個階段來描述
- signal driven IO - 信號驅動IO
- asynchronous IO - 異步IO
-
場景體驗,說人話引出epoll
-
場景解析:模擬一個tcp服務器處理30個客戶socket。 假設你是一個監考老師,讓30個學生解答一道競賽考題,然后負責驗收學生答卷,你有下面幾個選擇:
- 第一種選擇(輪詢):按順序逐個驗收,先驗收A,然后是B,之后是C、D…這中間如果有一個學生卡住,全班都會被耽誤,你用循環挨個處理socket,根本不具有并發能力。
- 第二種選擇(來一個new一個,1對1服):你創律30個分身線程,每個分身線程檢查一個學生的答案是否正確。這種類似于為每一個用戶創建一個進程或者線程處理連接。
- 第三種選擇(響應式處理,1對多服務):你站在講臺上等,誰解答完誰舉手。這時C、D舉手,表示他們解答問題完畢,你下去依次檢查C、D的答案然后繼續回到講臺上等。此時E、A又舉手,然后去處理E和A…這種就是IO復用模型。 Linux下的select、poll和epoll就是干這個的。
-
IO多路復用模型,簡單明了版理解:
-
將用戶socket對應的文件描述符(FileDescriptor)注冊進epoll,然后epoll幫你監聽哪些socket上有消息到達,這樣就避免了大量的無用操作。此時的socket應該采用非阻塞模式。這樣,整個過程只在調用select、poll、epoll這些調用的時候才會阻塞,收發客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,這就是事件驅動,所謂的reactor反應模式。
- 在單個線程通過記錄跟蹤每一個Sockek(I/0流)的狀態來同時管理多個I/0流,一個服務端進程可以同時處理多個套接字描述符。 目的是盡量多的提高服務器的吞吐能力。 大家都用過nginx,nginx使用epoll接收請求,ngnix會有很多鏈接進來,epoll會把他們都監視起來,然后像撥開關一樣,誰有數據就撥向誰然后調用相應的代碼處理。redis類似同理,這就是IO多路復用原理,有請求就響應,沒請求不打擾。
- 小總結:只使用一個服務端進程可以同時處理多個套接字描述符連接
- 面試題:redis為什么這么快?
- IO多路復用+epoll函數使用,才是redis為什么這么快的直接原因,而不是僅僅單線程命令+redis安裝在內存中。
- 簡單說明:
- Redis工作線程是單線程的,但是,整個Redis來說,是多線程的;
- 主線程和IO線程怎么協作完成請求處理的-精簡版
- I/O 的讀和寫本身是堵塞的,比如當 socket 中有數據時,Redis 會通過調用先將數據從內核態空間拷貝到用戶態空間,再交給 Redis 調用,而這個拷貝的過程就是阻塞的,當數據量越大時拷貝所需要的時間就越多,而這些操作都是基于單線程完成的。
- 從Redis6開始,就新增了多線程的功能來提高 I/O 的讀寫性能,他的主要實現思路是將主線程的 I/O 讀寫任務拆分給一組獨立的線程去執行,這樣就可以使多個 socket 的讀寫可以并行化了,采用多路 I/0 復用技術可以讓單個線程高效的處理多個連接請求 (盡量減少網絡IO的時間消耗》,將最耗時的Socket的讀取、請求解析、寫入單獨外包出去,剩下的命令執行仍然由主線程串行執行并和內存的數據交互。
- 結合圖可知,網絡IO操作就變成多線程化了,其他核心部分仍然是線程安全的,是個不錯的折中辦法。
-
結論:Redis6 -> 7將網絡數據讀寫、請求協議解析通過多個IO線程的來處理,對于真正的命令執行來說,仍然使用主線程操作,一舉兩得
-
Redis 的基本 IO 處理模型:
- Redis 設計目標是簡單、可預期延遲,不是極端追求高并發吞吐。
- 主線程 完成所有命令處理,保證線程安全。
- IO 線程只輔助做socket 的讀/寫,不會改內部數據結構(否則要加鎖)。
-
Redis 的標準工作流程(Redis 6.0 之后支持 IO 多線程,之前完全單線程):
- 沒有啟用 IO 多線程:完全單線程 → 非常簡單、性能也很好(因為 Redis 是內存數據庫,命令快,單線程足夠)。
- 啟用 IO 多線程(從 Redis 6.0 開始,可以配置 io-threads):Redis 啟用 IO 多線程后,分成兩部分——1、IO 線程:負責 read/write socket,但是不解析命令、不執行命令。2、主線程:執行所有命令邏輯,改 Redis 內部狀態。
# 沒有啟用 IO 多線程
主線程:accept socketread socket → 解析協議 → 執行命令 → write socket# 啟用 IO 多線程
主線程:accept socketdispatch read 任務給 IO 線程IO 線程:read socket 數據 → 把讀到的請求 buffer 交回主線程主線程:解析協議 → 執行命令 → 生成響應主線程:dispatch write 任務給 IO 線程IO 線程:write socket 返回響應
- 解析協議 + 執行命令,Redis 仍然是主線程干!IO 線程只是做純 IO 操作(read socket → 填 buffer,write socket → 發送響應)。
- 為什么這樣設計?保證 Redis 內部數據結構始終只被主線程訪問,完全避免加鎖。只用多線程加速網絡 IO 部分,不影響主線程的數據一致性。
- 主線程負責解析命令 + 執行命令 + 生成響應,I/O 線程(如果啟用)只負責讀 socket 到 buffer、把 buffer 里的結果寫回 socket,不參與解析和執行邏輯。
Redis7多線程
- 如果你在實際應用中,發現Redis實例的CPU開銷不大但吞吐量卻沒有提升,可以考慮使用Redis7的多線程機制,加速網絡處理,進而提升實例的吞吐量
- Redis7將所有數據放在內存中,內存的響應時長大約為100納秒,對于小數據包,Redis服務器可以處理8W到10W的QPS。
- 這也是Redis處理的極限了,對于80%的公司來說,單線程的Redis已經足夠使用了。
- 設置 io-thread-do-reads 配置項為yes,表示啟動多線程。
- 設置線程個數。關于線程數的設置,官方的建議是如果為4核的CPU,建議線程數設置為2或3,如果為8核CPU建議線程數設置為6,安程數一定要小于機器核數,線程數并不是越大越好。
- 總結:
- Redis自身出道就是優秀,基于內存操作、數據結構簡單、多路復用和非阻塞I/O、避免了不必要的線程上下文切換等特性,在單線程的環境下依然很快;
- 但對于大數據的 key刪除還是卡頓厲害,因此在Redis 4.0引入了多線程 unlink key / flushall async 等命令,主要用于Redis 數據的異步刪除;
- 而在Redis6/7中引入了I/O多線程的讀寫,這樣就可以更加高效的處理更多的任務了,Redis只是將I/O讀寫變成了多線程,而命令的執行依舊是由主線程串行執行的,因此在多線程下操作 Redis不會出現線程安全的問題。
- Redis無論是當初的單線程設計,還是如今與當初設計相背的多線程,目的只有一個:讓 Redis變得越來越快。