Redis為什麼這麼快?

ITPUB社群發表於2023-03-03

導讀

 Redis是一個開源的記憶體中的資料結構儲存系統,在實際的開發過程中,Redis已經成為不可或缺的元件之一,基於記憶體實現、合理的資料結構、合理的資料編碼、合理的執行緒模型等特徵不僅僅讓Redis變得如此之快,同時也造就了Redis對更多或者複雜的場景的支援。




01 
Redis的發家史


在今年的敏捷團隊建設中,我透過Suite執行器實現了一鍵自動化單元測試。Juint除了Suite執行器還有哪些執行器呢?由此我的Runner探索之旅開始了!
  • 2009年由 Salvatore Sanfilippo(Redis之父)釋出初始版本。

  • 2013年5月之前,由VMare贊助。

  • 2013年5月-2015年6月,由Pivotal贊助。

  • 2015年6月起,由Redis Labs贊助。


Redis為什麼這麼快?

     圖1 Redis之父
     根據db-engines.com上的排名,到目前為止Redis依然是最流行的鍵值對儲存系統。

Redis為什麼這麼快?

圖2 Redis在db-engineer.com上的排名


Redis為什麼這麼快?


圖3 Redis每年的受歡迎程度



02 
  Redis主要版本  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

  • 2009年5月釋出Redis初始版本;

  • 2012年釋出Redis 2.6.0;

  • 2013年11月釋出Redis 2.8.0;

  • 2015年4月釋出Redis 3.0.0,在該版本Redis引入叢集;

  • 2017年7月釋出Redis 4.0.0,在該版本Redis引入了模組系統;

  • 2018年10月釋出Redis 5.0.0,在該版本引入了Streams結構;

  • 2020年5月2日釋出 6.0.1(穩定版),在該版本中引入多執行緒、RESP3協議、無盤複製副本;

  • 2022年1月31日釋出 7.0 RC1,在該版本中主要是對效能和記憶體進行最佳化,新的AOF模式。




03 
  Redis 有多快?   


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。
      Redis中帶有一個可以進行效能測試的工具redis-benchmark,透過這個命令就可以模擬多個客戶端同時發起請求的場景,並且可以檢測Redis處理這些請求所需要的時間。

       根據官方的文件,Redis 已經在超過 60000 個連線上進行了基準測試,並且在這些條件下仍然能夠維持 50000 q/s。同樣的請求量如果打到MySQL上,那很可能直接崩掉。

  •      With high-end configurations, the number of client connections is also an important factor. Being based on epoll/kqueue, the Redis event loop is quite scalable. Redis has already  been benchmarked at more than 60000 connections, and was still able to sustain 50000 q/s in these conditions. As a rule of thumb, an instance with 30000 connections can only process half the throughput achievable with 100 connections. Here is an example showing the throughput of a Redis  instance per number of connections;


Redis為什麼這麼快?

圖4 Redis不通連結數情況下的QPS


04 
  Redis為什麼可以這麼快?  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

      那是什麼原因造就了Redis可以具有如此高的效能?主要分為以下幾個方面:

Redis為什麼這麼快?

圖5 Redis為什麼這麼快-思維導圖

4.1  基於記憶體實現


    

      Mysql的資料儲存持久化是儲存到磁碟上的,讀取資料是記憶體中如果沒有的話,就會產生磁碟I/O,先把資料讀取到記憶體中,再讀取資料。而Redis則是直接把資料儲存到記憶體中,減少了磁碟I/O造成的消耗。

Redis為什麼這麼快?

圖6 Redis與Mysql儲存方式區別

4.2  高效的資料結構


    

       合理的資料結構,就是可以讓應用/程式更快。Mysql索引為了提高效率,選擇了B+樹的資料結構。先看下Redis的資料結構&內部編碼圖:

Redis為什麼這麼快?

圖7 Redis底層資料結構

4.2.1 SDS簡單動態字串

Redis沒有采用原生C語言的字串型別而是自己實現了字串結構-簡單動態字串(simple dynamic string)。

Redis為什麼這麼快?

圖8 C語言字串型別

Redis為什麼這麼快?

圖9 SDS字串型別

SDS與C語言字串的區別:

  • 獲取字串長度:C字串的複雜度為O(N),而SDS的複雜度為O(1)。

  • 杜絕緩衝區溢位(C語言每次需要手動擴容),如果C字串想要擴容,在沒有申請足夠多的記憶體空間下,會出現記憶體溢位的情況,而SDS記錄了字串的長度,如果長度不夠的情況下會進行擴容。

  • 減少修改字串時帶來的記憶體重分配次數。

    • 空間預分配,

      • 規則1:修改後長度< 1MB,預分配同樣大小未使用空間,free=len;

      • 規則2:修改後長度 >= 1MB,預分配1MB未使用空間。

    • 惰性空間釋放,SDS 縮短時,不是回收多餘的記憶體空間,而是free記錄下多餘的空間,後續有變更,直接使用free中記錄的空間,減少分配。



4.2.2 embstr & raw

      Redis 的字串有兩種儲存方式,在長度特別短時,使用 emb 形式儲存(embeded),當長度超過 44 時,使用 raw 形式儲存。

Redis為什麼這麼快?

圖10 embstr和raw資料結構

為什麼分界線是 44 呢?

      在CPU和主記憶體之間還有一個高速資料緩衝區,有L1,L2,L3三級快取,L1級快取時距離CPU最近的,CPU會有限從L1快取中獲取資料,其次是L2,L3。

Redis為什麼這麼快?

圖11 CPU三級快取

      L1最快但是其儲存空間也是有限的,大概64位元組,拋去物件固定屬性佔用的空間,以及‘\0’,剩餘的空間最多是44個位元組,超過44位元組L1快取就會存不下。

Redis為什麼這麼快?

圖12 SDS在L1快取中的儲存方式

4.2.3 字典(DICT)

      Redis 作為 K-V 型記憶體資料庫,所有的鍵值就是用字典來儲存。字典就是雜湊表,比如HashMap,透過key就可以直接獲取到對應的value。而雜湊表的特性,在O(1)時間複雜度就可以獲得對應的值。









【Objective-c】//字典結構資料typedef struct dict {    dictType *type;  //介面實現,為字典提供多型性    void *privdata;  //儲存一些額外的資料    dictht ht[2];   //兩個hash表    long rehashidx. //漸進式rehash時記錄當前rehash的位置} dict;

      兩個hashtable通常情況下只有一個hashtable是有值的,另外一個是在進行rehash的時候才會用到,在擴容時逐漸的從一個hashtable中遷移至另外一個hashtable中,搬遷結束後舊的hashtable會被清空。

Redis為什麼這麼快?

圖13 Redis hashtable




















【Objective-c】//hashtable的結構如下:typedef struct dictht {   dictEntry **table;  //指向第一個陣列   unsigned long size;  //陣列的長度   unsigned long sizemask; //用於快速hash定位   unsigned long used; //陣列中元素的個數} dictht;
typedef struct dictEntry {   void *key;   union {       void *val;       uint64_t u64;       int64_t s64;       double d;   //用於zset,儲存score值       } v;       struct dictEntry *next;} dictEntry;

Redis為什麼這麼快?

圖14 Redis hashtable

4.2.4 壓縮列表(ziplist)

      redis為了節省記憶體空間,zset和hash物件在資料比較少的時候採用的是ziplist的結構,是一塊連續的記憶體空間,並且支援雙向遍歷。

Redis為什麼這麼快?

圖15 ziplist資料結構

4.2.5 跳躍表

  • 跳躍表是Redis特有的資料結構,就是在連結串列的基礎上,增加多級索引提升查詢效率。


  • 跳躍表支援平均 O(logN),最壞 O(N)複雜度的節點查詢,還可以透過順序性操作批次處理節點。


Redis為什麼這麼快?

圖16 跳躍表資料結構

4.3 合理的資料編碼


    
Redis 支援多種資料型別,每種基本型別,可能對多種資料結構。什麼時候使用什麼樣的資料結構,使用什麼樣的編碼,是redis設計者總結最佳化的結果。

  • String:如果儲存數字的話,是用int型別的編碼;如果儲存非數字,小於等於39位元組的字串,是embstr;大於39個位元組,則是raw編碼。
  • List:如果列表的元素個數小於512個,列表每個元素的值都小於64位元組(預設),使用ziplist編碼,否則使用linkedlist編碼。
  • Hash:雜湊型別元素個數小於512個,所有值小於64位元組的話,使用ziplist編碼,否則使用hashtable編碼。
  • Set:如果集合中的元素都是整數且元素個數小於512個,使用intset編碼,否則使用hashtable編碼。
  • Zset:當有序集合的元素個數小於128個,每個元素的值小於64位元組時,使用ziplist編碼,否則使用skiplist(跳躍表)編碼


      4.4 合理的執行緒模型


         

             首先是單執行緒模型-避免了上下文切換造成的時間浪費,單執行緒指的是網路請求模組使用了一個執行緒,即一個執行緒處理所有網路請求,其他模組仍然會使用多執行緒;在使用多執行緒的過程中,如果沒有一個良好的設計,很有可能造成線上程數增加的前期吞吐率增加,後期吞吐率反而增長沒有那麼明顯了。

            多執行緒的情況下通常會出現共享一部分資源,當多個執行緒同時修改這一部分共享資源時就需要有額外的機制來進行保障,就會造成額外的開銷。

      Redis為什麼這麼快?

      圖17 執行緒數與吞吐率關係
      另外一點則是I/O多路複用模型,在不瞭解原理的情況下,我們類比一個例項:在課堂上讓全班30個人同時做作業,做完後老師檢查,30個學生的作業都檢查完成才能下課。如何在有限的資源下,以最快的速度下課呢?

      • 第一種:安排一個老師,按順序逐個檢查。先檢查A,然後是B,之後是C、D...這中間如果有一個學生卡住,全班都會被耽誤。這種模式就好比用迴圈挨個處理socket,根本不具有併發能力。這種方式只需要一個老師,但是耗時時間會比較長。
      • 第二種:安排30個老師,每個老師檢查一個學生的作業。這種類似於為每一個socket建立一個程式或者執行緒處理連線。這種方式需要30個老師(最消耗資源),但是速度最快。
      • 第三種:安排一個老師,站在講臺上,誰解答完誰舉手。這時C、D舉手,表示他們作業做完了,老師下去依次檢查C、D的答案,然後繼續回到講臺上等。此時E、A又舉手,然後去處理E和A。這種方式可以在最小的資源消耗的情況下,最快的處理完任務。

      Redis為什麼這麼快?

      圖18 I/O多路複用

            多路I/O複用技術可以讓單個執行緒高效的處理多個連線請求,而Redis使用epoll作為I/O多路複用技術的實現。並且,Redis自身的事件處理模型將epoll中的連線、讀寫、關閉都轉換為事件,不在網路I/O上浪費過多的時間。


      05 
        使用場景  


      理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

      Redis為什麼這麼快?

      圖18 Redis使用場景


      06 
        總結  


      理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。
            基於以上的內容,我們可以瞭解到Redis為什麼可以這麼快的原因:

      - 純記憶體操作,記憶體的訪問是非常迅速的;

      - 多路複用的I/O模型,可以高併發的處理更多的請求;

      - 精心設計的高效的資料結構;

      - 合理的內部資料編碼,對記憶體空間的高效實用。

            總之,Redis為了高效能,從各個方面都做了很多最佳化,在使用Redis的過程中,掌握了其執行原理,才能在使用的過程中注意到那些操作會影響到Redis的效能。

      來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2938051/,如需轉載,請註明出處,否則將追究法律責任。

      相關文章