大廠經典面試題:Redis為什麼這麼快?
前言
我們都知道Redis很快,它QPS可達10萬(每秒請求數)。Redis為什麼這麼快呢,本文將跟大家一起學習。
- 公眾號:撿田螺的小男孩
- github地址,感謝每一顆star
基於記憶體實現
我們都知道記憶體讀寫是比磁碟讀寫快很多的。Redis是基於記憶體儲存實現的資料庫,相對於資料存在磁碟的資料庫,就省去磁碟磁碟I/O的消耗。MySQL等磁碟資料庫,需要建立索引來加快查詢效率,而Redis資料存放在記憶體,直接操作記憶體,所以就很快。
高效的資料結構
我們知道,MySQL索引為了提高效率,選擇了B+樹的資料結構。其實合理的資料結構,就是可以讓你的應用/程式更快。先看下Redis的資料結構&內部編碼圖:
SDS簡單動態字串
struct sdshdr { //SDS簡單動態字串
int len; //記錄buf中已使用的空間
int free; // buf中空閒空間長度
char buf[]; //儲存的實際內容
}
字串長度處理
在C語言中,要獲取撿田螺的小男孩
這個字串的長度,需要從頭開始遍歷,複雜度為O(n); 在Redis中, 已經有一個len欄位記錄當前字串的長度啦,直接獲取即可,時間複雜度為O(1)。
減少記憶體重新分配的次數
在C語言中,修改一個字串,需要重新分配記憶體,修改越頻繁,記憶體分配就越頻繁,而分配記憶體是會消耗效能的。而在Redis中,SDS提供了兩種優化策略:空間預分配和惰性空間釋放。
空間預分配
當SDS簡單動態字串修改和空間擴充時,除了分配必需的記憶體空間,還會額外分配未使用的空間。分配規則是醬紫的:
- SDS修改後,len的長度小於1M,那麼將額外分配與len相同長度的未使用空間。比如len=100,重新分配後,buf 的實際長度會變為100(已使用空間)+100(額外空間)+1(空字元)=201。
- SDS修改後, len長度大於1M,那麼程式將分配1M的未使用空間。
惰性空間釋放
當SDS縮短時,不是回收多餘的記憶體空間,而是用free記錄下多餘的空間。後續再有修改操作,直接使用free中的空間,減少記憶體分配。
雜湊
Redis 作為一個K-V的記憶體資料庫,它使用用一張全域性的雜湊來儲存所有的鍵值對。這張雜湊表,有多個雜湊桶組成,雜湊桶中的entry元素儲存了*key
*value
指標,其中*key
指向了實際的鍵,*value
指向了實際的值。
雜湊表查詢速率很快的,有點類似於Java中的HashMap,它讓我們在O(1) 的時間複雜度快速找到鍵值對。首先通過key計算雜湊值,找到對應的雜湊桶位置,然後定位到entry,在entry找到對應的資料。
有些小夥伴可能會有疑問:你往雜湊表中寫入大量資料時,不是會遇到雜湊衝突問題嘛,那效率就會降下來啦。
雜湊衝突: 通過不同的key,計算出一樣的雜湊值,導致落在同一個雜湊桶中。
Redis為了解決雜湊衝突,採用了鏈式雜湊。鏈式雜湊是指同一個雜湊桶中,多個元素用一個連結串列來儲存,它們之間依次用指標連線。
有些小夥伴可能還會有疑問:雜湊衝突鏈上的元素只能通過指標逐一查詢再操作。當往雜湊表插入資料很多,衝突也會越多,衝突連結串列就會越長,那查詢效率就會降低了。
為了保持高效,Redis 會對雜湊表做rehash操作,也就是增加雜湊桶,減少衝突。為了rehash更高效,Redis還預設使用了兩個全域性雜湊表,一個用於當前使用,稱為主雜湊表,一個用於擴容,稱為備用雜湊表。
跳躍表
跳躍表是Redis特有的資料結構,它其實就是在連結串列的基礎上,增加多級索引,以提高查詢效率。跳躍表的簡單原理圖如下:
- 每一層都有一條有序的連結串列,最底層的連結串列包含了所有的元素。
- 跳躍表支援平均 O(logN),最壞 O(N)複雜度的節點查詢,還可以通過順序性操作批量處理節點。
壓縮列表ziplist
壓縮列表ziplist是列表鍵和字典鍵的的底層實現之一。它是由一系列特殊編碼的記憶體塊構成的列表, 一個ziplist可以包含多個entry, 每個entry可以儲存一個長度受限的字元陣列或者整數,如下:
- zlbytes :記錄整個壓縮列表佔用的記憶體位元組數
- zltail: 尾節點至起始節點的偏移量
- zllen : 記錄整個壓縮列表包含的節點數量
- entryX: 壓縮列表包含的各個節點
- zlend : 特殊值0xFF(十進位制255),用於標記壓縮列表末端
由於記憶體是連續分配的,所以遍歷速度很快。。
合理的資料編碼
Redis支援多種資料基本型別,每種基本型別對應不同的資料結構,每種資料結構對應不一樣的編碼。為了提高效能,Redis設計者總結出,資料結構最適合的編碼搭配。
Redis是使用物件(redisObject)來表示資料庫中的鍵值,當我們在 Redis 中建立一個鍵值對時,至少建立兩個物件,一個物件是用做鍵值對的鍵物件,另一個是鍵值對的值物件。
//關注公眾號:撿田螺的小男孩
typedef struct redisObject{
//型別
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層資料結構的指標
void *ptr;
//...
}robj;
redisObject中,type 對應的是物件型別,包含String物件、List物件、Hash物件、Set物件、zset物件。encoding 對應的是編碼。
- 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(跳躍表)編碼
合理的執行緒模型
單執行緒模型:避免了上下文切換
Redis是單執行緒的,其實是指Redis的網路IO和鍵值對讀寫是由一個執行緒來完成的。但Redis的其他功能,比如持久化、非同步刪除、叢集資料同步等等,實際是由額外的執行緒執行的。
Redis的單執行緒模型,避免了CPU不必要的上下文切換和競爭鎖的消耗。也正因為是單執行緒,如果某個命令執行過長(如hgetall命令),會造成阻塞。Redis是面向快速執行場景的記憶體資料庫,所以要慎用如lrange和smembers、hgetall等命令。
什麼是上下文切換?舉個粟子:
- 比如你在看一本英文小說,你看到某一頁,發現有個單詞不會讀,你加了個書籤,然後去查字典。查完字典後,你回來從書籤那裡繼續開始讀,這個流程就很舒暢。
- 如果你一個人讀這本書,肯定沒啥問題。但是如果你去查字典的時候,別的小夥伴翻了一下你的書,然後溜了。你再回來看的時候,發現書不是你看的那一頁了,你得花時間找到你的那一頁。
- 一本書,你一個人怎麼看怎麼打標籤都沒事,但是人多了翻來翻去,這本書各種標記就很亂了。可能這個解釋很粗糙,但是道理應該是一樣的。
I/O 多路複用
什麼是I/O多路複用?
- I/O :網路 I/O
- 多路 :多個網路連線
- 複用:複用同一個執行緒。
- IO多路複用其實就是一種同步IO模型,它實現了一個執行緒可以監視多個檔案控制代碼;一旦某個檔案控制代碼就緒,就能夠通知應用程式進行相應的讀寫操作;而沒有檔案控制代碼就緒時,就會阻塞應用程式,交出cpu。
多路I/O複用技術可以讓單個執行緒高效的處理多個連線請求,而Redis使用用epoll作為I/O多路複用技術的實現。並且Redis自身的事件處理模型將epoll中的連線、讀寫、關閉都轉換為事件,不在網路I/O上浪費過多的時間。
虛擬記憶體機制
Redis直接自己構建了VM機制 ,不會像一般的系統會呼叫系統函式處理,會浪費一定的時間去移動和請求。
Redis的虛擬記憶體機制是啥呢?
虛擬記憶體機制就是暫時把不經常訪問的資料(冷資料)從記憶體交換到磁碟中,從而騰出寶貴的記憶體空間用於其它需要訪問的資料(熱資料)。通過VM功能可以實現冷熱資料分離,使熱資料仍在記憶體中、冷資料儲存到磁碟。這樣就可以避免因為記憶體不足而造成訪問速度下降的問題。