經過本文你將瞭解到如下內容:html
舒適提示:內容並不難,就怕你不看。git
看不懂能夠先收藏先Mark,等到深刻研究的時間再翻出來看看,你就發現真是24K乾貨呀!中止吹噓,寫點不同的文字吧!github
Redis是一個使用ANSI C編寫的開源、支持網絡、基於內存、可選持久化的高性能鍵值對數據庫。Redis的之父是來自意大利的西西里島的Salvatore Sanfilippo,Github網名antirez,筆者找了做者的一些簡要信息並翻譯了一下,如圖:面試
從2009年第一個版本起Redis已經走過了10個年頭,目前Redis仍然是最流行的key-value型內存數據庫的之一。redis
優秀的開源項目離不開大公司的支持,在2013年5月以前,其開發由VMware贊助,而2013年5月至2015年6月期間,其開發由畢威拓贊助,從2015年6月開始,Redis的開發由Redis Labs贊助。算法
筆者也使用過一些其餘的NoSQL,有的支持的value類型很是單一,所以不少操做都必須在客戶端實現,好比value是一個結構化的數據,須要修改其中某個字段就須要總體讀出來修改再總體寫入,顯得很笨重,可是Redis的value支持多種類型,實現了不少操做在服務端就能夠完成了,這個對客戶端而言很是方便。數據庫
固然Redis因爲是內存型的數據庫,數據量存儲量有限並且分佈式集羣成本也會很是高,所以有不少公司開發了基於SSD的類Redis系統,好比360開發的SSDB、Pika等數據庫,可是筆者認爲從0到1的難度是大於從1到2的難度的,毋庸置疑Redis是NoSQL中濃墨重彩的一筆,值得咱們去深刻研究和使用。api
Redis提供了Java、C/C++、C#、 PHP 、JavaScript、 Perl 、Object-C、Python、Ruby、Erlang、Golang等多種主流語言的客戶端,所以不管使用者是什麼語言棧總會找到屬於本身的那款客戶端,受衆很是廣。數組
筆者查了http://datanyze.com網站看了下Redis和MySQL的最新市場份額和排名對比以及全球Top站點的部署量對比(網站數據更新到寫做當日2019.12.11):緩存
能夠看到Redis整體份額排名第9而且在全球Top100站點中部署數量與MySQL基本持平,因此Redis仍是有必定的江湖地位的。
目前Redis發佈的穩定版本已經到了5.x,功能也愈來愈強大,從國內外互聯網公司來看Redis幾乎是標配了。做爲開發人員在平常筆試面試和工做中遇到Redis相關問題的機率很是大,掌握Redis的相關知識點都十分有必要。
學習和梳理一個複雜的東西確定不能鬍子眉毛一把抓,每一個人都有本身的認知思路,筆者認爲要從充分掌握Redis須要從底向上、從外到內去理解Redis。
Redis的實戰知識點能夠簡單分爲三個層次:
深刻理解和熟練使用Redis須要時間錘鍊,要作到信手拈來着實不易,想在短期內突破只能從熱點題目入手,雖然這樣感受有些功利,不過也算無可厚非吧,爲了吃飯咱們仍是傾向於原諒懶惰的本身,要否則吃土喝風?
底層實現篇的題目主要是與Redis的源碼和設計相關,能夠說是Redis功能的基石,瞭解底層實現可讓咱們更好地掌握功能,因爲底層代碼不少,在後續的基礎架構篇中仍然會穿插源碼來分析,所以本篇只列舉一些熱點的問題。
Q1: Redis經常使用五種數據類型是如何實現的?
Redis支持的經常使用5種數據類型指的是value類型,分別爲:字符串String、列表List、哈希Hash、集合Set、有序集合Zset,可是Redis後續又豐富了幾種數據類型分別是Bitmaps、HyperLogLogs、GEO。
因爲Redis是基於標準C寫的,只有最基礎的數據類型,所以Redis爲了知足對外使用的5種數據類型,開發了屬於本身獨有的一套基礎數據結構,使用這些數據結構來實現5種數據類型。
Redis底層的數據結構包括:簡單動態數組SDS、鏈表、哈希表、跳躍鏈表、整數集合、壓縮列表、對象。
Redis爲了平衡空間和時間效率,針對value的具體類型在底層會採用不一樣的數據結構來實現,其中哈希表和壓縮列表是複用比較多的數據結構,以下圖展現了對外數據類型和底層數據結構之間的映射關係:
從圖中能夠看到ziplist壓縮列表能夠做爲Zset、Set、List三種數據類型的底層實現,看來很強大,壓縮列表是一種爲了節約內存而開發的且通過特殊編碼以後的連續內存塊順序型數據結構,底層結構仍是比較複雜的。
Q2: Redis的SDS和C中字符串相比有什麼優點?
在C語言中使用N+1長度的字符數組來表示字符串,尾部使用'\0'做爲結尾標誌,對於此種實現沒法知足Redis對於安全性、效率、豐富的功能的要求,所以Redis單獨封裝了SDS簡單動態字符串結構。
在理解SDS的優點以前須要先看下SDS的實現細節,找了github最新的src/sds.h的定義看下:
typedef char *sds; /*這個用不到 忽略便可*/ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; /*不一樣長度的header 8 16 32 64共4種 都給出了四個成員 len:當前使用的空間大小;alloc去掉header和結尾空字符的最大空間大小 flags:8位的標記 下面關於SDS_TYPE_x的宏定義只有5種 3bit足夠了 5bit沒有用 buf:這個跟C語言中的字符數組是同樣的,從typedef char* sds能夠知道就是這樣的。 buf的最大長度是2^n 其中n爲sdshdr的類型,如當選擇sdshdr16,buf_max=2^16。 */ struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3
看了前面的定義,筆者畫了個圖:
從圖中能夠知道sds本質分爲三部分:header、buf、null結尾符,其中header能夠認爲是整個sds的指引部分,給定了使用的空間大小、最大分配大小等信息,再用一張網上的圖來清晰看下sdshdr8的實例:
在sds.h/sds.c源碼中可清楚地看到sds完整的實現細節,本文就不展開了要否則篇幅就過長了,快速進入主題說下sds的優點:
老規矩上一張黃健宏大神總結好的圖:
Q3:Redis的字典是如何實現的?簡述漸進式rehash的過程。
字典算是Redis5中經常使用數據類型中的明星成員了,前面說過字典能夠基於ziplist和hashtable來實現,咱們只討論基於hashtable實現的原理。
字典是個層次很是明顯的數據類型,如圖:
有了個大概的概念,咱們看下最新的src/dict.h源碼定義:
//哈希節點結構 typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; //封裝的是字典的操做函數指針 typedef struct dictType { uint64_t (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); } dictType; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ //哈希表結構 該部分是理解字典的關鍵 typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht; //字典結構 typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict;
C語言的好處在於定義必須是由最底層向外的,所以咱們能夠看到一個明顯的層次變化,因而筆者又畫一圖來展示具體的層次概念:
dictEntry是哈希表節點,也就是咱們存儲數據地方,其保護的成員有:key,v,next指針。key保存着鍵值對中的鍵,v保存着鍵值對中的值,值能夠是一個指針或者是uint64_t或者是int64_t。next是指向另外一個哈希表節點的指針,這個指針能夠將多個哈希值相同的鍵值對鏈接在一次,以此來解決哈希衝突的問題。
如圖爲兩個衝突的哈希節點的鏈接關係:
從源碼看哈希表包括的成員有table、size、used、sizemask。table是一個數組,數組中的每一個元素都是一個指向dictEntry結構的指針, 每一個dictEntry結構保存着一個鍵值對;size 屬性記錄了哈希表table的大小,而used屬性則記錄了哈希表目前已有節點的數量。sizemask等於size-1和哈希值計算一個鍵在table數組的索引,也就是計算index時用到的。
如上圖展現了一個大小爲4的table中的哈希節點狀況,其中k1和k0在index=2發生了哈希衝突,進行開鏈表存在,本質上是先存儲的k0,k1放置是發生衝突爲了保證效率直接放在衝突鏈表的最前面,由於該鏈表沒有尾指針。
從源碼中看到dict結構體就是字典的定義,包含的成員有type,privdata、ht、rehashidx。其中dictType指針類型的type指向了操做字典的api,理解爲函數指針便可,ht是包含2個dictht的數組,也就是字典包含了2個哈希表,rehashidx進行rehash時使用的變量,privdata配合dictType指向的函數做爲參數使用,這樣就對字典的幾個成員有了初步的認識。
//僞碼:使用哈希函數,計算鍵key的哈希值 hash = dict->type->hashFunction(key); //僞碼:使用哈希表的sizemask和哈希值,計算出在ht[0]或許ht[1]的索引值 index = hash & dict->ht[x].sizemask; //源碼定義 #define dictHashKey(d, key) (d)->type->hashFunction(key)
redis使用MurmurHash算法計算哈希值,該算法最初由Austin Appleby在2008年發明,MurmurHash算法的不管數據輸入狀況如何均可以給出隨機分佈性較好的哈希值而且計算速度很是快,目前有MurmurHash2和MurmurHash3等版本。
哈希表保存的鍵值對數量是動態變化的,爲了讓哈希表的負載因子維持在一個合理的範圍以內,就須要對哈希表進行擴縮容。
擴縮容是經過執行rehash從新散列來完成,對字典的哈希表執行普通rehash的基本步驟爲分配空間->逐個遷移->交換哈希表,詳細過程以下:
Redis的rehash動做並非一次性完成的,而是分屢次、漸進式地完成的,緣由在於當哈希表裏保存的鍵值對數量很大時, 一次性將這些鍵值對所有rehash到ht[1]可能會致使服務器在一段時間內中止服務,這個是沒法接受的。
針對這種狀況Redis採用了漸進式rehash,過程的詳細步驟:
漸進式 rehash的思想在於將rehash鍵值對所需的計算工做分散到對字典的每一個添加、刪除、查找和更新操做上,從而避免了集中式rehash而帶來的阻塞問題。
看到這裏不由去想這種捎帶腳式的rehash會不會致使整個過程很是漫長?若是某個value一直沒有操做那麼須要擴容時因爲一直不用因此影響不大,須要縮容時若是一直不處理可能形成內存浪費,具體的還沒來得及研究,先埋個問題吧!
Q4:跳躍鏈表瞭解嗎?Redis的Zset如何使用跳錶實現的?
ZSet這種數據類型也很是有用,在作排行榜需求時很是有用,筆者就曾經使用這種數據類型來實現某日活2000w的app的排行榜,因此瞭解下ZSet的底層實現頗有必要,以前筆者寫過兩篇文章介紹跳躍鏈表和ZSet的實現,所以查閱便可。
深刻理解跳躍鏈表[一]
深刻理解跳錶在Redis中的應用
Q5:Redis爲何使用單線程?講講Redis網絡模型以及單線程如何協調各類事件運行起來的?
Redis在新版本中並非單純的單線程服務,一些輔助工做會有BIO後臺線程來完成,而且Redis底層使用epoll來實現了基於事件驅動的反應堆模式,在整個主線程運行工程中不斷協調時間事件和文件事件來完成整個系統的運行,筆者以前寫過兩篇相關的文章,查閱便可獲得更深層次的答案。
理解Redis單線程運行模式
理解Redis的反應堆模式
淺析Redis 4.0新特性之LazyFree