日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者 | 一洺

一、什么是NoSQL?

Nosql = not only sql(不僅僅是SQL)。

關系型數據庫:列+行,同一個表下數據的結構是一樣的。

非關系型數據庫:數據存儲沒有固定的格式,并且可以進行橫向擴展。

NoSQL泛指非關系型數據庫,隨著web2.0互聯網的誕生,傳統的關系型數據庫很難對付web2.0大數據時代!尤其是超大規模的高并發的社區,暴露出來很多難以克服的問題,NoSQL在當今大數據環境下發展的十分迅速,redis是發展最快的。

傳統RDBMS和NoSQL

 
RDBMS
 - 組織化結構
 - 固定SQL
 - 數據和關系都存在單獨的表中(行列)
 - DML(數據操作語言)、DDL(數據定義語言)等
 - 嚴格的一致性(ACID): 原子性、一致性、隔離性、持久性
 - 基礎的事務

NoSQL
 - 不僅僅是數據
 - 沒有固定查詢語言
 - 鍵值對存儲(redis)、列存儲(HBase)、文檔存儲(MongoDB)、圖形數據庫(不是存圖形,放的是關系)(Neo4j)
 - 最終一致性(BASE):基本可用、軟狀態/柔性事務、最終一致性

二、Redis是什么?

Redis = Remote Dictionary Server,即遠程字典服務。

是一個開源的使用ANSI C語言編寫、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。

與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎上實現了master-slave(主從)同步。

三、Redis五大基本類型

Redis是一個開源,內存存儲的數據結構服務器,可用作數據庫,高速緩存和消息隊列代理。它支持字符串、哈希表、列表、集合、有序集合,位圖,hyperloglogs等數據類型。內置復制、Lua腳本、LRU收回、事務以及不同級別磁盤持久化功能,同時通過Redis Sentinel提供高可用,通過Redis Cluster提供自動分區。

由于redis類型大家很熟悉,且網上命令使用介紹很多,下面重點介紹五大基本類型的底層數據結構與應用場景,以便當開發時,可以熟練使用redis。

1.String(字符串)

(1)String類型是redis的最基礎的數據結構,也是最經常使用到的類型。 而且其他的四種類型多多少少都是在字符串類型的基礎上構建的,所以String類型是redis的基礎。

(2)String 類型的值最大能存儲 512MB,這里的String類型可以是簡單字符串、 復雜的xml/json的字符串、二進制圖像或者音頻的字符串、以及可以是數字的字符串。

應用場景

(1)緩存功能:String字符串是最常用的數據類型,不僅僅是redis,各個語言都是最基本類型,因此,利用redis作為緩存,配合其它數據庫作為存儲層,利用redis支持高并發的特點,可以大大加快系統的讀寫速度、以及降低后端數據庫的壓力。

(2)計數器:許多系統都會使用redis作為系統的實時計數器,可以快速實現計數和查詢的功能。而且最終的數據結果可以按照特定的時間落地到數據庫或者其它存儲介質當中進行永久保存。

(3)統計多單位的數量:eg,uid:gongming count:0 根據不同的uid更新count數量。

(4)共享用戶session:用戶重新刷新一次界面,可能需要訪問一下數據進行重新登錄,或者訪問頁面緩存cookie,這兩種方式做有一定弊端,1)每次都重新登錄效率低下 2)cookie保存在客戶端,有安全隱患。這時可以利用redis將用戶的session集中管理,在這種模式只需要保證redis的高可用,每次用戶session的更新和獲取都可以快速完成。大大提高效率。

2.List(列表)

 
list類型是用來存儲多個有序的字符串的,列表當中的每一個字符看做一個元素
一個列表當中可以存儲有一個或者多個元素,redis的list支持存儲2^32次方-1個元素。
redis可以從列表的兩端進行插入(pubsh)和彈出(pop)元素,支持讀取指定范圍的元素集,
  或者讀取指定下標的元素等操作。redis列表是一種比較靈活的鏈表數據結構,它可以充當隊列或者棧的角色。
redis列表是鏈表型的數據結構,所以它的元素是有序的,而且列表內的元素是可以重復的。
  意味著它可以根據鏈表的下標獲取指定的元素和某個范圍內的元素集。

應用場景

(1)消息隊列:reids的鏈表結構,可以輕松實現阻塞隊列,可以使用左進右出的命令組成來完成隊列的設計。比如:數據的生產者可以通過Lpush命令從左邊插入數據,多個數據消費者,可以使用BRpop命令阻塞的“搶”列表尾部的數據。

(2)文章列表或者數據分頁展示的應用。比如,我們常用的博客網站的文章列表,當用戶量越來越多時,而且每一個用戶都有自己的文章列表,而且當文章多時,都需要分頁展示,這時可以考慮使用redis的列表,列表不但有序同時還支持按照范圍內獲取元素,可以完美解決分頁查詢功能。大大提高查詢效率。

3.Set(集合)

 
.redis集合(set)類型和list列表類型類似,都可以用來存儲多個字符串元素的集合。
但是和list不同的是set集合當中不允許重復的元素。而且set集合當中元素是沒有順序的,不存在元素下標。
redis的set類型是使用哈希表構造的,因此復雜度是O(1),它支持集合內的增刪改查,
并且支持多個集合間的交集、并集、差集操作。可以利用這些集合操作,解決程序開發過程當中很多數據集合間的問題。

(1)標簽:比如我們博客網站常常使用到的興趣標簽,把一個個有著相同愛好,關注類似內容的用戶利用一個標簽把他們進行歸并。

(2)共同好友功能,共同喜好,或者可以引申到二度好友之類的擴展應用。

(3)統計網站的獨立IP。利用set集合當中元素不唯一性,可以快速實時統計訪問網站的獨立IP。

數據結構

set的底層結構相對復雜寫,使用了intset和hashtable兩種數據結構存儲,intset可以理解為數組。

4.sorted set(有序集合)

redis有序集合也是集合類型的一部分,所以它保留了集合中元素不能重復的特性,但是不同的是,有序集合給每個元素多設置了一個分數。

 
redis有序集合也是集合類型的一部分,所以它保留了集合中元素不能重復的特性,但是不同的是,
有序集合給每個元素多設置了一個分數,利用該分數作為排序的依據。

應用場景

(1)排行榜:有序集合經典使用場景。例如視頻網站需要對用戶上傳的視頻做排行榜,榜單維護可能是多方面:按照時間、按照播放量、按照獲得的贊數等。

(2)用Sorted Sets來做帶權重的隊列,比如普通消息的score為1,重要消息的score為2,然后工作線程可以選擇按score的倒序來獲取工作任務。讓重要的任務優先執行。

5.hash(哈希)

 
Redis hash數據結構 是一個鍵值對(key-value)集合,它是一個 string 類型的 field 和 value 的映射表,
redis本身就是一個key-value型數據庫,因此hash數據結構相當于在value中又套了一層key-value型數據。
所以redis中hash數據結構特別適合存儲關系型對象

應用場景

(1)由于hash數據類型的key-value的特性,用來存儲關系型數據庫中表記錄,是redis中哈希類型最常用的場景。一條記錄作為一個key-value,把每列屬性值對應成field-value存儲在哈希表當中,然后通過key值來區分表當中的主鍵。

(2)經常被用來存儲用戶相關信息。優化用戶信息的獲取,不需要重復從數據庫當中讀取,提高系統性能。

四、五大基本類型底層數據存儲結構

在學習基本類型底層數據存儲結構前,首先看下redis整體的存儲結構。

redis內部整體的存儲結構是一個大的hashmap,內部是數組實現的hash,key沖突通過掛鏈表去實現,每個dictEntry為一個key/value對象,value為定義的redisObject。

結構圖如下:

dictEntry是存儲key->value的地方,再讓我們看一下dictEntry結構體。

 
/*
 * 字典
 */
typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        // 指向具體redisObject
        void *val;
        // 
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個哈希表節點,形成鏈表
    struct dictEntry *next;
} dictEntry;

1.redisObject

我們接著再往下看redisObject究竟是什么結構的。

 
/*
 * Redis 對象
 */
typedef struct redisObject {
    // 類型 4bits
    unsigned type:4;
    // 編碼方式 4bits
    unsigned encoding:4;
    // LRU 時間(相對于 server.lruclock) 24bits
    unsigned lru:22;
    // 引用計數 Redis里面的數據可以通過引用計數進行共享 32bits
    int refcount;
    // 指向對象的值 64-bit
    void *ptr;
} robj;

*ptr指向具體的數據結構的地址;type表示該對象的類型,即String,List,Hash,Set,Zset中的一個,但為了提高存儲效率與程序執行效率,每種對象的底層數據結構實現都可能不止一種,encoding 表示對象底層所使用的編碼。

redis對象底層的八種數據結構

REDIS_ENCODING_INT(long 類型的整數)
 REDIS_ENCODING_EMBSTR embstr (編碼的簡單動態字符串)
 REDIS_ENCODING_RAW (簡單動態字符串)
 REDIS_ENCODING_HT (字典)
 REDIS_ENCODING_LINKEDLIST (雙端鏈表)
 REDIS_ENCODING_ZIPLIST (壓縮列表)
 REDIS_ENCODING_INTSET (整數集合)
 REDIS_ENCODING_SKIPLIST (跳躍表和字典)

好了,通過redisObject就可以具體指向redis數據類型了,總結一下每種數據類型都使用了哪些數據結構,如下圖所示:

前期準備知識已準備完畢,下面分每種基本類型來講。

2.String數據結構

String類型的轉換順序

  • 當保存的值為整數且值的大小不超過long的范圍,使用整數存儲。
  • 當字符串長度不超過44字節時,使用EMBSTR 編碼。

它只分配一次內存空間,redisObject和sds是連續的內存,查詢效率會快很多,也正是因為redisObject和sds是連續在一起,伴隨了一些缺點:當字符串增加的時候,它長度會增加,這個時候又需要重新分配內存,導致的結果就是整個redisObject和sds都需要重新分配空間,這樣是會影響性能的,所以redis用embstr實現一次分配而后,只允許讀,如果修改數據,那么它就會轉成raw編碼,不再用embstr編碼了。

  • 大于44字符時,使用raw編碼。

SDS

embstr和raw都為sds編碼,看一下sds的結構體:

 
/* 針對不同長度整形做了相應的數據結構
 * Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. 
 */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

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[];
};

由于redis底層使用c語言實現,可能會有疑問為什么不用c語言的字符串呢,而是用sds結構體。

1) 低復雜度獲取字符串長度:由于len存在,可以直接查出字符串長度,復雜度O(1);如果用c語言字符串,查詢字符串長度需要遍歷整個字符串,復雜度為O(n);

2) 避免緩沖區溢出:進行兩個字符串拼接c語言可使用strcat函數,但如果沒有足夠的內存空間。就會造成緩沖區溢出;而用sds在進行合并時會先用len檢查內存空間是否滿足需求,如果不滿足,進行空間擴展,不會造成緩沖區溢出;

3)減少修改字符串的內存重新分配次數:c語言字符串不記錄字符串長度,如果要修改字符串要重新分配內存,如果不進行重新分配會造成內存緩沖區泄露;

redis sds實現了空間預分配和惰性空間釋放兩種策略。

空間預分配:

1)如果sds修改后,sds長度(len的值)將于1mb,那么會分配與len相同大小的未使用空間,此時len與free值相同。例如,修改之后字符串長度為100字節,那么會給分配100字節的未使用空間。最終sds空間實際為 100 + 100 + 1(保存空字符'');

2)如果大于等于1mb,每次給分配1mb未使用空間惰性空間釋放:對字符串進行縮短操作時,程序不立即使用內存重新分配來回收縮短后多余的字節,而是使用 free 屬性將這些字節的數量記錄下來,等待后續使用(sds也提供api,我們可以手動觸發字符串縮短);

3)二進制安全:因為C字符串以空字符作為字符串結束的標識,而對于一些二進制文件(如圖片等),內容可能包括空字符串,因此C字符串無法正確存取;而所有 SDS 的API 都是以處理二進制的方式來處理 buf 里面的元素,并且 SDS 不是以空字符串來判斷是否結束,而是以 len 屬性表示的長度來判斷字符串是否結束;

4)遵從每個字符串都是以空字符串結尾的慣例,這樣可以重用 C 語言庫 中的一部分函數。

學習完sds,我們回到上面講到的,為什么小于44字節用embstr編碼呢?

再看一下rejectObject和sds定義的結構(短字符串的embstr用最小的sdshdr8)。

 
typedef struct redisObject {
    // 類型 4bits
    unsigned type:4;
    // 編碼方式 4bits
    unsigned encoding:4;
    // LRU 時間(相對于 server.lruclock) 24bits
    unsigned lru:22;
    // 引用計數 Redis里面的數據可以通過引用計數進行共享 32bits
    int refcount;
    // 指向對象的值 64-bit
    void *ptr;
} robj;
 
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[];
};

redisObject占用空間

4 + 4 + 24 + 32 + 64 = 128bits = 16字節。

sdshdr8占用空間

1(uint8_t) + 1(uint8_t)+ 1 (unsigned char)+ 1(buf[]中結尾的''字符)= 4字節。

初始最小分配為64字節,所以只分配一次空間的embstr最大為 64 - 16- 4 = 44字節。

3.List存儲結構

(1)Redis3.2之前的底層實現方式:壓縮列表ziplist 或者 雙向循環鏈表linkedlist。

當list存儲的數據量比較少且同時滿足下面兩個條件時,list就使用ziplist存儲數據:

  • list中保存的每個元素的長度小于 64 字節;
  • 列表中數據個數少于512個。

(2)Redis3.2及之后的底層實現方式:quicklist。

quicklist是一個雙向鏈表,而且是一個基于ziplist的雙向鏈表,quicklist的每個節點都是一個ziplist,結合了雙向鏈表和ziplist的優點。

ziplist

ziplist是一種壓縮鏈表,它的好處是更能節省內存空間,因為它所存儲的內容都是在連續的內存區域當中的。當列表對象元素不大,每個元素也不大的時候,就采用ziplist存儲。但當數據量過大時就ziplist就不是那么好用了。因為為了保證他存儲內容在內存中的連續性,插入的復雜度是O(N),即每次插入都會重新進行realloc。如下圖所示,redisObject對象結構中ptr所指向的就是一個ziplist。整個ziplist只需要malloc一次,它們在內存中是一塊連續的區域。

ziplist結構如下:

(1)zlbytes:用于記錄整個壓縮列表占用的內存字節數。

(2)zltail:記錄要列表尾節點距離壓縮列表的起始地址有多少字節 。

(3)zllen:記錄了壓縮列表包含的節點數量。

(4)entryX:要說列表包含的各個節點。

(5)zlend:用于標記壓縮列表的末端。

為什么數據量大時不用ziplist?

因為ziplist是一段連續的內存,插入的時間復雜化度為O(n),而且每當插入新的元素需要realloc做內存擴展;而且如果超出ziplist內存大小,還會做重新分配的內存空間,并將內容復制到新的地址。如果數量大的話,重新分配內存和拷貝內存會消耗大量時間。所以不適合大型字符串,也不適合存儲量多的元素。

快速列表(quickList)

快速列表是ziplist和linkedlist的混合體,是將linkedlist按段切分,每一段用ziplist來緊湊存儲,多個ziplist之間使用雙向指針鏈接。

為什么不直接使用linkedlist?

linkedlist的附加空間相對太高,prev和next指針就要占去16個字節,而且每一個結點都是單獨分配,會加劇內存的碎片化,影響內存管理效率。

quicklist結構

 
typedef struct quicklist {
    // 指向quicklist的頭部
    quicklistNode *head;
    // 指向quicklist的尾部
    quicklistNode *tail;
    unsigned long count;
    unsigned int len;
    // ziplist大小限定,由list-max-ziplist-size給定
    int fill : 16;
    // 節點壓縮深度設置,由list-compress-depth給定
    unsigned int compress : 16;
} quicklist;

typedef struct quicklistNode {
    // 指向上一個ziplist節點
    struct quicklistNode *prev;
    // 指向下一個ziplist節點
    struct quicklistNode *next;
    // 數據指針,如果沒有被壓縮,就指向ziplist結構,反之指向quicklistLZF結構
    unsigned char *zl;
    // 表示指向ziplist結構的總長度(內存占用長度)
    unsigned int sz;
    // ziplist數量
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    // 預留字段,存放數據的方式,1--NONE,2--ziplist
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    // 解壓標記,當查看一個被壓縮的數據時,需要暫時解壓,標記此參數為1,之后再重新進行壓縮
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    // 擴展字段
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

typedef struct quicklistLZF {
    // LZF壓縮后占用的字節數
    unsigned int sz; /* LZF size in bytes*/
    // 柔性數組,存放壓縮后的ziplist字節數組
    char compressed[];
} quicklistLZF;

結構圖如下:

ziplist的長度

quicklist內部默認單個ziplist長度為8k字節,超出了這個字節數,就會新起一個ziplist。關于長度可以使用list-max-ziplist-size決定。

壓縮深度

我們上面說到了quicklist下是用多個ziplist組成的,同時為了進一步節約空間,Redis還會對ziplist進行壓縮存儲,使用LZF算法壓縮,可以選擇壓縮深度。quicklist默認的壓縮深度是0,也就是不壓縮。壓縮的實際深度由配置參數list-compress-depth決定。為了支持快速push/pop操作,quicklist 的首尾兩個 ziplist 不壓縮,此時深度就是 1。如果深度為 2,就表示 quicklist 的首尾第一個 ziplist 以及首尾第二個 ziplist 都不壓縮。

4.Hash類型

當Hash中數據項比較少的情況下,Hash底層才用壓縮列表ziplist進行存儲數據,隨著數據的增加,底層的ziplist就可能會轉成dict,具體配置如下:

 
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

在List中已經介紹了ziplist,下面來介紹下dict。

看下數據結構:

 
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

typedef struct dictht {
    //指針數組,這個hash的桶
    dictEntry **table;
    //元素個數
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictEntry大家應該熟悉,在上面有講,使用來真正存儲key->value的地方
typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        // 指向具體redisObject
        void *val;
        // 
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個哈希表節點,形成鏈表
    struct dictEntry *next;
} dictEntry;

我們可以看到每個dict中都有兩個hashtable。

結構圖如下:

雖然dict結構有兩個hashtable,但是通常情況下只有一個hashtable是有值的。但是在dict擴容縮容的時候,需要分配新的hashtable,然后進行漸近式搬遷,這時候兩個hashtable存儲的舊的hashtable和新的hashtable。搬遷結束后,舊hashtable刪除,新的取而代之。

下面讓我們學習下rehash全貌。

5.漸進式rehash

所謂漸進式rehash是指我們的大字典的擴容是比較消耗時間的,需要重新申請新的數組,然后將舊字典所有鏈表的元素重新掛接到新的數組下面,是一個O(n)的操作。但是因為我們的redis是單線程的,無法承受這樣的耗時過程,所以采用了漸進式rehash小步搬遷,雖然慢一點,但是可以搬遷完畢。

擴容條件

我們的擴容一般會在Hash表中的元素個數等于第一維數組的長度的時候,就會開始擴容。擴容的大小是原數組的兩倍。不過在redis在做bgsave(RDB持久化操作的過程),為了減少內存頁的過多分離(Copy On Write),redis不會去擴容。但是如果hash表的元素個數已經到達了第一維數組長度的5倍的時候,就會強制擴容,不管你是否在持久化。

不擴容主要是為了盡可能減少內存頁過多分離,系統需要更多的開銷去回收內存。

縮容條件

當我們的hash表元素逐漸刪除的越來越少的時候。redis于是就會對hash表進行縮容來減少第一維數組長度的空間占用。縮容的條件是元素個數低于數組長度的10%,并且縮容不考慮是否在做redis持久化。

不用考慮bgsave主要是因為我們的縮容的內存都是已經使用過的,縮容的時候可以直接置空,而且由于申請的內存比較小,同時會釋放掉一些已經使用的內存,不會增大系統的壓力。

rehash步驟

(1)為ht[1] 分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表;

(2)定時維持一個索引計數器變量rehashidx,并將它的值設置為0,表示rehash 開始;

(3)在rehash進行期間,每次對字典執行CRUD操作時,程序除了執行指定的操作以外,還會將ht[0]中的數據rehash 到ht[1]表中,并且將rehashidx加一;

(4)當ht[0]中所有數據轉移到ht[1]中時,將rehashidx 設置成-1,表示rehash 結束;

(采用漸進式rehash 的好處在于它采取分而治之的方式,避免了集中式rehash 帶來的龐大計算量。特別的在進行rehash時只能對h[0]元素減少的操作,如查詢和刪除;而查詢是在兩個哈希表中查找的,而插入只能在ht[1]中進行,ht[1]也可以查詢和刪除。)

(5)將ht[0]釋放,然后將ht[1]設置成ht[0],最后為ht[1]分配一個空白哈希表。

過程如下圖:

6.set數據結構

Redis 的集合相當于JAVA中的 HashSet,它內部的鍵值對是無序、唯一的。它的內部實現相當于一個特殊的字典,字典中所有的 value 都是一個值 NULL。集合Set類型底層編碼包括hashtable和inset。

當存儲的數據同時滿足下面這樣兩個條件的時候,Redis 就采用整數集合intset來實現set這種數據類型:

  • 存儲的數據都是整數
  • 存儲的數據元素個數小于512個

當不能同時滿足這兩個條件的時候,Redis 就使用dict來存儲集合中的數據。

hashtable在上面介紹過了,我們就只介紹inset。

inset結構體

 
typedef struct intset {
    uint32_t encoding;
    // length就是數組的實際長度
    uint32_t length;
    // contents 數組是實際保存元素的地方,數組中的元素有以下兩個特性:
    // 1.沒有重復元素
    // 2.元素在數組中從小到大排列
    int8_t contents[];
} intset;

// encoding 的值可以是以下三個常量的其中一個
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

inset的查詢

intset是一個有序集合,查找元素的復雜度為O(logN)(采用二分法),但插入時不一定為O(logN),因為有可能涉及到升級操作。比如當集合里全是int16_t型的整數,這時要插入一個int32_t,那么為了維持集合中數據類型的一致,那么所有的數據都會被轉換成int32_t類型,涉及到內存的重新分配,這時插入的復雜度就為O(N)了。是intset不支持降級操作。

**inset是有序不要和我們zset搞混,zset是設置一個score來進行排序,而inset這里只是單純的對整數進行升序而已。

7.Zset數據結構

Zset有序集合和set集合有著必然的聯系,他保留了集合不能有重復成員的特性,但不同的是,有序集合中的元素是可以排序的,但是它和列表的使用索引下標作為排序依據不同的是,它給每個元素設置一個分數,作為排序的依據。

zet的底層編碼有兩種數據結構,一個ziplist,一個是skiplist。

Zset也使用了ziplist做了排序,所以下面講一下ziplist如何做排序。

ziplist做排序

每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員(member),而第二個元素則保存元素的分值(score)。

存儲結構圖如下一目了然:

skiplist跳表

結構體如下,skiplist是與dict結合來使用的,這個結構比較復雜。

 
/*
 * 跳躍表
 */
typedef struct zskiplist {
    // 頭節點,尾節點
    struct zskiplistNode *header, *tail;
    // 節點數量
    unsigned long length;
    // 目前表內節點的最大層數
    int level;
} zskiplist;

/*
 * 跳躍表節點
 */
typedef struct zskiplistNode {
    // member 對象
    robj *obj;
    // 分值
    double score;
    // 后退指針
    struct zskiplistNode *backward;
    // 層
    struct zskiplistLevel {
        // 前進指針
        struct zskiplistNode *forward;
        // 這個層跨越的節點數量
        unsigned int span;
    } level[];
} zskiplistNode;

跳表是什么?

我們先看下鏈表:

如果想查找到node5需要從node1查到node5,查詢耗時。

但如果在node上加上索引:

這樣通過索引就能直接從node1查找到node5。

redis跳躍表

讓我們再看下redis的跳表結構(圖太復雜,直接從網上找了張圖說明):

  • header:指向跳躍表的表頭節點,通過這個指針程序定位表頭節點的時間復雜度就為O(1);
  • tail:指向跳躍表的表尾節點,通過這個指針程序定位表尾節點的時間復雜度就為O(1);
  • level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內),通過這個屬性可以再O(1)的時間復雜度內獲取層高最好的節點的層數;
  • length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內),通過這個屬性,程序可以再O(1)的時間復雜度內返回跳躍表的長度。

結構右方的是四個 zskiplistNode結構,該結構包含以下屬性:

  • 層(level):

節點中用L1、L2、L3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,以此類推。

每個層都帶有兩個屬性:前進指針和跨度。前進指針用于訪問位于表尾方向的其他節點,而跨度則記錄了前進指針所指向節點和當前節點的距離(跨度越大、距離越遠)。在上圖中,連線上帶有數字的箭頭就代表前進指針,而那個數字就是跨度。當程序從表頭向表尾進行遍歷時,訪問會沿著層的前進指針進行。

每次創建一個新跳躍表節點的時候,程序都根據冪次定律(powerlaw,越大的數出現的概率越小)隨機生成一個介于1和32之間的值作為level數組的大小,這個大小就是層的“高度”。

  • 后退(backward)指針:

節點中用BW字樣標記節點的后退指針,它指向位于當前節點的前一個節點。后退指針在程序從表尾向表頭遍歷時使用。與前進指針所不同的是每個節點只有一個后退指針,因此每次只能后退一個節點。

  • 分值(score):

各個節點中的1.0、2.0和3.0是節點所保存的分值。在跳躍表中,節點按各自所保存的分值從小到大排列。

  • 成員對象(oj):

各個節點中的o1、o2和o3是節點所保存的成員對象。在同一個跳躍表中,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分值卻可以是相同的:分值相同的節點將按照成員對象在字典序中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭的方向),而成員對象較大的節點則會排在后面(靠近表尾的方向)。

五、三大特殊數據類型

1.geospatial(地理位置)

 
geospatial將指定的地理空間位置(緯度、經度、名稱)添加到指定的key中。
  這些數據將會存儲到sorted set這樣的目的是為了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令對數據進行半徑查詢等操作。
sorted set使用一種稱為Geohash的技術進行填充。經度和緯度的位是交錯的,以形成一個獨特的52位整數。
  sorted set的double score可以代表一個52位的整數,而不會失去精度。(有興趣的同學可以學習一下Geohash技術,使用二分法構建唯一的二進制串)
有效的經度是-180度到180度
  有效的緯度是-85.05112878度到85.05112878度

應用場景

  • 查看附近的人
  • 微信位置共享
  • 地圖上直線距離的展示

2.Hyperloglog(基數)

什么是基數? 不重復的元素。

 
hyperloglog 是用來做基數統計的,其優點是:輸入的提及無論多么大,hyperloglog使用的空間總是固定的12KB ,利用12KB,它可以計算2^64個不同元素的基數!非常節省空間!但缺點是估算的值,可能存在誤差

應用場景

網頁統計UV (瀏覽用戶數量,同一天同一個ip多次訪問算一次訪問,目的是計數,而不是保存用戶)。

傳統的方式,set保存用戶的id,可以統計set中元素數量作為標準判斷。

但如果這種方式保存大量用戶id,會占用大量內存,我們的目的是為了計數,而不是去保存id。

3.Bitmaps(位存儲)

 
Redis提供的Bitmaps這個“數據結構”可以實現對位的操作。Bitmaps本身不是一種數據結構,實際上就是字符串,但是它可以對字符串的位進行操作。
可以把Bitmaps想象成一個以位為單位數組,數組中的每個單元只能存0或者1,數組的下標在bitmaps中叫做偏移量。單個bitmaps的最大長度是512MB,即2^32個比特位。

應用場景

兩種狀態的統計都可以使用bitmaps,例如:統計用戶活躍與非活躍數量、登錄與非登錄、上班打卡等等。

六、Redis事務

事務本質:一組命令的集合。

1.數據庫事務與redis事務

數據庫的事務

數據庫事務通過ACID(原子性、一致性、隔離性、持久性)來保證。

數據庫中除查詢操作以外,插入(Insert)、刪除(Delete)和更新(Update)這三種操作都會對數據造成影響,因為事務處理能夠保證一系列操作可以完全地執行或者完全不執行,因此在一個事務被提交以后,該事務中的任何一條SQL語句在被執行的時候,都會生成一條撤銷日志(Undo Log)。

Redis事務

redis事務提供了一種“將多個命令打包, 然后一次性、按順序地執行”的機制, 并且事務在執行的期間不會主動中斷 —— 服務器在執行完事務中的所有命令之后, 才會繼續處理其他客戶端的其他命令。

Redis中一個事務從開始到執行會經歷開始事務(muiti)、命令入隊和執行事務(exec)三個階段,事務中的命令在加入時都沒有被執行,直到提交時才會開始執行(Exec)一次性完成。

一組命令中存在兩種錯誤不同處理方式:

  • 代碼語法錯誤(編譯時異常)所有命令都不執行。
  • 代碼邏輯錯誤(運行時錯誤),其他命令可以正常執行 (該點不保證事務的原子性)。

為什么redis不支持回滾來保證原子性?

這種做法的優點:

  • Redis 命令只會因為錯誤的語法而失敗(并且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
  • 因為不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。

**鑒于沒有任何機制能避免程序員自己造成的錯誤, 并且這類錯誤通常不會在生產環境中出現, 所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。

事務監控

悲觀鎖:認為什么時候都會出現問題,無論做什么操作都會加鎖。

樂觀鎖:認為什么時候都不會出現問題,所以不會上鎖!更新數據的時候去判斷一下,在此期間是否有人修改過這個數據。

使用cas實現樂觀鎖

redis使用watch key監控指定數據,相當于加樂觀鎖。

watch保證事務只能在所有被監視鍵都沒有被修改的前提下執行, 如果這個前提不能滿足的話,事務就不會被執行。

watch執行流程:

七、Redis持久化

Redis是一種內存型數據庫,一旦服務器進程退出,數據庫的數據就會丟失,為了解決這個問題Redis供了兩種持久化的方案,將內存中的數據保存到磁盤中,避免數據的丟失兩種持久化方式:快照(RDB文件)和追加式文件(AOF文件),下面分別為大家介紹兩種方式的原理。

  • RDB持久化方式會在一個特定的間隔保存那個時間點的數據快照。
  • AOF持久化方式則會記錄每一個服務器收到的寫操作。在服務啟動時,這些記錄的操作會逐條執行從而重建出原來的數據。寫操作命令記錄的格式跟Redis協議一致,以追加的方式進行保存。
  • Redis的持久化是可以禁用的,就是說你可以讓數據的生命周期只存在于服務器的運行時間里。
  • 兩種方式的持久化是可以同時存在的,但是當Redis重啟時,AOF文件會被優先用于重建數據。

1 RDB持久化

RDB持久化產生的文件是一個經過壓縮的二進制文件,這個文件可以被保存到硬盤中,可以通過這個文件還原數據庫的狀態,它可以手動執行,也可以在redis.conf配置文件中配置,定時執行。

工作原理

在進行RDB時,redis的主進程不會做io操作,會fork一個子進程來完成該操作:

Redis 調用forks。同時擁有父進程和子進程。

子進程將數據集寫入到一個臨時 RDB 文件中。

當子進程完成對新 RDB 文件的寫入時,Redis 用新 RDB 文件替換原來的 RDB 文件,并刪除舊的 RDB 文件。

這種工作方式使得 Redis 可以從寫時復制(copy-on-write)機制中獲益(因為是使用子進程進行寫操作,而父進程依然可以接收來自客戶端的請求)。

觸發機制

在Redis中RDB持久化的觸發分為兩種:自己手動觸發與自動觸發。

主動觸發

  • save命令是同步的命令,會占用主進程,會造成阻塞,阻塞所有客戶端的請求。
  • bgsave。

bgsave是異步進行,進行持久化的時候,redis還可以將繼續響應客戶端請求。

bgsave和save對比

命令

save

bgsave

IO類型

同步

異步

阻塞

是(阻塞發生在fock(),通常非常快)

復雜度

O(n)

O(n)

優點

不會消耗額外的內存

不阻塞客戶端命令

缺點

阻塞客戶端命令

需要fock子進程,消耗內存

自動觸發

(1)save自動觸發配置,見下面配置,滿足m秒內修改n次key,觸發rdb。

 
# 時間策略   save m n m秒內修改n次key,觸發rdb
save 900 1
save 300 10
save 60 10000

# 文件名稱
dbfilename dump.rdb

# 文件保存路徑
dir /home/work/app/redis/data/

# 如果持久化出錯,主進程是否停止寫入
stop-writes-on-bgsave-error yes

# 是否壓縮
rdbcompression yes

# 導入時是否檢查
rdbchecksum yes

(2)從節點全量復制時,主節點發送rdb文件給從節點完成復制操作,主節點會觸發bgsave命令;

(3)執行flushall命令,會觸發rdb

(4)退出redis,且沒有開啟aof時

優點:

  • RDB 的內容為二進制的數據,占用內存更小,更緊湊,更適合做為備份文件;
  • RDB 對災難恢復非常有用,它是一個緊湊的文件,可以更快的傳輸到遠程服務器進行 Redis 服務恢復;
  • RDB 可以更大程度的提高 Redis 的運行速度,因為每次持久化時 Redis 主進程都會 fork() 一個子進程,進行數據持久化到磁盤,Redis 主進程并不會執行磁盤 I/O 等操作;
  • 與 AOF 格式的文件相比,RDB 文件可以更快的重啟。

缺點:

  • 因為 RDB 只能保存某個時間間隔的數據,如果中途 Redis 服務被意外終止了,則會丟失一段時間內的 Redis 數據。
  • RDB 需要經常 fork() 才能使用子進程將其持久化在磁盤上。如果數據集很大,fork() 可能很耗時,并且如果數據集很大且 CPU 性能不佳,則可能導致 Redis 停止為客戶端服務幾毫秒甚至一秒鐘。

2.AOF(Append Only File)

以日志的形式來記錄每個寫的操作,將Redis執行過的所有指令記錄下來(讀操作不記錄),只許追加文件但不可以改寫文件,redis啟動之初會讀取該文件重新構建數據,換言之,redis重啟的話就根據日志文件的內容將寫指令從前到后執行一次以完成數據的恢復工作。

AOF配置項

 
# 默認不開啟aof  而是使用rdb的方式
appendonly no

# 默認文件名
appendfilename "appendonly.aof"

# 每次修改都會sync 消耗性能
# appendfsync always
# 每秒執行一次 sync 可能會丟失這一秒的數據
appendfsync everysec
# 不執行 sync ,這時候操作系統自己同步數據,速度最快
# appendfsync no 

AOF的整個流程大體來看可以分為兩步,一步是命令的實時寫入(如果是appendfsync everysec 配置,會有1s損耗),第二步是對aof文件的重寫。

AOF 重寫機制

隨著Redis的運行,AOF的日志會越來越長,如果實例宕機重啟,那么重放整個AOF將會變得十分耗時,而在日志記錄中,又有很多無意義的記錄,比如我現在將一個數據 incr一千次,那么就不需要去記錄這1000次修改,只需要記錄最后的值即可。所以就需要進行 AOF 重寫。

Redis 提供了bgrewriteaof指令用于對AOF日志進行重寫,該指令運行時會開辟一個子進程對內存進行遍歷,然后將其轉換為一系列的 Redis 的操作指令,再序列化到一個日志文件中。完成后再替換原有的AOF文件,至此完成。

同樣的也可以在redis.config中對重寫機制的觸發進行配置:

通過將no-appendfsync-on-rewrite設置為yes,開啟重寫機制;auto-aof-rewrite-percentage 100意為比上次從寫后文件大小增長了100%再次觸發重寫;

auto-aof-rewrite-min-size 64mb意為當文件至少要達到64mb才會觸發制動重寫。

觸發方式

  • 手動觸發:bgrewriteaof。
  • 自動觸發 就是根據配置規則來觸發,當然自動觸發的整體時間還跟Redis的定時任務頻率有關系。

優點

  • 數據安全,aof 持久化可以配置 appendfsync 屬性,有 always,每進行一次 命令操作就記錄到 aof 文件中一次。
  • 通過 append 模式寫文件,即使中途服務器宕機,可以通過 redis-check-aof 工具解決數據一致性問題。
  • AOF 機制的 rewrite 模式。AOF 文件沒被 rewrite 之前(文件過大時會對命令 進行合并重寫),可以刪除其中的某些命令(比如誤操作的 flushall))。

缺點

  • AOF 文件比 RDB 文件大,且恢復速度慢。
  • 數據集大的時候,比 rdb 啟動效率低。

3.rdb與aof對比

比較項

RDB

AOF

啟動優先級

體積

恢復速度

數據安全性

丟數據

根據策略決定

八、發布與訂閱

redis發布與訂閱是一種消息通信的模式:發送者(pub)發送消息,訂閱者(sub)接收消息。

redis通過PUBLISH和SUBSCRIBE等命令實現了訂閱與發布模式,這個功能提供兩種信息機制,分別是訂閱/發布到頻道、訂閱/發布到模式的客戶端。

1.頻道(channel)

訂閱

發布

完整流程

發布者發布消息

發布者向頻道channel:1發布消息hi。

 
127.0.0.1:6379> publish channel:1 hi
(integer) 1

訂閱者訂閱消息

 
127.0.0.1:6379> subscribe channel:1
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 消息類型
2) "channel:1" // 頻道
3) "hi" // 消息內容

執行subscribe后客戶端會進入訂閱狀態,僅可以使subscribe、unsubscribe、psubscribe和punsubscribe這四個屬于"發布/訂閱"之外的命令。

訂閱頻道后的客戶端可能會收到三種消息類型:

  • subscribe。表示訂閱成功的反饋信息。第二個值是訂閱成功的頻道名稱,第三個是當前客戶端訂閱的頻道數量。
  • message。表示接收到的消息,第二個值表示產生消息的頻道名稱,第三個值是消息的內容。
  • unsubscribe。表示成功取消訂閱某個頻道。第二個值是對應的頻道名稱,第三個值是當前客戶端訂閱的頻道數量,當此值為0時客戶端會退出訂閱狀態,之后就可以執行其他非"發布/訂閱"模式的命令了。

數據結構

基于頻道的發布訂閱模式是通過字典數據類型實現的。

 
struct redisServer {
    // ...
    dict *pubsub_channels;
    // ...
};

其中,字典的鍵為正在被訂閱的頻道, 而字典的值則是一個鏈表, 鏈表中保存了所有訂閱這個頻道的客戶端。

訂閱

當使用subscribe訂閱時,在字典中找到頻道key(如沒有則創建),并將訂閱的client關聯在鏈表后面。

當client 10執行subscribe channel1 channel2 channel3時,會將client 10分別加到 channel1 channel2 channel3關聯的鏈表尾部。

發布

發布時,根據key,找到字典匯總key的地址,然后將msg發送到關聯的鏈表每一臺機器。

退訂

遍歷關聯的鏈表,將指定的地址刪除即可。

2.模式(pattern)

pattern使用了通配符的方式來訂閱。

通配符中?表示1個占位符,*表示任意個占位符(包括0),?*表示1個以上占位符。

所以當使用 publish命令發送信息到某個頻道時, 不僅所有訂閱該頻道的客戶端會收到信息, 如果有某個/某些模式和這個頻道匹配的話, 那么所有訂閱這個/這些頻道的客戶端也同樣會收到信息。

訂閱發布完整流程

發布者發布消息

 
127.0.0.1:6379> publish b m1
(integer) 1
127.0.0.1:6379> publish b1 m1
(integer) 1
127.0.0.1:6379> publish b11 m1
(integer) 1

訂閱者訂閱消息

 
127.0.0.1:6379> psubscribe b*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "b*"
3) (integer) 3
1) "pmessage"
2) "b*"
3) "b"
4) "m1"
1) "pmessage"
2) "b*"
3) "b1"
4) "m1"
1) "pmessage"
2) "b*"
3) "b11"
4) "m1"

數據結構

pattern屬性是一個鏈表,鏈表中保存著所有和模式相關的信息。

 
struct redisServer {
    // ...
    list *pubsub_patterns;
    // ...
};
// 鏈表中的每一個節點結構如下,保存著客戶端與模式信息
typedef struct pubsubPattern {
    redisClient *client;
    robj *pattern;
} pubsubPattern;

數據結構圖如下:

訂閱

當有信的訂閱時,會將訂閱的客戶端和模式信息添加到鏈表后面。

發布

當發布者發布消息時,首先會發送到對應的頻道上,在遍歷模式列表,根據key匹配模式,匹配成功將消息發給對應的訂閱者。

完成的發布偽代碼如下:

def PUBLISH(channel, message):
    # 遍歷所有訂閱頻道 channel 的客戶端
    for client in server.pubsub_channels[channel]:
        # 將信息發送給它們
        send_message(client, message)
    # 取出所有模式,以及訂閱模式的客戶端
    for pattern, client in server.pubsub_patterns:
        # 如果 channel 和模式匹配
        if match(channel, pattern):
            # 那么也將信息發給訂閱這個模式的客戶端
            send_message(client, message)

退訂

使用punsubscribe,可以將訂閱者退訂,將改客戶端移除出鏈表。

九、主從復制

什么是主從復制

主從復制,是指將一臺Redis服務器的數據,復制到其他的Redis服務器。
前者稱為主節點(master),后者稱為從節點(slave);數據的復制是單向的,只能由主節點到從節點
默認情況下,每臺redis服務器都是主節點;且一個主節點可以有多個從節點(或者沒有),但一個從節點只有一個主

主從復制的作用主要包括:

  • 數據冗余:主從復制實現了數據的熱備份,是持久化之外的一種數據冗余方式。
  • 故障恢復:當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復;實際上是一種服務的冗余。
  • 負載均衡:在主從復制的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫Redis數據時應用連接主節點,讀Redis數據時應用連接從節點),分擔服務器負載;尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高Redis服務器的并發量。
  • 高可用基石:除了上述作用以外,主從復制還是哨兵和集群能夠實施的基礎,因此說主從復制是Redis高可用的基礎。

主從庫采用的是讀寫分離的方式:

1.原理

分為全量復制與增量復制。

全量復制:發生在第一次復制時。

增量復制:只會把主從庫網絡斷連期間主庫收到的命令,同步給從庫。

2.全量復制的三個階段

第一階段是主從庫間建立連接、協商同步的過程。

主要是為全量復制做準備。從庫和主庫建立起連接,并告訴主庫即將進行同步,主庫確認回復后,主從庫間就可以開始同步了。

具體來說,從庫給主庫發送 psync 命令,表示要進行數據同步,主庫根據這個命令的參數來啟動復制。psync 命令包含了主庫的 runID 和復制進度 offset 兩個參數。runID,是每個 Redis 實例啟動時都會自動生成的一個隨機 ID,用來唯一標記這個實例。當從庫和主庫第一次復制時,因為不知道主庫的 runID,所以將 runID 設為“?”。offset,此時設為 -1,表示第一次復制。主庫收到 psync 命令后,會用 FULLRESYNC 響應命令帶上兩個參數:主庫 runID 和主庫目前的復制進度 offset,返回給從庫。從庫收到響應后,會記錄下這兩個參數。這里有個地方需要注意,FULLRESYNC 響應表示第一次復制采用的全量復制,也就是說,主庫會把當前所有的數據都復制給從庫。

第二階段,主庫將所有數據同步給從庫。

從庫收到數據后,在本地完成數據加載。這個過程依賴于內存快照生成的 RDB 文件。

具體來說,主庫執行 bgsave 命令,生成 RDB 文件,接著將文件發給從庫。從庫接收到 RDB 文件后,會先清空當前數據庫,然后加載 RDB 文件。這是因為從庫在通過 replicaof 命令開始和主庫同步前,可能保存了其他數據。為了避免之前數據的影響,從庫需要先把當前數據庫清空。在主庫將數據同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則,Redis 的服務就被中斷了。但是,這些請求中的寫操作并沒有記錄到剛剛生成的 RDB 文件中。為了保證主從庫的數據一致性,主庫會在內存中用專門的 replication buffer,記錄 RDB 文件生成后收到的所有寫操作。

第三個階段,主庫會把第二階段執行過程中新收到的寫命令,再發送給從庫。

具體的操作是,當主庫完成 RDB 文件發送后,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。

十、哨兵機制

哨兵的核心功能是主節點的自動故障轉移。

下圖是一個典型的哨兵集群監控的邏輯圖:

Redis Sentinel包含了若個Sentinel節點,這樣做也帶來了兩個好處:

  • 對于節點的故障判斷是由多個Sentinel節點共同完成,這樣可以有效地防止誤判。
  • 即使個別Sentinel節點不可用,整個Sentinel集群依然是可用的。

哨兵實現了一下功能:

  • 監控:每個Sentinel節點會對數據節點(Redis master/slave 節點)和其余Sentinel節點進行監控。
  • 通知:Sentinel節點會將故障轉移的結果通知給應用方
  • 故障轉移:實現slave晉升為master,并維護后續正確的主從關系。
  • 配置中心:在Redis Sentinel模式中,客戶端在初始化的時候連接的是Sentinel節點集合,從中獲取主節點信息。

其中,監控和自動故障轉移功能,使得哨兵可以及時發現主節點故障并完成轉移;而配置中心和通知功能,則需要在與客戶端的交互中才能體現。

1.原理

監控

Sentinel節點需要監控master、slave以及其它Sentinel節點的狀態。這一過程是通過Redis的pub/sub系統實現的。Redis Sentinel一共有三個定時監控任務,完成對各個節點發現和監控:

  • 監控主從拓撲信息:每隔10秒,每個Sentinel節點,會向master和slave發送INFO命令獲取最新的拓撲結構。
  • Sentinel節點信息交換:每隔2秒,每個Sentinel節點,會向Redis數據節點的__sentinel__:hello頻道上,發送自身的信息,以及對主節點的判斷信息。這樣,Sentinel節點之間就可以交換信息。
  • 節點狀態監控:每隔1秒,每個Sentinel節點,會向master、slave、其余Sentinel節點發送PING命令做心跳檢測,來確認這些節點當前是否可達。

主觀/客觀下線

主觀下線

每個Sentinel節點,每隔1秒會對數據節點發送ping命令做心跳檢測,當這些節點超過down-after-milliseconds沒有進行有效回復時,Sentinel節點會對該節點做失敗判定,這個行為叫做主觀下線。

客觀下線

客觀下線,是指當大多數Sentinel節點,都認為master節點宕機了,那么這個判定就是客觀的,叫做客觀下線。

那么這個大多數是指多少呢?這其實就是分布式協調中的quorum判定了,大多數就是過半數,比如哨兵數量是5,那么大多數就是5/2+1=3個,哨兵數量是10大多數就是10/2+1=6個。

注:Sentinel節點的數量至少為3個,否則不滿足quorum判定條件。

哨兵選舉

如果發生了客觀下線,那么哨兵節點會選舉出一個Leader來進行實際的故障轉移工作。Redis使用了Raft算法來實現哨兵領導者選舉,大致思路如下:

  • 每個Sentinel節點都有資格成為領導者,當它主觀認為某個數據節點宕機后,會向其他Sentinel節點發送sentinel is-master-down-by-addr命令,要求自己成為領導者;
  • 收到命令的Sentinel節點,如果沒有同意過其他Sentinel節點的sentinelis-master-down-by-addr命令,將同意該請求,否則拒絕(每個Sentinel節點只有1票);
  • 如果該Sentinel節點發現自己的票數已經大于等于MAX(quorum, num(sentinels)/2+1),那么它將成為領導者;
  • 如果此過程沒有選舉出領導者,將進入下一次選舉。

故障轉移

選舉出的Leader Sentinel節點將負責故障轉移,也就是進行master/slave節點的主從切換。故障轉移,首先要從slave節點中篩選出一個作為新的master,主要考慮以下slave信息:

  • 跟master斷開連接的時長:如果一個slave跟master的斷開連接時長已經超過了down-after-milliseconds的10倍,外加master宕機的時長,那么該slave就被認為不適合選舉為master;
  • slave的優先級配置:slave priority參數值越小,優先級就越高;
  • 復制offset:當優先級相同時,哪個slave復制了越多的數據(offset越靠后),優先級越高;
  • run id:如果offset和優先級都相同,則哪個slave的run id越小,優先級越高。

接著,篩選完slave后, 會對它執行slaveof no one命令,讓其成為主節點。

最后,Sentinel領導者節點會向剩余的slave節點發送命令,讓它們成為新的master節點的從節點,復制規則與parallel-syncs參數有關。

Sentinel節點集合會將原來的master節點更新為slave節點,并保持著對其關注,當其恢復后命令它去復制新的主節點。

注:Leader Sentinel節點,會從新的master節點那里得到一個configuration epoch,本質是個version版本號,每次主從切換的version號都必須是唯一的。其他的哨兵都是根據version來更新自己的master配置。

十一、緩存穿透、擊穿、雪崩

1.緩存穿透

  • 問題來源

緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷發起請求。由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。

如發起為id為“-1”的數據或id為特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。

  • 解決方案

接口層增加校驗,如用戶鑒權校驗,id做基礎校驗,id<=0的直接攔截;

從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設置短點,如30秒(設置太長會導致正常情況也沒法使用)。這樣可以防止攻擊用戶反復用同一個id暴力攻擊。

布隆過濾器。類似于一個hash set,用于快速判某個元素是否存在于集合中,其典型的應用場景就是快速判斷一個key是否存在于某容器,不存在就直接返回。布隆過濾器的關鍵就在于hash算法和容器大小。

2.緩存擊穿

  • 問題來源

緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由于并發用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力。

解決方案

設置熱點數據永遠不過期。

接口限流與熔斷,降級。重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時要降級準備,當接口中的某些服務不可用時候,進行熔斷,失敗快速返回機制。

加互斥鎖

3.緩存雪崩

  • 問題來源

緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機。和緩存擊穿不同的是,緩存擊穿指并發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。

  • 解決方案

緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。

如果緩存數據庫是分布式部署,將熱點數據均勻分布在不同的緩存數據庫中。

設置熱點數據永遠不過期。

分享到:
標簽:Redis
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定