天天看點

面試官:你确定 Redis 是單線程的程序嗎?

作者:小林coding

計算機八股文網站:https://xiaolincoding.com

大家好,我是小林。

這次主要分享 Redis 線程模型篇的面試題。

  • Redis 是單線程嗎?
  • Redis 單線程模式是怎樣的?
  • Redis 采用單線程為什麼還這麼快?
  • Redis 6.0 之前為什麼使用單線程?
  • Redis 6.0 之後為什麼引入了多線程?

Redis 是單線程嗎?

Redis 單線程指的是「接收用戶端請求->解析請求 ->進行資料讀寫等操作->發生資料給用戶端」這個過程是由一個線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。

但是,Redis 程式并不是單線程的,Redis 在啟動的時候,是會啟動背景線程(BIO)的:

  • Redis 在 2.6 版本,會啟動 2 個背景線程,分别處理關閉檔案、AOF 刷盤這兩個任務;
  • Redis 在 4.0 版本之後,新增了一個新的背景線程,用來異步釋放 Redis 記憶體,也就是 lazyfree 線程。例如執行 unlink key / flushdb async / flushall async 等指令,會把這些删除操作交給背景線程來執行,好處是不會導緻 Redis 主線程卡頓。是以,當我們要删除一個大 key 的時候,不要使用 del 指令删除,因為 del 是在主線程處理的,這樣會導緻 Redis 主線程卡頓,是以我們應該使用 unlink 指令來異步删除大key。

之是以 Redis 為「關閉檔案、AOF 刷盤、釋放記憶體」這些任務建立單獨的線程來處理,是因為這些任務的操作都是很耗時的,如果把這些任務都放在主線程來處理,那麼 Redis 主線程就很容易發生阻塞,這樣就無法處理後續的請求了。

背景線程相當于一個消費者,生産者把耗時任務丢到任務隊列中,消費者(BIO)不停輪詢這個隊列,拿出任務就去執行對應的方法即可。

面試官:你确定 Redis 是單線程的程式嗎?

關閉檔案、AOF 刷盤、釋放記憶體這三個任務都有各自的任務隊列:

  • BIO_CLOSE_FILE,關閉檔案任務隊列:當隊列有任務後,背景線程會調用 close(fd) ,将檔案關閉;
  • BIO_AOF_FSYNC,AOF刷盤任務隊列:當 AOF 日志配置成 everysec 選項後,主線程會把 AOF 寫日志操作封裝成一個任務,也放到隊列中。當發現隊列有任務後,背景線程會調用 fsync(fd),将 AOF 檔案刷盤,
  • BIO_LAZY_FREE,lazy free 任務隊列:當隊列有任務後,背景線程會 free(obj) 釋放對象 / free(dict) 删除資料庫所有對象 / free(skiplist) 釋放跳表對象;

Redis 單線程模式是怎樣的?

Redis 6.0 版本之前的單線模式如下圖:

面試官:你确定 Redis 是單線程的程式嗎?

圖中的藍色部分是一個事件循環,是由主線程負責的,可以看到網絡 I/O 和指令處理都是單線程。 Redis 初始化的時候,會做下面這幾年事情:

  • 首先,調用 epoll_create() 建立一個 epoll 對象和調用 socket() 一個服務端 socket
  • 然後,調用 bind() 綁定端口和調用 listen() 監聽該 socket;
  • 然後,将調用 epoll_crt() 将 listen socket 加入到 epoll,同時注冊「連接配接事件」處理函數。

初始化完後,主線程就進入到一個事件循環函數,主要會做以下事情:

  • 首先,先調用處理發送隊列函數,看是發送隊列裡是否有任務,如果有發送任務,則通過 write 函數将用戶端發送緩存區裡的資料發送出去,如果這一輪資料沒有發生完,就會注冊寫事件處理函數,等待 epoll_wait 發現可寫後再處理 。
  • 接着,調用 epoll_wait 函數等待事件的到來:
    • 如果是連接配接事件到來,則會調用連接配接事件處理函數,該函數會做這些事情:調用 accpet 擷取已連接配接的 socket -> 調用 epoll_ctr 将已連接配接的 socket 加入到 epoll -> 注冊「讀事件」處理函數;
    • 如果是讀事件到來,則會調用讀事件處理函數,該函數會做這些事情:調用 read 擷取用戶端發送的資料 -> 解析指令 -> 處理指令 -> 将用戶端對象添加到發送隊列 -> 将執行結果寫到發送緩存區等待發送;
    • 如果是寫事件到來,則會調用寫事件處理函數,該函數會做這些事情:通過 write 函數将用戶端發送緩存區裡的資料發送出去,如果這一輪資料沒有發生完,就會繼續注冊寫事件處理函數,等待 epoll_wait 發現可寫後再處理 。

以上就是 Redis 單線模式的工作方式,如果你想看源碼解析,可以參考這一篇:為什麼單線程的 Redis 如何做到每秒數萬 QPS ?

Redis 采用單線程為什麼還這麼快?

官方使用基準測試的結果是,單線程的 Redis 吞吐量可以達到 10W/每秒,如下圖所示:

面試官:你确定 Redis 是單線程的程式嗎?

之是以 Redis 采用單線程(網絡 I/O 和執行指令)那麼快,有如下幾個原因:

  • Redis 的大部分操作都在記憶體中完成,并且采用了高效的資料結構,是以 Redis 瓶頸可能是機器的記憶體或者網絡帶寬,而并非 CPU,既然 CPU 不是瓶頸,那麼自然就采用單線程的解決方案了;
  • Redis 采用單線程模型可以避免了多線程之間的競争,省去了多線程切換帶來的時間和性能上的開銷,而且也不會導緻死鎖問題。
  • Redis 采用了 I/O 多路複用機制處理大量的用戶端 Socket 請求,IO 多路複用機制是指一個線程處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 隻運作單線程的情況下,該機制允許核心中,同時存在多個監聽 Socket 和已連接配接 Socket。核心會一直監聽這些 Socket 上的連接配接請求或資料請求。一旦有請求到達,就會交給 Redis 線程處理,這就實作了一個 Redis 線程處理多個 IO 流的效果。

Redis 6.0 之前為什麼使用單線程?

我們都知道單線程的程式是無法利用伺服器的多核 CPU 的,那麼早期 Redis 版本的主要工作(網絡 I/O 和執行指令)為什麼還要使用單線程呢?我們不妨先看一下Redis官方給出的FAQ。

面試官:你确定 Redis 是單線程的程式嗎?

核心意思是:CPU 并不是制約 Redis 性能表現的瓶頸所在,更多情況下是受到記憶體大小和網絡I/O的限制,是以 Redis 核心網絡模型使用單線程并沒有什麼問題,如果你想要使用服務的多核CPU,可以在一台伺服器上啟動多個節點或者采用分片叢集的方式。

除了上面的官方回答,選擇單線程的原因也有下面的考慮。

使用了單線程後,可維護性高,多線程模型雖然在某些方面表現優異,但是它卻引入了程式執行順序的不确定性,帶來了并發讀寫的一系列問題,增加了系統複雜度、同時可能存線上程切換、甚至加鎖解鎖、死鎖造成的性能損耗。

Redis 6.0 之後為什麼引入了多線程?

雖然 Redis 的主要工作(網絡 I/O 和執行指令)一直是單線程模型,但是在 Redis 6.0 版本之後,也采用了多個 I/O 線程來處理網絡請求,這是因為随着網絡硬體的性能提升,Redis 的性能瓶頸有時會出現在網絡 I/O 的處理上。

是以為了提高網絡請求處理的并行度,Redis 6.0 對于網絡請求采用多線程來處理。但是對于讀寫指令,Redis 仍然使用單線程來處理,是以大家不要誤解 Redis 有多線程同時執行指令。

Redis 官方表示,Redis 6.0 版本引入的多線程 I/O 特性對性能提升至少是一倍以上。

Redis 6.0 版本支援的 I/O 多線程特性,預設是 I/O 多線程隻處理寫操作(write client socket),并不會以多線程的方式處理讀操作(read client socket)。要想開啟多線程處理用戶端讀請求,就需要把 Redis.conf 配置檔案中的 io-threads-do-reads 配置項設為 yes。

//讀請求也使用io多線程
io-threads-do-reads yes 
           

同時, Redis.conf 配置檔案中提供了 IO 多線程個數的配置項。

// io-threads N,表示啟用 N-1 個 I/O 多線程(主線程也算一個 I/O 線程)
io-threads 4 
           

關于線程數的設定,官方的建議是如果為 4 核的 CPU,建議線程數設定為 2 或 3,如果為 8 核 CPU 建議線程數設定為 6,線程數一定要小于機器核數,線程數并不是越大越好。 是以, Redis 6.0 版本之後,Redis 在啟動的時候,預設情況下會有 6 個線程:

  • Redis-server : Redis的主線程,主要負責執行指令;
  • bio_close_file、bio_aof_fsync、bio_lazy_free:三個背景線程,分别異步處理關閉檔案任務、AOF刷盤任務、釋放記憶體任務;
  • io_thd_1、io_thd_2、io_thd_3:三個 I/O 線程,io-threads 預設是 4 ,是以會啟動 3(4-1)個 I/O 多線程,用來分擔 Redis 網絡 I/O 的壓力。

系列《圖解Redis》文章:

面試篇:

  • 3 萬字 + 40 張圖 | 攻破 40 道 Redis 常見面試題

資料類型篇:

  • 2 萬字 + 30 張圖 | 細說 Redis 九種資料類型和應用場景
  • 2 萬字 + 40 張圖 | 圖解 Redis 九種資料結構的實作

持久化篇:

  • AOF 持久化是怎麼實作的?
  • RDB 快照是怎麼實作的?

功能篇:

  • Redis 過期删除政策和記憶體淘汰政策有什麼差別?

高可用篇:

  • 主從複制是怎麼實作的?
  • 為什麼要有哨兵?

緩存篇:

  • 什麼是緩存雪崩、擊穿、穿透?
  • 資料庫和緩存如何保證一緻性?

微信搜尋公衆号:「小林coding」 ,回複「圖解」即可免費獲得「圖解網絡、圖解系統、圖解MySQL、圖解Redis」PDF 電子書