【3y】從零單排學Redis【青銅】

前言

只有光頭才能變強git

redis

最近在學Redis,我相信只要是接觸過Java開發的都會聽過Redis這麼一個技術。面試也是很是高頻的一個知識點,以前一直都是處於瞭解階段。秋招事後這段時間是沒有什麼壓力的,因此打算系統學學Redis,這也算是我從零學習Redis的筆記吧。程序員

本文力求講清每一個知識點,但願你們看完能有所收穫。github

1、介紹一下Redis

首先,確定是去官網看看官方是怎麼介紹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入門。瀏覽器

1.1爲何要用Redis?

從上面可知:Redis是基於內存,經常使用做於緩存的一種技術,而且Redis存儲的方式是以key-value的形式。

咱們能夠發現這不就是Java的Map容器所擁有的特性嗎,那爲何還須要Redis呢?

  • Java實現的Map是本地緩存,若是有多臺實例(機器)的話,每一個實例都須要各自保存一份緩存,緩存不具備一致性
  • Redis實現的是分佈式緩存,若是有多臺實例(機器)的話,每一個實例都共享一份緩存,緩存具備一致性
  • Java實現的Map不是專業作緩存的,JVM內存太大容易掛掉的。通常用作於容器來存儲臨時數據,緩存的數據隨着JVM銷燬而結束。Map所存儲的數據結構,緩存過時機制等等是須要程序員本身手寫的。
  • Redis是專業作緩存的,能夠用幾十個G內存來作緩存。Redis通常用做於緩存,能夠將緩存數據保存在硬盤中,Redis重啓了後能夠將其恢復。原生提供豐富的數據結構、緩存過時機制等等簡單好用的功能。

參考資料:

1.2爲何要用緩存?

若是咱們的網站出現了性能問題(訪問時間慢),按經驗來講,通常是因爲數據庫撐不住了。由於通常數據庫的讀寫都是要通過磁盤的,而磁盤的速度能夠說是至關慢的(相對內存來講)

數據庫撐不住了

若是學過Mybaits、Hibernate的同窗就能夠知道,它們有一級緩存、二級緩存這樣的功能(終究來講仍是本地緩存)。目的就是爲了:不用每次讀取的時候,都要查一次數據庫

有了緩存以後,咱們的訪問就變成這樣了:

有了緩存提升了併發和性能

2、Redis的數據結構

本文不會講述命令的使用方式,具體的如何使用可查詢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數據結構

但要值得注意的是:Redis並沒有直接使用這些數據結構來實現key-value數據庫,而是基於這些數據結構建立了一個對象系統

  • 簡單來講:Redis使用對象來表示數據庫中的鍵和值。每次咱們在Redis數據庫中新建立一個鍵值對時,至少會建立出兩個對象。一個是鍵對象,一個是值對象。

Redis中的每一個對象都由一個redisObject結構來表示:

typedef struct redisObject{
	
	// 對象的類型
	unsigned type 4:;

	// 對象的編碼格式
	unsigned encoding:4;

	// 指向底層實現數據結構的指針
	void * ptr;

	//.....


}robj;


複製代碼

數據結構對應的類型與編碼

簡單來講就是Redis對key-value封裝成對象,key是一個對象,value也是一個對象。每一個對象都有type(類型)、encoding(編碼)、ptr(指向底層數據結構的指針)來表示。

以值爲1006的字符串對象爲例

下面我就來講一下咱們Redis常見的數據類型:string、list、hash、set、sortset。它們的底層數據結構到底是怎麼樣的!

2.1SDS簡單動態字符串

簡單動態字符串(Simple dynamic string,SDS)

Redis中的字符串跟C語言中的字符串,是有點差距的

Redis使用sdshdr結構來表示一個SDS值:

struct sdshdr{

	// 字節數組,用於保存字符串
	char buf[];

	// 記錄buf數組中已使用的字節數量,也是字符串的長度
	int len;

	// 記錄buf數組未使用的字節數量
	int free;
}
複製代碼

例子:

SDS例子

2.1.1使用SDS的好處

SDS與C的字符串表示比較

  1. sdshdr數據結構中用len屬性記錄了字符串的長度。那麼獲取字符串的長度時,時間複雜度只須要O(1)
  2. SDS不會發生溢出的問題,若是修改SDS時,空間不足。先會擴展空間,再進行修改!(內部實現了動態擴展機制)。
  3. SDS能夠減小內存分配的次數(空間預分配機制)。在擴展空間時,除了分配修改時所必要的空間,還會分配額外的空閒空間(free 屬性)。
  4. SDS是二進制安全的,全部SDS API都會以處理二進制的方式來處理SDS存放在buf數組裏的數據。

2.2鏈表

對於鏈表而言,咱們不會陌生的了。在大學期間確定開過數據結構與算法課程,鏈表確定是講過的了。在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

複製代碼

具體的結構如圖:

2.2.1Redis鏈表的特性

Redis的鏈表有如下特性:

  • 無環雙向鏈表
  • 獲取表頭指針,表尾指針,鏈表節點長度的時間複雜度均爲O(1)
  • 鏈表使用void *指針來保存節點值,能夠保存各類不一樣類型的值

2.3哈希表

聲明:《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中有兩個哈希表

  • ht[0]:用於存放真實key-vlaue數據
  • ht[1]:用於擴容(rehash)

Redis中哈希算法和哈希衝突跟Java實現的差很少,它倆差別就是:

  • Redis哈希衝突時:是將新節點添加在鏈表的表頭
  • JDK1.8後,Java在哈希衝突時:是將新的節點添加到鏈表的表尾

2.3.1rehash的過程

下面來具體講講Redis是怎麼rehash的,由於咱們從上面能夠明顯地看到,Redis是專門使用一個哈希表來作rehash的。這跟Java一次性直接rehash是有區別的。

在對哈希表進行擴展或者收縮操做時,reash過程並非一次性地完成的,而是漸進式地完成的。

Redis在rehash時採起漸進式的緣由:數據量若是過大的話,一次性rehash會有龐大的計算量,這極可能致使服務器一段時間內中止服務

Redis具體是rehash時這麼幹的:

  • (1:在字典中維持一個索引計數器變量rehashidx,並將設置爲0,表示rehash開始。
  • (2:在rehash期間每次對字典進行增長、查詢、刪除和更新操做時,除了執行指定命令外;還會將ht[0]中rehashidx索引上的值rehash到ht[1],操做完成後rehashidx+1。
  • (3:字典操做不斷執行,最終在某個時間點,全部的鍵值對完成rehash,這時將rehashidx設置爲-1,表示rehash完成
  • (4:在漸進式rehash過程當中,字典會同時使用兩個哈希表ht[0]和ht[1],全部的更新、刪除、查找操做也會在兩個哈希表進行。例如要查找一個鍵的話,服務器會優先查找ht[0],若是不存在,再查找ht[1],諸如此類。此外當執行新增操做時,新的鍵值對一概保存到ht[1],再也不對ht[0]進行任何操做,以保證ht[0]的鍵值對數量只減不增,直至變爲空表。

2.4跳躍表(shiplist)

跳躍表(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;


複製代碼

最後咱們整個跳躍表的示例圖以下:

跳躍表示例圖

2.5整數集合(intset)

整數集合是set(集合)的底層數據結構之一。當一個set(集合)只包含整數值元素,而且元素的數量很少時,Redis就會採用整數集合(intset)做爲set(集合)的底層實現。

整數集合(intset)保證了元素是不會出現重複的,而且是有序的(從小到大排序),intset的結構是這樣子的:

typeof struct intset {
        // 編碼方式
        unit32_t encoding;
        // 集合包含的元素數量
        unit32_t lenght;
        // 保存元素的數組
        int8_t contents[];
} intset;

複製代碼

intset示例圖:

intset示例圖

說明:雖然intset結構將contents屬性聲明爲int8_t類型的數組,但實際上contents數組並不保存任何int8_t類型的值,contents數組的真正類型取決於encoding屬性的值

  • INTSET_ENC_INT16
  • INTSET_ENC_INT32
  • INTSET_ENC_INT64

從編碼格式的名字咱們就能夠知道,16,32,64編碼對應能存放的數字範圍是不同的。16明顯最少,64明顯最大。

若是原本是INTSET_ENC_INT16的編碼,想要存放大於INTSET_ENC_INT16編碼能存放的整數值,此時就得編碼升級(從16升級成32或者64)。步驟以下:

  • 1)根據新元素類型拓展整數集合底層數組的空間併爲新元素分配空間。
  • 2)將底層數組現有的因此元素都轉換成與新元素相同的類型,並將類型轉換後的元素放到正確的位上,須要維持底層數組的有序性質不變。
  • 3)將新元素添加到底層數組。

另一提:只支持升級操做,並不支持降級操做

2.6壓縮列表(ziplist)

壓縮列表(ziplist)是list和hash的底層實現之一。若是list的每一個都是小整數值,或者是比較短的字符串,壓縮列表(ziplist)做爲list的底層實現。

壓縮列表(ziplist)是Redis爲了節約內存而開發的,是由一系列的特殊編碼的連續內存塊組成的順序性數據結構。

壓縮列表結構圖例以下:

壓縮列表的組成部分

下面咱們看看節點的結構圖:

壓縮列表從表尾節點倒序遍歷,首先指針經過zltail偏移量指向表尾節點,而後經過指向節點記錄的前一個節點的長度依次向前遍歷訪問整個壓縮列表

3、Redis中數據結構的對象

再次看回這張圖,覺不以爲就很好理解了?

數據結構對應的類型與編碼

3.1字符串(stirng)對象

在上面的圖咱們知道string類型有三種編碼格式

  • int:整數值,這個整數值可使用long類型來表示
    • 若是是浮點數,那就用embstr或者raw編碼。具體用哪一個就看這個數的長度了
  • embstr:字符串值,這個字符串值的長度小於39字節
  • raw:字符串值,這個字符串值的長度大於39字節

embstr和raw的區別

  • raw分配內存和釋放內存的次數是兩次,embstr是一次
  • embstr編碼的數據保存在一塊連續的內存裏面

編碼之間的轉換

  • int類型若是存的再也不是一個整數值,則會從int轉成raw
  • embstr是隻讀的,在修改的時候回從embstr轉成raw

3.2列表(list)對象

在上面的圖咱們知道list類型有兩種編碼格式

  • ziplist:字符串元素的長度都小於64個字節&&總數量少於512個
  • linkedlist:字符串元素的長度大於64個字節||總數量大於512個

ziplist編碼的列表結構:

redis > RPUSH numbers 1 "three" 5
	(integer) 3 

複製代碼

ziplist的列表結構

linkedlist編碼的列表結構:

linkedlist編碼的列表結構

編碼之間的轉換:

  • 本來是ziplist編碼的,若是保存的數據長度太大或者元素數量過多,會轉換成linkedlist編碼的。

3.3哈希(hash)對象

在上面的圖咱們知道hash類型有兩種編碼格式

  • ziplist:key和value的字符串長度都小於64字節&&鍵值對總數量小於512
  • hashtable:key和value的字符串長度大於64字節||鍵值對總數量大於512

ziplist編碼的哈希結構:

ziplist編碼的哈希結構1
ziplist編碼的哈希結構2

hashtable編碼的哈希結構:

hashtable編碼的哈希結構

編碼之間的轉換:

  • 本來是ziplist編碼的,若是保存的數據長度太大或者元素數量過多,會轉換成hashtable編碼的。

3.4集合(set)對象

在上面的圖咱們知道set類型有兩種編碼格式

  • intset:保存的元素全都是整數&&總數量小於512
  • hashtable:保存的元素不是整數||總數量大於512

intset編碼的集合結構:

intset編碼的集合結構

hashtable編碼的集合結構:

hashtable編碼的集合結構

編碼之間的轉換:

  • 本來是intset編碼的,若是保存的數據不是整數值或者元素數量大於512,會轉換成hashtable編碼的。

3.5有序集合(sortset)對象

在上面的圖咱們知道set類型有兩種編碼格式

  • ziplist:元素長度小於64&&總數量小於128
  • skiplist:元素長度大於64||總數量大於128

ziplist編碼的有序集合結構:

ziplist編碼的有序集合結構1

ziplist編碼的有序集合結構2

skiplist編碼的有序集合結構:

skiplist編碼的有序集合結構

有序集合(sortset)對象同時採用skiplist和哈希表來實現

  • skiplist可以達到插入的時間複雜度爲O(logn),根據成員查分值的時間複雜度爲O(1)

編碼之間的轉換:

  • 本來是ziplist編碼的,若是保存的數據長度大於64或者元素數量大於128,會轉換成skiplist編碼的。

3.6Redis對象一些細節

  • (1:服務器在執行某些命令的時候,會先檢查給定的鍵的類型可否執行指定的命令。
    • 好比咱們的數據結構是sortset,但你使用了list的命令。這是不對的,服務器會檢查一下咱們的數據結構是什麼纔會進一步執行命令
  • (2:Redis的對象系統帶有引用計數實現的內存回收機制
    • 對象再也不被使用的時候,對象所佔用的內存會釋放掉
  • (3:Redis會共享值爲0到9999的字符串對象
  • (4:對象會記錄本身的最後一次被訪問時間,這個時間能夠用於計算對象的空轉時間。

最後

本文主要講了一下Redis經常使用的數據結構,以及這些數據結構的底層設計是怎麼樣的。總體來講不會太難,由於這些數據結構咱們在學習的過程當中多多少少都接觸過了,《Redis設計與實現》這本書寫得也足夠通俗易懂。

至於咱們在使用的時候挑選哪些數據結構做爲存儲,能夠簡單看看:

  • string-->簡單的key-value
  • list-->有序列表(底層是雙向鏈表)-->可作簡單隊列
  • set-->無序列表(去重)-->提供一系列的交集、並集、差集的命令
  • hash-->哈希表-->存儲結構化數據
  • sortset-->有序集合映射(member-score)-->排行榜

若是你們有更好的理解方式或者文章有錯誤的地方還請你們不吝在評論區留言,你們互相學習交流~~~

參考博客:

參考資料:

  • 《Redis設計與實現》
  • 《Redis實戰》

一個堅持原創的Java技術公衆號:Java3y,歡迎你們關注

原創技術文章導航:

相關文章
相關標籤/搜索