只有光頭才能變強git
最近在學Redis,我相信只要是接觸過Java開發的都會聽過Redis這麼一個技術。面試也是很是高頻的一個知識點,以前一直都是處於瞭解階段。秋招事後這段時間是沒有什麼壓力的,因此打算系統學學Redis,這也算是我從零學習Redis的筆記吧。程序員
本文力求講清每一個知識點,但願你們看完能有所收穫。github
首先,確定是去官網看看官方是怎麼介紹Redis的啦。redis.io/topics/intr…面試
若是像我同樣,英語可能不太好的,可能看不太懂。沒事,我們Chrome瀏覽器能夠切換成中文的,中文是咱們的母語,確定沒啥壓力了。Eumm...redis
讀完以後,發現中文也就那樣了。算法
一大堆沒見過的技術:lua(Lua腳本)、replication(複製)、Redis Sentinel(哨兵)、Redis Cluster(Redis 集羣),固然咱們也會有看得懂的技術:transactions(事務)、different levels of on-disk persistence(數據持久化)、LRU eviction(LRU淘汰機制)..數據庫
至少官方介紹Redis的第一句應該是能夠很容易看懂:"Redis is an open source (BSD licensed),in-memory data structure store, used as a database,cache and message broker."segmentfault
Redis是一個開源的,基於內存的數據結構存儲,可用做於數據庫、緩存、消息中間件。數組
就我我的認爲:學習一種新技術,先把握該技術總體的知識(思想),再扣細節,這樣學習起來會比較輕鬆一些。因此咱們先以「內存」、「數據結構」、「緩存」來對Redis入門。瀏覽器
從上面可知:Redis是基於內存,經常使用做於緩存的一種技術,而且Redis存儲的方式是以key-value
的形式。
咱們能夠發現這不就是Java的Map容器所擁有的特性嗎,那爲何還須要Redis呢?
參考資料:
若是咱們的網站出現了性能問題(訪問時間慢),按經驗來講,通常是因爲數據庫撐不住了。由於通常數據庫的讀寫都是要通過磁盤的,而磁盤的速度能夠說是至關慢的(相對內存來講)
若是學過Mybaits、Hibernate的同窗就能夠知道,它們有一級緩存、二級緩存這樣的功能(終究來講仍是本地緩存)。目的就是爲了:不用每次讀取的時候,都要查一次數據庫。
有了緩存以後,咱們的訪問就變成這樣了:
本文不會講述命令的使用方式,具體的如何使用可查詢API。
Redis支持豐富的數據結構,經常使用的有string、list、hash、set、sortset這幾種。學習這些數據結構是使用Redis的基礎!
"Redis is written in ANSI C"-->Redis由C語言編寫
首先仍是得聲明一下,Redis的存儲是以key-value
的形式的。Redis中的key必定是字符串,value能夠是string、list、hash、set、sortset這幾種經常使用的。
但要值得注意的是:Redis並沒有直接使用這些數據結構來實現key-value
數據庫,而是基於這些數據結構建立了一個對象系統。
Redis中的每一個對象都由一個redisObject結構來表示:
typedef struct redisObject{
// 對象的類型
unsigned type 4:;
// 對象的編碼格式
unsigned encoding:4;
// 指向底層實現數據結構的指針
void * ptr;
//.....
}robj;
複製代碼
簡單來講就是Redis對key-value
封裝成對象,key是一個對象,value也是一個對象。每一個對象都有type(類型)、encoding(編碼)、ptr(指向底層數據結構的指針)來表示。
下面我就來講一下咱們Redis常見的數據類型:string、list、hash、set、sortset。它們的底層數據結構到底是怎麼樣的!
簡單動態字符串(Simple dynamic string,SDS)
Redis中的字符串跟C語言中的字符串,是有點差距的。
Redis使用sdshdr結構來表示一個SDS值:
struct sdshdr{
// 字節數組,用於保存字符串
char buf[];
// 記錄buf數組中已使用的字節數量,也是字符串的長度
int len;
// 記錄buf數組未使用的字節數量
int free;
}
複製代碼
例子:
SDS與C的字符串表示比較
對於鏈表而言,咱們不會陌生的了。在大學期間確定開過數據結構與算法課程,鏈表確定是講過的了。在Java中Linkedxxx容器底層數據結構也是鏈表+[xxx]的。咱們來看看Redis中的鏈表是怎麼實現的:
使用listNode結構來表示每一個節點:
typedef strcut listNode{
//前置節點
strcut listNode *pre;
//後置節點
strcut listNode *pre;
//節點的值
void *value;
}listNode
複製代碼
使用listNode是能夠組成鏈表了,Redis中使用list結構來持有鏈表:
typedef struct list{
//表頭結點
listNode *head;
//表尾節點
listNode *tail;
//鏈表長度
unsigned long len;
//節點值複製函數
void *(*dup) (viod *ptr);
//節點值釋放函數
void (*free) (viod *ptr);
//節點值對比函數
int (*match) (void *ptr,void *key);
}list
複製代碼
具體的結構如圖:
Redis的鏈表有如下特性:
void *
指針來保存節點值,能夠保存各類不一樣類型的值聲明:《Redis設計與實現》裏邊有「字典」這麼一個概念,我我的認爲仍是直接叫哈希表比較通俗易懂。從代碼上看:「字典」也是在哈希表基礎上再抽象了一層而已。
在Redis中,key-value
的數據結構底層就是哈希表來實現的。對於哈希表來講,咱們也並不陌生。在Java中,哈希表實際上就是數組+鏈表的形式來構建的。下面咱們來看看Redis的哈希表是怎麼構建的吧。
在Redis裏邊,哈希表使用dictht結構來定義:
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
//老是等於size-1
unsigned long sizemark;
//哈希表已有節點數量
unsigned long used;
}dictht
複製代碼
咱們下面繼續寫看看哈希表的節點是怎麼實現的吧:
typedef struct dictEntry {
//鍵
void *key;
//值
union {
void *value;
uint64_tu64;
int64_ts64;
}v;
//指向下個哈希節點,組成鏈表
struct dictEntry *next;
}dictEntry;
複製代碼
從結構上看,咱們能夠發現:Redis實現的哈希表和Java中實現的是相似的。只不過Redis多了幾個屬性來記錄經常使用的值:sizemark(掩碼)、used(已有的節點數量)、size(大小)。
一樣地,Redis爲了更好的操做,對哈希表往上再封裝了一層(參考上面的Redis實現鏈表),使用dict結構來表示:
typedef struct dict {
//類型特定函數
dictType *type;
//私有數據
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//當rehash不進行時,值爲-1
int rehashidx;
}dict;
//-----------------------------------
typedef struct dictType{
//計算哈希值的函數
unsigned int (*hashFunction)(const void * key);
//複製鍵的函數
void *(*keyDup)(void *private, const void *key);
//複製值得函數
void *(*valDup)(void *private, const void *obj);
//對比鍵的函數
int (*keyCompare)(void *privdata , const void *key1, const void *key2)
//銷燬鍵的函數
void (*keyDestructor)(void *private, void *key);
//銷燬值的函數
void (*valDestructor)(void *private, void *obj);
}dictType
複製代碼
因此,最後咱們能夠發現,Redis所實現的哈希表最後的數據結構是這樣子的:
從代碼實現和示例圖上咱們能夠發現,Redis中有兩個哈希表:
key-vlaue
數據Redis中哈希算法和哈希衝突跟Java實現的差很少,它倆差別就是:
下面來具體講講Redis是怎麼rehash的,由於咱們從上面能夠明顯地看到,Redis是專門使用一個哈希表來作rehash的。這跟Java一次性直接rehash是有區別的。
在對哈希表進行擴展或者收縮操做時,reash過程並非一次性地完成的,而是漸進式地完成的。
Redis在rehash時採起漸進式的緣由:數據量若是過大的話,一次性rehash會有龐大的計算量,這極可能致使服務器一段時間內中止服務。
Redis具體是rehash時這麼幹的:
跳躍表(shiplist)是實現sortset(有序集合)的底層數據結構之一!
跳躍表可能對於大部分人來講不太常見,以前我在學習的時候發現了一篇不錯的文章講跳躍表的,建議你們先去看完下文再繼續回來閱讀:
Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成。其中zskiplist保存跳躍表的信息(表頭,表尾節點,長度),zskiplistNode則表示跳躍表的節點。
按照慣例,咱們來看看zskiplistNode跳躍表節點的結構是怎麼樣的:
typeof struct zskiplistNode {
// 後退指針
struct zskiplistNode *backward;
// 分值
double score;
// 成員對象
robj *obj;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
複製代碼
zskiplistNode的對象示例圖(帶有不一樣層高的節點):
示例圖以下:
zskiplist的結構以下:
typeof struct zskiplist {
// 表頭節點,表尾節點
struct skiplistNode *header,*tail;
// 表中節點數量
unsigned long length;
// 表中最大層數
int level;
} zskiplist;
複製代碼
最後咱們整個跳躍表的示例圖以下:
整數集合是set(集合)的底層數據結構之一。當一個set(集合)只包含整數值元素,而且元素的數量很少時,Redis就會採用整數集合(intset)做爲set(集合)的底層實現。
整數集合(intset)保證了元素是不會出現重複的,而且是有序的(從小到大排序),intset的結構是這樣子的:
typeof struct intset {
// 編碼方式
unit32_t encoding;
// 集合包含的元素數量
unit32_t lenght;
// 保存元素的數組
int8_t contents[];
} intset;
複製代碼
intset示例圖:
說明:雖然intset結構將contents屬性聲明爲int8_t類型的數組,但實際上contents數組並不保存任何int8_t類型的值,contents數組的真正類型取決於encoding屬性的值:
從編碼格式的名字咱們就能夠知道,16,32,64編碼對應能存放的數字範圍是不同的。16明顯最少,64明顯最大。
若是原本是INTSET_ENC_INT16的編碼,想要存放大於INTSET_ENC_INT16編碼能存放的整數值,此時就得編碼升級(從16升級成32或者64)。步驟以下:
另一提:只支持升級操做,並不支持降級操做。
壓縮列表(ziplist)是list和hash的底層實現之一。若是list的每一個都是小整數值,或者是比較短的字符串,壓縮列表(ziplist)做爲list的底層實現。
壓縮列表(ziplist)是Redis爲了節約內存而開發的,是由一系列的特殊編碼的連續內存塊組成的順序性數據結構。
壓縮列表結構圖例以下:
下面咱們看看節點的結構圖:
壓縮列表從表尾節點倒序遍歷,首先指針經過zltail偏移量指向表尾節點,而後經過指向節點記錄的前一個節點的長度依次向前遍歷訪問整個壓縮列表。
再次看回這張圖,覺不以爲就很好理解了?
在上面的圖咱們知道string類型有三種編碼格式:
embstr和raw的區別:
編碼之間的轉換:
在上面的圖咱們知道list類型有兩種編碼格式:
&&
總數量少於512個||
總數量大於512個ziplist編碼的列表結構:
redis > RPUSH numbers 1 "three" 5
(integer) 3
複製代碼
linkedlist編碼的列表結構:
編碼之間的轉換:
在上面的圖咱們知道hash類型有兩種編碼格式:
&&
鍵值對總數量小於512||
鍵值對總數量大於512ziplist編碼的哈希結構:
hashtable編碼的哈希結構:
編碼之間的轉換:
在上面的圖咱們知道set類型有兩種編碼格式:
&&
總數量小於512||
總數量大於512intset編碼的集合結構:
hashtable編碼的集合結構:
編碼之間的轉換:
在上面的圖咱們知道set類型有兩種編碼格式:
&&
總數量小於128||
總數量大於128ziplist編碼的有序集合結構:
skiplist編碼的有序集合結構:
有序集合(sortset)對象同時採用skiplist和哈希表來實現:
編碼之間的轉換:
本文主要講了一下Redis經常使用的數據結構,以及這些數據結構的底層設計是怎麼樣的。總體來講不會太難,由於這些數據結構咱們在學習的過程當中多多少少都接觸過了,《Redis設計與實現》這本書寫得也足夠通俗易懂。
至於咱們在使用的時候挑選哪些數據結構做爲存儲,能夠簡單看看:
key-value
若是你們有更好的理解方式或者文章有錯誤的地方還請你們不吝在評論區留言,你們互相學習交流~~~
參考博客:
參考資料:
一個堅持原創的Java技術公衆號:Java3y,歡迎你們關注
原創技術文章導航: