Redis是如何作到訪問速度很快的

對於Redis這種內存數據庫來講,除了訪問的是內存以外,Redis訪問速度飛快還取決於其餘的一些因素,而這些都跟Redis的高可用性有很大關係。下面是衡量Redis的三個緯度:
1.高性能:線程模型、網絡I/O模型、數據結構,合理的數據編碼2.高可用性:主從複製、哨兵模式、Cluster分片集羣和持久化機制3.高拓展性:負載均衡
本篇文章,咱們主要來介紹Redis的高性能特性的幾個相關因素。 根據官方數據,Redis的QPS能夠達到約100000/秒,橫軸表示鏈接數,縱軸表示QPS。

參考資料:https://redis.io/topics/benchmarks

因素1: 線程模型 

Redis4.0以前是單線程模型,緣由是由於在此以前CPU不是瓶頸,網絡I/O纔是瓶頸,而單線程模型一來避免了多線程模型之間的上下文切換,二來又能夠經過多路複用來實現併發,而且代碼更容易維護。javascript

不過在Redis4.0以後,redis新增了一些能夠被其餘線程異步處理的刪除操做,例如:UNLINKFLUSHALL ASYNC 和 FLUSHDB ASYNC。緣由是有一些超大的鍵值對佔用了很大的內容,例如幾十 MB 或者幾百 MB 的數據,這些數據的刪除在幾百毫秒內結束不了,若是是同步的就會阻塞待處理的任務,因此加入了多線程,目的就是爲了異步處理這些大的數據。
css


參考資料爲:https://zhuanlan.zhihu.com/p/91490643https://stackoverflow.com/questions/10489298/redis-is-single-threaded-then-how-does-it-do-concurrent-i-ohttp://www.odbms.org/2018/03/the-little-known-feature-of-redis-4-0-that-will-speed-up-your-applications/

因素2:網絡I/O模型

由於Redis是單線程模型的緣由,實現併發操做的話,是須要採用了多路複用機制的,例如:epoll,關於epoll的介紹能夠參考個人另一篇文章4.同時管理多個socket的高效方法-epoll
html

參考資料:https://draveness.me/redis-io-multiplexing/https://www.jianshu.com/p/8f2fb61097b8https://baijiahao.baidu.com/s?id=1676709704453688282&wfr=spider&for=pc

因素3:數據結構

首先,Redis整個數據庫就是一個全局哈希表,而哈希表的時間複雜度是 O(1),只須要計算每一個鍵的哈希值,便知道對應的哈希桶位置,定位桶裏面的 entry 找到對應數據,這個也是 Redis 快的緣由之一。java


其次,不一樣的數據類型使用不一樣的數據結構速度才得以提高,而且每種數據類型都有一種或者多種數據結構來支撐。web



1) SDS 簡單動態字符
SDS與C中的字符串區別以下所示:

sds與c字符串相比,優點以下:
1.Redis 將獲取字符串長度所需的複雜度從 O(N) 下降到了 O(1)  , 這確保了獲取字符串長度的工做不會成爲 Redis 的性能瓶頸。
2. 與 C 字符串不一樣, SDS 的空間分配策略徹底杜絕了發生緩衝區溢出的可能性。
3.空間預分配: SDS 被修改後,程序不只會爲 SDS 分配所須要的必須空間,還會分配額外的未使用空間。分配規則以下:若是對 SDS 修改後,len 的長度小於 1M,那麼程序將分配和 len 相同長度的未使用空間。舉個例子,若是 len=10,從新分配後,buf 的實際長度會變爲 10(已使用空間)+10(額外空間)+1(空字符)=21。若是對 SDS 修改後 len 長度大於 1M,那麼程序將分配 1M 的未使用空間。
4.惰性釋放空間: 當對 SDS 進行縮短操做時,程序並不會回收多餘的內存空間,而是使用 free 字段將這些字節數量記錄下來不釋放,後面若是須要 append 操做,則直接使用 free 中未使用的空間,減小了內存的分配。
經過惰性空間釋放策略, SDS 避免了縮短字符串時所需的內存重分配操做, 併爲未來可能有的增加操做提供了優化。
5.二進制安全 爲了確保 Redis 能夠適用於各類不一樣的使用場景, SDS 的 API 都是二進制安全的(binary-safe):全部 SDS API 都會以處理二進制的方式來處理 SDS 存放在  buf  數組裏的數據, 程序不會對其中的數據作任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。

這也是咱們將 SDS 的 buf 屬性稱爲字節數組的緣由 —— Redis 不是用這個數組來保存字符, 而是用它來保存一系列二進制數據。redis

好比說, 使用 SDS 來保存以前提到的特殊數據格式就沒有任何問題, 由於 SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結束, 如圖 2-18 所示。算法

參考資料:http://redisbook.com/preview/sds/different_between_sds_and_c_string.html

2) zipList 壓縮列表

壓縮列表是 List 、hash、 sorted Set 三種數據類型底層實現之一。數據庫

當一個列表只有少許數據的時候,而且每一個列表項要麼就是小整數值,要麼就是長度比較短的字符串,那麼 Redis 就會使用壓縮列表來作列表鍵的底層實現。數組

Ziplist 是由一系列特殊編碼的內存塊構成的列表, 一個 ziplist 能夠包含多個節點(entry), 每一個節點能夠保存一個長度受限的字符數組(不以 \0 結尾的 char 數組)或者整數。安全


字符數組長度小於等於 63 (26−1)字節的字符數組長度小於等於 16383 (214−1) 字節的字符數組長度小於等於 4294967295 (232−1)字節的字符數組整數4 位長,介於 0 至 12 之間的無符號整數1 字節長,有符號整數3 字節長,有符號整數int16_t 類型整數int32_t 類型整數int64_t 類型整數

ziplist 在表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表佔用字節數、列表尾的偏移量和列表中的 entry 個數;壓縮列表在表尾還有一個 zlend,表示列表結束。

若是咱們要查找定位第一個元素和最後一個元素,能夠經過表頭三個字段的長度直接定位,複雜度是 O(1)。而查找其餘元素時,就沒有這麼高效了,只能逐個查找,此時的複雜度就是 O(N)

3)雙端列表

Redis List 數據類型一般被用於隊列、微博關注人時間軸列表等場景。不論是先進先出的隊列,仍是先進後出的棧,雙端列表都很好的支持這些特性。

雙端鏈表仍是 Redis 列表類型的底層實現之一, 當對列表類型的鍵進行操做 —— 好比執行 RPUSH 、 LPOP 或 LLEN 等命令時, 程序在底層操做的可能就是雙端鏈表。

雙端鏈表及其節點的性能特性以下:

1.節點帶有前驅和後繼指針,訪問前驅節點和後繼節點的複雜度爲 O(1)O(1) ,而且對鏈表的迭代能夠在從表頭到表尾和從表尾到表頭兩個方向進行;2.鏈表帶有指向表頭和表尾的指針,所以對錶頭和表尾進行處理的複雜度爲 O(1)O(1) ;3.鏈表帶有記錄節點數量的屬性,因此能夠在 O(1)O(1) 複雜度內返回鏈表的節點數量(長度);

除此以外,Redis爲雙端鏈表還實現了一個迭代器, 這個迭代器能夠從兩個方向對雙端鏈表進行迭代:

1.沿着節點的 next 指針前進,從表頭向表尾迭代;2.沿着節點的 prev 指針前進,從表尾向表頭迭代;

雙端鏈表的實現:

參考資料:http://origin.redisbook.com/internal-datastruct/adlist.html

4)skipList 跳躍表


和字典、鏈表或者字符串這幾種在 Redis 中大量使用的數據結構不一樣, 跳躍表在 Redis 的惟一做用, 就是實現有序集數據類型。

跳錶簡介:
跳躍表以有序的方式在層次化的鏈表中保存元素, 效率和平衡樹媲美 —— 查找、刪除、添加等操做均可以在對數指望時間下完成, 而且比起平衡樹來講, 跳躍表的實現要簡單直觀得多。

從圖中能夠看到, 跳躍表主要由如下部分構成:
1.表頭(head):負責維護跳躍表的節點指針。2.跳躍表節點:保存着元素值,以及多個層。3.層:保存着指向其餘元素的指針。高層的指針越過的元素數量大於等於低層的指針,爲了提升查找的效率,程序老是從高層先開始訪問,而後隨着元素值範圍的縮小,慢慢下降層次。4.表尾:所有由 NULL 組成,表示跳躍表的末尾。
參考資料:https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

5)整數集合(intset)


整數集合(intset)用於有序、無重複地保存多個整數值, 根據元素的值, 自動選擇該用什麼長度的整數類型來保存元素。

舉個例子, 若是在一個 intset 裏面, 最長的元素能夠用  int16_t 類型來保存, 那麼這個 intset 的全部元素都以  int16_t 類型來保存。

另外一方面, 若是有一個新元素要加入到這個 intset , 而且這個元素不能用  int16_t 類型來保存 —— 好比說, 新元素的長度爲  int32_t , 那麼這個 intset 就會自動進行「升級」:先將集合中現有的全部元素從  int16_t 類型轉換爲  int32_t 類型, 接着再將新元素加入到集合中。

根據須要, intset 能夠自動從  int16_t 升級到  int32_t 或  int64_t , 或者從  int32_t 升級到  int64_t 。

      Intset 只支持升級,不支持降級。

    Intset 是有序的,程序使用二分查找算法來實現查找操做,複雜度爲