爲何 Redis 單線程能達到百萬+QPS?

redis.jpg
做者:在江湖中coding
https://juejin.im/post/5e6097...react

性能測試報告redis

查看了下阿里 Redis 的性能測試報告以下,可以達到數十萬、百萬級別的 QPS(暫時忽略阿里對 Redis 所作的優化),咱們從 Redis 的設計和實現來分析一下 Redis 是怎麼作的。後端

Redis的設計與實現服務器

其實 Redis 主要是經過三個方面來知足這樣高效吞吐量的性能需求微信

  • 高效的數據結構
  • 多路複用 IO 模型
  • 事件機制

一、高效的數據結構

Redis 支持的幾種高效的數據結構 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)數據結構

以上幾種對外暴露的數據結構它們的底層編碼方式都是作了不一樣的優化的,不細說了,不是本文重點。架構

二、多路複用 IO 模型

假設某一時刻與 Redis 服務器創建了 1 萬個長鏈接,對於阻塞式 IO 的作法就是,對每一條鏈接都創建一個線程來處理,那麼就須要 1萬個線程,同時根據咱們的經驗對於 IO 密集型的操做咱們通常設置,線程數 = 2 * CPU 數量 + 1,對於 CPU 密集型的操做通常設置線程 = CPU 數量 + 1。框架

固然各類書籍或者網上也有一個詳細的計算公式能夠算出更加合適準確的線程數量,可是獲得的結果每每是一個比較小的值,像阻塞式 IO 這也動則建立成千上萬的線程,系統是沒法承載這樣的負荷的更加彈不上高效的吞吐量和服務了。異步

而多路複用 IO 模型的作法是,用一個線程將這一萬個創建成功的連接陸續的放入 event_poll,event_poll 會爲這一萬個長鏈接註冊回調函數,當某一個長鏈接準備就緒後(創建創建成功、數據讀取完成等),就會經過回調函數寫入到 event_poll 的就緒隊列 rdlist 中,這樣這個單線程就能夠經過讀取 rdlist 獲取到須要的數據。socket

須要注意的是,除了異步 IO 外,其它的 I/O 模型其實均可以歸類爲阻塞式 I/O 模型,不一樣的是像阻塞式 I/O 模型在第一階段讀取數據的時候,若是此時數據未準備就緒須要阻塞,在第二階段數據準備就緒後須要將數據從內核態複製到用戶態這一步也是阻塞的。而多路複用 IO 模型在第一階段是不阻塞的,只會在第二階段阻塞。

經過這種方式,就能夠用 1 個或者幾個線程來處理大量的鏈接了,極大的提高了吐吞量

三、事件機制

Redis 客戶端與 Redis 服務端創建鏈接,發送命令,Redis 服務器響應命令都是須要經過事件機制來作的,以下圖

  • 首先 redis 服務器運行,監聽套接字的 AE_READABLE 事件處於監聽的狀態下,此時鏈接應答處理器工做
  • 客戶端與 Redis 服務器發起創建鏈接,監聽套接字產生 AE_READABLE 事件,當 IO 多路複用程序監聽到其準備就緒後,將該事件壓入隊列中,由文件事件分派器獲取隊列中的事件交於鏈接應答處理器工做處理,應答客戶端創建鏈接成功,同時將客戶端 socket 的 AE_READABLE 事件壓入隊列由文件事件分派器獲取隊列中的事件交命令請求處理器關聯
  • 客戶端發送 set key value 請求,客戶端 socket 的 AE_READABLE 事件,當 IO 多路複用程序監聽到其準備就緒後,將該事件壓入隊列中,由文件事件分派器獲取隊列中的事件交於命令請求處理器關聯處理
  • 命令請求處理器關聯處理完成後,須要響應客戶端操做完成,此時將產生 socket 的 AE_WRITEABLE 事件壓入隊列,由文件事件分派器獲取隊列中的事件交於命令恢復處理器處理,返回操做結果,完成後將解除 AE_WRITEABLE 事件與命令恢復處理器的關聯

reactor模式

大致上能夠說 Redis 的工做模式是,reactor 模式配合一個隊列,用一個 serverAccept 線程來處理創建請求的連接,而且經過 IO 多路複用模型,讓內核來監聽這些 socket,一旦某些 socket 的讀寫事件準備就緒後就對應的事件壓入隊列中,而後 worker 工做,由文件事件分派器從中獲取事件交於對應的處理器去執行,當某個事件執行完成後文件事件分派器纔會從隊列中獲取下一個事件進行處理。

能夠類比在 netty 中,咱們通常會設置 bossGroup 和 workerGroup 默認狀況下 bossGroup 爲 1,workerGroup = 2 * cpu 數量,這樣能夠由多個線程來處理讀寫就緒的事件,可是其中不能有比較耗時的操做若是有的話須要將其放入線程池中,否則會下降其吐吞量。在 Redis 中咱們能夠看作這兩者的值都是 1。

爲何說存儲的值不宜過大

好比一個 string key = a,存儲了 500MB,首先讀取事件壓入隊列中,文件事件分派器從中獲取到後,交於命令請求處理器處理,此處就涉及到從磁盤中加載 500MB。

好比是普通的 SSD 硬盤,讀取速度 200MB/S,那麼須要 2.5S 的讀取時間,在內存中讀取數據比較快好比 DDR4 中 50G/秒,讀取 500MB 須要 100 毫秒左右。

線程的庫通常默認 10 毫秒就算慢查詢了,大部分的指令執行時間都是微秒級別,此時其它 socket 全部的請求都將處於等待過程當中,就會致使阻塞了 100 毫秒,同時又會佔用較大的帶寬致使吞吐量進一步降低。

看完本文有收穫?請轉發分享給更多人

關注「後端開發者社區」,提高Java技能

關注後端開發者社區微信公衆號,後臺回覆:碼農大禮包 能夠獲取最新整理的技術資料一份。涵蓋Java 框架學習、架構師學習等!

相關文章
相關標籤/搜索