Redis
參考博客
https://www.cnblogs.com/beiluowuzheng/
https://www.cnblogs.com/hunternet/
如有侵權,請聯系我洗掉,謝謝!
目錄
-
一、什么是redis
-
二、資料結構
-
1.1 SDS,簡單動態字串
-
1.1.1 SDS底層結構
-
1.1.2 SDS記憶體重分配
-
1.1.3 二進制安全
-
1.1.4 為什么使用SDS
-
1.1.5 SDS API
-
1.1.6 Redis3.2之后的SDS
-
Redis3.2 之后的SDS共有五個結構體
-
Redis是如何創建SDS物件
-
為什么SDS不用記憶體對齊
-
-
-
1.2 鏈表
-
1.2.1 list底層結構
-
1.2.2 Redis的鏈表實作的特性
-
1.2.3 雙向無環鏈表在Redis中的使用
-
-
1.3 字典
- 1.3.1 Redis如何解決散列沖突
-
1.4 跳躍表
-
1.5 整數集合
- 1.5.1 整數集合升級
-
1.6 壓縮串列
-
1.6.1 Redis壓縮串列的構成
-
1.6.2 Redis壓縮串列節點的構成
-
-
1.7 快速串列
- 1.7.1 基本結構
-
-
三、Redis物件系統
-
3.1 Redis 物件型別和編碼
-
3.2 優勢
-
-
四、Redis物件型別
-
4.1 字串
-
4.1.1 內部實作
-
4.1.2 常用操作
-
4.1.3 應用場景
-
4.1.3.1 作為快取層
-
4.1.3.2 計數器\限速器\分布式系統ID
-
4.1.3.3分布式系統共享session
-
-
-
4.2 哈希
-
4.2.1 內部實作
-
4.2.2 應用場景
-
-
4.3 串列List
-
4.3.1 內部實作
-
4.3.2 應用場景
-
-
4.4 集合set
-
4.4.1 內部實作
-
4.4.2 使用場景
-
-
4.5 有序集合 sorted set
-
4.5.1 內部實作
-
4.5.2 使用場景
-
-
-
五、型別檢查與命令多型
-
5.1 型別檢查和實作
-
5.2 多型命令的實作
-
-
六、記憶體回收
-
七 物件共享
-
八、物件的空轉時長
-
九、資料淘汰策略
-
9.1 過期淘汰策略
-
9.1.3 惰性洗掉
-
9.1.4 定期洗掉
-
-
9.2 快取淘汰策略
-
9.1.2 Redis的LRU演算法 (Least recently used) 最近最少使用
-
9.1.3 LFU (Least frequently used) 最不經常使用
-
-
-
十、持久化
-
10.1 RDB持久化
-
10.1.1 是什么
-
10.1.2 持久化流程
-
10.1.3 save vs bgsave
-
10.1.4 BGSAVE命令執行時的服務器狀態
-
10.1.5 自動間隔性保存
-
10.1.6 優缺點
-
-
10.2 AOF持久化
-
10.2.1 是什么
-
10.2.2 持久化流程
-
10.2.3 重寫流程
-
10.2.4 優缺點
-
-
-
十一、檔案事件
- 11.1檔案事件處理器的構成
-
十二、主從復制
- 12.1 新版復制功能的實作
-
十三、Redis哨兵高可用框架
-
十四 集群
-
十五 redis參考問題解決
-
15.1 快取穿透
-
15.2 快取擊穿
-
15.3 快取雪崩
-
一、什么是redis
Redis是一個使用c語言開發的資料庫,與傳統資料庫不同的是,Redis的資料是存盤在記憶體中的,讀寫速度非常快,因此被廣泛應用于快取方向,另外,Redis除了做快取之外,Redis也經常用來做分布式鎖,甚至是訊息佇列,
Redis 提供了多種資料型別來?持不同的業務場景,包括String,list,set,hash,sortedset,Redis 還?持事務 、持久化、Lua 腳本、多種集群?案
二、資料結構
1.1 SDS,簡單動態字串
Redis默認并未直接使用C字串(C字串,僅僅作為字串字面量,用在一些無需對字串進行修改的地方,如列印日志,),Redis使用Struct的形式構造了一個SDS的抽象型別,當Redis需要一個可以被修改的字串時,就會使用SDS來表示,
-
在Redis資料庫里,包含字串值的鍵值對都是由SDS實作的
-
Redis中所有的鍵都是由字串物件實作的即底層是由SDS實作,Redis中所有的值物件中包含的字串物件底層也是由SDS實作,
1.1.1 SDS底層結構

struct sdshdr{
//位元組陣列用于保存字串 sds遵循了c字串以空字符結尾的慣例目的是為了重用c字串函式庫里的函式
char buf[];
//int 記錄buf陣列中未使用位元組的數量 如上圖free為0代表未使用位元組的數量為0
int free;
//int 記錄buf陣列中已使用位元組的數量即sds的長度 如上圖len為5代表未使用位元組的數量為5
int len;
}
1.1.2 SDS記憶體重分配
在C語言中,如果對字串進行修改,就需要面臨記憶體重分配的情況,C字串是有一個長度為n+1的字符陣列,C字串為靜態陣列,初始化后的陣列長度不會改變,如果像增加字串的長度,就需要重新分配一塊更長的記憶體空間,否者會產生記憶體溢位,
-
擴容機制(SDS最大長度為512M):空間預分配
-
如果修改后的len長度小于1M,擴容為原來的一倍:n*2+1;
-
如果修改后的len超過1M,只擴容1M的空間:n+1M+1,
-
-
釋放機制:惰性空間釋放
當字串縮短時,Redis首先使用free屬性記錄,等待將來使用,
1.1.3 二進制安全
為了時SDS能夠保存諸如圖片、音頻、視頻等二進制資料,Redis API使用len屬性來判斷字串的長度,而不是使用空字串作為判斷依據,因此SDS是二進制安全的,但SDS依然遵循C字串的慣例使用空字串結尾,目的是能夠復用一部分的<string.h>庫中的函式,
1.1.4 為什么使用SDS
-
杜絕緩沖區溢位
-
減少字串操作中的記憶體重分配次數
-
二進制安全
-
由于SDS遵循以空字符結尾的慣例,因此兼容部門C字串函式
1.1.5 SDS API
| sdsnew | 創建一個包含給定C字串的SDS | O(N) ,N 為給定C字串的長度 |
| sdsempty | 創建一個不包含任何內容的空SDS | O(1) |
| sdsfree | 釋放給定的SDS | O(1) |
| sdslen | 回傳SDS的已使用空間位元組數 | 這個值可以通過讀取SDS的len屬性來直接獲得,復雜度為O(1) |
| sdsavail | 回傳SDS的未使用空間位元組數 | 這個值可以通過讀取SDS的free屬性來直接獲得,復雜度為 O(1) |
| sdsdup | 創建一個給定SDS的副本(copy) | O(N),N為給定SDS的長度 |
| sdsclear | 清空SDS保存的字串內容 | 因為惰性空間釋放策略,復雜度為O(1) |
| sdscat | 將給定C字串拼接到SDS字串的末尾 | O(N),N為被拼接C字串的長度 |
| sdscatsds | 將給定SDS字串拼接到另一個SDS字串的末尾 | O(N),N為被拼接SDS字串的長度 |
| sdscpy | 將給定的C字串復制到SDS里面,覆寫SDS原有的字串 | O(N),N為被復制C字串的長度 |
| sdsgrowzero | 用空字符將SDS擴展至給定長度 | O(N),N為擴展新增的位元組數 |
| sdsrange | 保留SDS給定區間內的資料,不在區間內的資料會被覆寫或清除 | O(N),N為被保留資料的位元組數 |
| sdstrim | 接受一個SDS和一個C字串作為引數,從SDS左右兩端分別移除所有在C字串中出現過的字符 | O(M*N),M為SDS的長度,N為給定C字串的長度 |
| sdscmp | 對比兩個SDS字串是否相同 | O(N),N為兩個SDS中較短的那個SDS的長度 |
1.1.6 Redis3.2之后的SDS
Redis3.2 之后的SDS共有五個結構體
typedef char *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[];// buf[0]: z: 0101001
};
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
-
Redis會根據我們設定的字串長度,選擇不同的SDS結構體來存盤,
-
除了sdshdr5,其他4個結構體都有len和alloc欄位,這里len依舊代表buf存盤的內容長度,而alloc則代表buf的總容量,如果我們要計算SDS剩余可存盤位元組,則用alloc-len,即為3.2前SDS的free欄位,
-
flags的最低3位表示SDS的型別,如果flags的值為SDS_TYPE_5(即為:0),則代表這個SDS的型別為sdshdr5、如果flags的值為SDS_TYPE_8(即為:1),則代表這個這個SDS的型別為sdshdr8,以此類推,如果flags的值為SDS_TYPE_64(即為:4,4的二進制表示為100),則代表這個SDS型別為sdshdr64,
-
這里會有人奇怪,為何每個SDS型別都需要一個flags來單獨表示其SDS的型別,難道結構體本身不是已經代表其型別了嗎?這是因為在Redis中很多時候傳遞SDS指標并不是以SDS物件的起始地址來傳遞的,而是以buf的起始地址來傳遞SDS物件,這里我們注意到每個SDS結構體的最后一個欄位都是char buf[],這是一個沒有指定長度的的字符陣列,這是C語言中定義字符陣列的一種特殊寫法,稱為柔性陣列(flexible array member),只能定義在結構體的最后一個欄位上,這個欄位只起到標記的作用,表示在flags欄位后面就是一個字符陣列,或者說,它指明了緊跟在flags欄位后面的這個字符陣列在結構體中的偏移位置,而程式在為結構體分配的記憶體的時候,并不會為柔性陣列計算需要占用的空間,如果計算sizeof(struct sdshdr16)的值,那么結果是5個位元組,不會把buf欄位的長度計算進去,當然,我們為一個SDS分配記憶體,并非要一板一眼只分配5個位元組,那么這5個位元組的記憶體空間,僅僅能存盤sdshdr16除buf外的欄位,如果我們申請一塊16位元組的記憶體分配給sdshdr16物件,那么除去5個位元組分配給sdshdr16除buf外的欄位,我們還有11個位元組來存盤字串,如果我們拿到buf的起始地址,flags即為buf[-1],拿到flags后,我們便可以計算這個buf具體的SDS型別,繼而可以獲得len和alloc這兩個欄位,從而計算出這個陣列要讀取多少個位元組才算結束,這個陣列還可以容納多少個位元組的資料,
Redis是如何創建SDS物件
sds sdsnewlen(const void *init, size_t initlen) {
//這個指標會指向SDS起始位置
void *sh;
//sds也是一個指標,這里會指向SDS中buf的起始位置
sds s;
//根據不同的長度回傳對應的SDS型別
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
//如果判斷要創建的SDS型別為5,且字串為空串,則型別替換成SDS8
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
//根據SDS型別獲取記憶體占用大小,以便后續創建記憶體
int hdrlen = sdsHdrSize(type);
//fp用于指向SDS的flag地址
unsigned char *fp; /* flags pointer. */
//申請SDS大小+字串長度+1的記憶體空間,這里+1是為了分配結束符號,C語言用\0表示字串結束
sh = s_malloc(hdrlen+initlen+1);
//判斷記憶體是否申請成功
if (sh == NULL) return NULL;
//判斷是否處于init階段
if (init==SDS_NOINIT)
init = NULL;
//如果不是init階段則將申請來的記憶體清零
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
//將s指向SDS的buf起始地址
s = (char*)sh+hdrlen;
//s指向buf的起始地址,往前一個位元組即指向SDS的flag地址
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
//這里使用了行內方法,宣告一個對應SDS型別的變數sh
//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
SDS_HDR_VAR(16,s);
//初始化len和alloc
sh->len = initlen;
sh->alloc = initlen;
//fp指標指向的記憶體賦值為對應的SDS型別
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
//如果initlen和init都不為空,則將init指向的記憶體拷貝initlen個位元組到buf
if (initlen && init)
memcpy(s, init, initlen);
//分配一個結束符
s[initlen] = '\0';
//回傳buf起始地址
return s;
}
-
Redis在創建SDS物件時,回傳的并不是SDS物件本身,而是buf的起始地址
-
在計算SDS字串長度、計算buf容量、計算buf剩余容量……時,傳入的也是buf的地址
為什么SDS不用記憶體對齊
-
什么是記憶體對齊
- 一個32位的CPU在一個周期內只能讀取32位的資料,而CPU每次讀取資料的起始地址都是4的倍數,如果我們宣告了一個struct A物件,a欄位的地址是0
1,b欄位的地址是14,那么CPU要讀取b欄位,需要先讀03,再讀34,CPU需要讀取兩次才能完整的讀取b欄位的資料,如果我們宣告了一個struct B物件,欄位a的地址是03,欄位b的地址是37,那么CPU就可以在一個周期內完整地讀取b欄位的資料,由此可見,浪費一點記憶體空間,會使得CPU的性能更為高效,
- 一個32位的CPU在一個周期內只能讀取32位的資料,而CPU每次讀取資料的起始地址都是4的倍數,如果我們宣告了一個struct A物件,a欄位的地址是0
-
既然記憶體對齊可以讓CPU更加高效,那么Redis作為非關系型記憶體資料庫又為何放棄記憶體對齊呢?
- 這是因為SDS本身結構的特殊性讓它只能是非記憶體對齊的,不同的SDS型別所需要的位元組填充都不一樣,如果不放棄記憶體對齊,我們很難通過buf的偏移獲取到flags進而推算出SDS的型別,
1.2 鏈表
1.2.1 list底層結構
adlist.h
//鏈表結點
typedef struct listNode {
//前置節點
struct listNode *prev;
//后置節點
struct listNode *next;
//節點的值
void *value;
} listNode;
//list
typedef struct list {
//表頭節點
listNode *head;
//表尾節點
listNode *tail;
//節點值復制函式
void *(*dup)(void *ptr);
//節點值釋放函式
void (*free)(void *ptr);
//節點值對比函式
int (*match)(void *ptr, void *key);
//鏈表所包含的節點數量
unsigned long len;
} list;
- list結構為鏈表提供了表頭指標head、表尾指標tail,以及鏈表長度計數器len,而dup、free和match成員則是用于實作多型鏈表所需的型別特定函式:
1.2.2 Redis的鏈表實作的特性
-
雙端:鏈表節點帶有prev和next指標,獲取某個節點的前置節點和后置節點的復雜度都為O(1)
-
無環:表頭節點的prev指標和表尾節點的next指標都指向NULL,對鏈表的訪問以NULL為終點
-
帶表頭指標和表尾指標:通過list結構的head指標和tail指標,程式獲取鏈表的表頭節點和表尾節點的時間復雜度為O(1)
-
帶鏈表長度計數器:程式使用list結構的len屬性來對list持有的鏈表節點進行計數,程式獲取鏈表長度的時間復雜度為O(1)
-
多型:鏈表節點使用void*指標來保存節點值,并且可以通過list結構的dup、free、match三個屬性為節點值設定型別特定函式,所以鏈表可以用于保存各個不同型別的值
1.2.3 雙向無環鏈表在Redis中的使用
| 操作\時間復雜度 | 陣列 | 單鏈表 | 雙向鏈表 |
|---|---|---|---|
| rpush(從右邊添加元素) | O(1) | O(1) | O(1) |
| lpush(從左邊添加元素) | 0(N) | O(1) | O(1) |
| lpop (從右邊洗掉元素) | O(1) | O(1) | O(1) |
| rpop (從左邊洗掉元素) | O(N) | O(1) | O(1) |
| lindex(獲取指定索引下標的元素) | O(1) | O(N) | O(N) |
| len (獲取長度) | O(N) | O(N) | O(1) |
| linsert(向某個元素前或后插入元素) | O(N) | O(N) | O(1) |
| lrem (洗掉指定元素) | O(N) | O(N) | O(N) |
| lset (修改指定索引下標元素) | O(N) | O(N) | O(N) |
1.3 字典
資料庫與哈希物件的底層實作就是字典,

-
Redis字典使用散串列最為底層實作,一個字典里有一個dicht屬性,ht屬性是一個包含兩個項的陣列,陣列中的每個項都是一個dictht哈希表, 一般情況下,字典只使用ht[0] 哈希表, ht[1]哈希表只會對ht[0]哈希表進行rehash時使用,
-
dictht中存盤著一個哈希表陣列(用一個二級指標指向想哈希表陣列),陣列中的每個元素都是一個指向dictEntry結構的指標,每個dictEntry結構保存著一個鍵值對,
1.3.1 Redis如何解決散列沖突
-
鏈表法,當產生沖突時,使用鏈表法
-
rehash,隨著操作的進行,散串列中保存的鍵值對會也會不斷地增加或減少,為了保證負載因子維持在一個合理的范圍,當散串列內的鍵值對過多或過少時,內需要定期進行rehash,以提升性能或節省記憶體,Redis的rehash的步驟如下:
-
為字典的ht[1]散串列分配空間
-
擴展操作,那么ht[1]的大小為第一個大于等于ht[0].used*2的2n(2的n次方冪)(2n>ht[0].used*2)
-
收縮操作,那么ht[1]的大小為第一個大于等于ht[0].used的2^n
-
-
將保存在ht[0]中的鍵值對重新計算鍵的散列值和索引值,然后放到ht[1]指定的位置上,
-
將ht[0]包含的所有鍵值對都遷移到了ht[1]之后,釋放ht[0],將ht[1]設定為ht[0],并創建一個新的ht[1]哈希表為下一次rehash做準備,
-
-
rehash操作需要滿足以下條件:
-
服務器目前沒有執行BGSAVE(rdb持久化)命令或者BGREWRITEAOF(AOF檔案重寫)命令,并且散串列的負載因子大于等于1,
-
服務器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,并且負載因子大于等于5,
-
當負載因子小于0.1時,程式自動開始執行收縮操作,
-
-
漸進式 rehash
對于rehash我們思考一個問題如果散串列當前大小為 1GB,要想擴容為原來的兩倍大小,那就需要對 1GB 的資料重新計算哈希值,并且從原來的散串列搬移到新的散串列,這種情況聽著就很耗時,而生產環境中甚至會更大,為了解決一次性擴容耗時過多的情況,可以將擴容操作穿插在插入操作的程序中,分批完成,當負載因子觸達閾值之后,只申請新空間,但并不將老的資料搬移到新散串列中,當有新資料要插入時,將新資料插入新散串列中,并且從老的散串列中拿出一個資料放入到新散串列,每次插入一個資料到散串列,都重復上面的程序,經過多次插入操作之后,老的散串列中的資料就一點一點全部搬移到新散串列中了,這樣沒有了集中的一次一次性資料搬移,插入操作就都變得很快了,
1.4 跳躍表
Redis使用跳躍表作為有序集合鍵的底層實作之一,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員是比較長的字串時, Redis就會使用跳躍表來作為有序集合健的底層實作
跳躍表在鏈表的基礎上增加了多級索引以提升查找的效率,但其是一個空間換時間的方案,必然會帶來一個問題——索引是占記憶體的,原始鏈表中存盤的有可能是很大的物件,而索引結點只需要存盤關鍵值值和幾個指標,并不需要存盤物件,因此當節點本身比較大或者元素數量比較多的時候,其優勢必然會被放大,而缺點則可以忽略,
-
跳躍表節點的level陣列可以包含多個元素,每個元素都包含一個指向其他節點的指標,程式可以通過這些層來加快訪問其他節點的速度,一般來說,層的數量越多,訪問其他節點的速度就越快,每次創建一個新跳躍表節點的時候,程式都根據冪次定律(power law,越大的數出現的概率越小)隨機生成一個介于1和32之間的值作為level陣列的大小,這個大小就是層的高度

-
最左邊的是 skiplist結構,該結構包含以下屬性
-
header:指向跳躍表的表頭節點,通過這個指標程式定位表頭節點的時間復雜度就為O(1)
-
tail:指向跳躍表的表尾節點,通過這個指標程式定位表尾節點的時間復雜度就為O(1)
-
level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內),通過這個屬性可以再O(1)的時間復雜度內獲取層高最好的節點的層數,
-
length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內),通過這個屬性,程式可以再O(1)的時間復雜度內回傳跳躍表的長度,
-
-
右方的是四個 zskiplistNode結構
-
層(level):
- 每個層都帶有兩個屬性:前進指標和跨度,前進指標用于訪問位于表尾方向的其他節點,而跨度則記錄了前進指標所指向節點和當前節點的距離(跨度越大、距離越遠),在上圖中,連線上帶有數字的箭頭就代表前進指標,而那個數字就是跨度,當程式從表頭向表尾進行遍歷時,訪問會沿著層的前進指標進行,
-
分值(score): 在跳躍表中,節點按各自所保存的分值從小到大排列,
-
成員物件(oj): 在同一個跳躍表中,各個節點保存的成員物件必須是唯一的,但是多個節點保存的分值卻可以是相同的
-
1.5 整數集合

整數集合(intset)并不是一個基礎的資料結構,而是Redis自己設計的一種存盤結構,是集合鍵的底層實作之一,當一個集合只包含整數值元素,并且這個集合的元素數量不多時, Redis 就會使用整數集合作為集合鍵的底層實作,各個項在陣列中按值的大小從小到大有序地排列,并且陣列中不包含任何重復項,
1.5.1 整數集合升級
當我們要將一個新元素添加到整數集合里面,并且新元素的型別比整數集合現有所有元素的型別都要長時,整數集合需要先進行升級,然后才能將新元素添加到整數集合里面,升級整數集合并添加新元素主要分三步來進行,
-
根據新元素的型別,擴展整數集合底層陣列的空間大小,并為新元素分配空間
-
將底層陣列現有的所有元素都轉換成與新元素相同的型別,并將型別轉換后的元素放置到正確的位上,而且在放置元素的程序中,需要繼續維持底層陣列的有序性質不變,
-
將新元素添加到底層陣列里面,
- 不支持降級操作
1.6 壓縮串列
同整數集合一樣壓縮串列也不是基礎資料結構,而是 Redis 自己設計的一種資料存盤結構,它有點兒類似陣列,通過一片連續的記憶體空間,來存盤資料,不過,它跟陣列不同的一點是,它允許存盤的資料大小不同,

-
壓縮串列(zip1ist)是串列和哈希的底層實作之一,
-
當一個串列只包含少量串列項,并且每個串列項要么就是小整數值,要么就是長度比較短的字串,那么Redis就會使用壓縮串列來做串列的底層實作,
-
當一個哈希只包含少量鍵值對,比且每個鍵值對的鍵和值要么就是小整數值,要么就是長度比較短的字串,那么Redis就會使用壓縮串列來做哈希的底層實作,
1.6.1 Redis壓縮串列的構成
壓縮串列是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)資料結枃,一個壓縮串列可以包含任意多個節點(entry),每個節點可以保存一個位元組陣列或者一個整數值,如下圖,

1.6.2 Redis壓縮串列節點的構成

-
每個壓縮串列節點可以保存一個位元組陣列或者一個整數值
-
長度小于等于63(2^6-1)位元組的位元組陣列;
-
長度小于等于16383(2^14-1)位元組的位元組陣列
-
長度小于等于4294967295(2^32-1)位元組的位元組陣列
-
-
整數值可以是以下6種長度中的一種
-
int16_t型別整數
-
int32_t型別整數
-
int64_t型別整數
-
4位無符號
-
1位元組
-
3位元組
-
1.7 快速串列
考慮到鏈表的附加空間相對太高,prev 和 next 指標就要占去 16 個位元組 (64bit 系統的指標是 8 個位元組),另外每個節點的記憶體都是單獨分配,會加劇記憶體的碎片化,影響記憶體管理效率,因此Redis3.2版本開始對串列資料結構進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
1.7.1 基本結構
quicklist 實際上是 zipList 和 linkedList 的混合體,它將 linkedList 按段切分,每一段使用 zipList 來緊湊存盤,多個 zipList 之間使用雙向指標串接起來,

三、Redis物件系統

Redis用到的所有主要資料結構,簡單動態字串(SDS)、雙端鏈表、字典、壓縮串列、整數集合、跳躍表,
Redis并沒有直接使用這些資料結構來實作鍵值對資料庫,而是基于這些資料結構創建了一個物件系統,這個系統包含字串物件、串列物件、哈希物件、集合物件和有序集合物件這五種型別的物件,而每種物件又通過不同的編碼映射到不同的底層資料結構,
3.1 Redis 物件型別和編碼
Redis中的每個物件都由一個redisObject結構表示,該結構中和保存資料有關的三個屬性分別是type屬性、 encoding屬性和ptr屬性
typedef struct redisObiect{
//type屬性記錄了物件的型別
unsigned type:4;
//encoding屬性記錄了物件使用的編碼,也即是說這個物件使用了什么資料結構作為物件的底層實作,
unsigned encoding:4;
//指向底層資料結構的指標
void *ptr;
}
-
Type
型別常量 物件的名稱 REDIS_STRING 字串物件 REDIS_LIST 串列物件 REDIS_HASH 哈希物件 REDIS_SET 集合物件 REDIS_ZSET 有序集合物件 -
encoding
編碼常量 編碼所對應的底層資料結構 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 跳躍表和字典 -
不同型別和編碼的物件
型別 編碼 物件 REDIS_STRING REDIS_ENCODING_INT 使用整數值實作的字串物件 REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr編碼的簡單動態字串實作的字串物件 REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字串實作的字串物件 REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮串列實作的串列物件 REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實作的串列物件 REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮串列實作的哈希物件 REDIS_HASH REDIS_ENCODING_HT 使用字典實作的哈希物件 REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實作的集合物件 REDIS_SET REDIS_ENCODING_HT 使用字典實作的集合物件 REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用壓縮串列實作的有序集合物件 REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳躍表和字典實作的有序集合物件
Redis使用物件來表示資料庫中的鍵和值,每次當我們在Redis的資料庫中新創建一個鍵值對時,我們至少會創建兩個物件,一個物件用作鍵值對的健(鍵物件),另一個物件用作鍵值對的值(值物件),
-
其中Redis的鍵物件都是字串物件,
-
Redis的值物件主要有字串、哈希、串列、集合、有序集合幾種
-
當我們對一個資料庫鍵執行TYPE命令時,命令回傳的結果為資料庫鍵對應的值物件型別,而不是鍵物件型別
-
當我們稱呼一個資料庫鍵為“字串鍵”時,我們指的是“這個資料庫鍵所對應的值為字串物件”
-
當我們稱呼一個資料庫鍵為“串列鍵”時,我們指的是“這個資料庫鍵所對應的值為串列物件”
3.2 優勢
-
可以自由改進內部編碼,而對外的資料結構和命令沒有影響,這樣一旦開發出更優秀的內部編碼,無需改動外部資料結構和命令.
例如Redis3.2提供了quicklist,其結合了ziplist和linkedlist兩者 的優勢,為串列型別提供了一種更為優秀的內部編碼實作,而對外部用戶來說基本感知不到, 這一點比較像程式設計中的分層架構,
-
多種內部編碼實作可以在不同場景下發揮各自的優勢,從而優化物件在不同場景下的使用效率
例如ziplist比較節省記憶體,但是在串列元素比較多的情況下,性能會有所下降,這時候Redis會根據配置選項將串列型別的內部實作轉換linkedlist, (后續文章將根據具體物件介紹)
四、Redis物件型別
4.1 字串
字串物件是 Redis 中最基本的資料型別,也是我們作業中最常用的資料型別,redis中的鍵都是字串物件,而且其他幾種資料結構都是在字串物件基礎上構建的,字串物件的值實際可以是字串、數字、甚至是二進制,最大不能超過512MB ,
4.1.1 內部實作
Redis字串物件底層的資料結構實作主要是int和簡單動態字串SDS,其通過不同的編碼方式映射到不同的資料結構,字串物件的內部編碼有3種 :int、raw和embstr,Redis會根據當前值的型別和長度來決定使用哪種編碼來實作,
-
如果一個字串物件保存的是整數值,并且這個整數值可以用
long型別來表示,那么字串物件會將整數值保存在字串物件結構的ptr屬性里面(將void*轉換成1ong),并將字串物件的編碼設定為int; -
如果字串物件保存的是一個字串值,并且這個字串值的長度大于32位元組,那么字串物件將使用一個簡單動態字串(SDS)來保存這個字串值,并將物件的編碼設定為
raw;
-
如果字串物件保存的是一個字串值,并且這個字符申值的長度小于等于32位元組,那么字串物件將使用一個簡單動態字串(SDS)來保存這個字串值,并將物件的編碼設定為
embstrembstr編碼是專門用于保存短字串的一種優化編碼方式,這種編碼方式和
raw編碼一樣,都使用redisObject結構和sdshdr結構來表示字串物件,但raw編碼會呼叫兩次記憶體分配函式來分別創建redisObject結構和sdshdr結構,而embstr編碼則通過呼叫一次記憶體分配函式來分配一塊連續的空間,空間中依次包含redisObject結構和sdshdr結構:
使用embstr編碼的字串來保存短字串值有以下好處:
-
embstr編碼將創建字串物件所需的記憶體分配次數從raw編碼的兩次降低為一次 -
釋放
embstr編碼的字串物件同樣只需要呼叫一次記憶體釋放函式 -
因為
embstr編碼的字串物件的所有資料都保存在一塊連續的記憶體里面可以更好的利用CPU快取提升性能,
4.1.2 常用操作
添加:set key value [ex seconds] [px milliseconds] [nx|xx]
-
set key value [ex seconds]操作是原子性的,相比連續執行set和expire,它更快, -
nx:鍵必須不存在,才可以設定成功,用于添加,由于set key value nx同樣是原子性的操作,因此可以作為分布式鎖的一種實作方案, -
xx:與nx相反,鍵必須存在,才可以設定成功,用于更新
批量設定:MSET key value [key value ...]
批量獲取:MGET key [key ...]
-
get: n次get時間 = n次網路時間 + n次命令時間
-
mget: n次get時間 = 1次網路時間 + n次命令時間
-
學會使用批量操作,有助于提高效率,但是要掌握一個平衡的度,每次批量操作所發送的命令數并不是無節制的由于Redis是單執行緒架構,如果數量過多可能造成Redis阻塞或者網路擁
| 命令 | 描述 | 時間復雜度 |
|---|---|---|
| `set key value [ex seconds] [px milliseconds] [nx | xx]` | 設定值 |
get key |
獲取值 | O(1) |
del key [key ...] |
洗掉key | O(N)(N是鍵的個數) |
mset key [key value ...] |
批量設定值 | O(N)(N是鍵的個數) |
mget key [key ...] |
批量獲取值 | O(N)(N是鍵的個數) |
incr key |
將 key 中儲存的數字值增一 | O(1) |
decr key |
將 key 中儲存的數字值減一 | O(1) |
incrby key increment |
將 key 所儲存的值加上給定的增量值(increment) | O(1) |
decrby key increment |
key 所儲存的值減去給定的減量值(decrement) | O(1) |
incrbyfloat key increment |
將 key 所儲存的值加上給定的浮點增量值(increment) | O(1) |
append key value |
如果 key 已經存在并且是一個字串, APPEND 命令將指定的 value 追加到該 key 原來值(value)的末尾 | O(1) |
strlen key |
回傳 key 所儲存的字串值的長度, | O(1) |
setrange key offset value |
用 value 引數覆寫給定 key 所儲存的字串值,從偏移量 offset 開始 | O(1) |
getrange key start end |
回傳 key 中字串值的子字符 | O(N)(N是字串的長度) |
4.1.3 應用場景
reids字串的使用場景應該是最為廣泛的,甚至有些對redis其它幾種物件不太熟悉的人,基本所有場景都會使用字串(序列化一下直接扔進去),在眾多的使用場景中總結一下大概分以下幾種,
4.1.3.1 作為快取層

Redis經常作為快取層,來快取一些熱點資料,來加速讀寫性能從而降低后端的壓力,一般在讀取資料的時候會先從Redis中讀取,如果Redis中沒有,再從資料庫中讀取,在Redis作為快取層使用的時候,必須注意一些問題,如:快取穿透、雪崩以及快取更新問題
4.1.3.2 計數器\限速器\分布式系統ID
計數器\限速器\分布式ID等主要是利用Redis字串自增自減的特性,
-
計數器:經常可以被用來做計數器,如微博的評論數、點贊數、分享數,抖音作品的收藏數,京東商品的銷售量、評價數等,
-
限速器:如驗證碼介面訪問頻率限制,用戶登陸時需要讓用戶輸入手機驗證碼,從而確定是否是用戶本人,但是為了短信介面不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過5次,
-
分布式ID:由于Redis自增自減的操作是原子性的因此也經常在分布式系統中用來生成唯一的訂單號、序列號等,
4.1.3.3分布式系統共享session
因分布式系統通常有很多個服務,每個服務又會同時部署在多臺機器上,通過負載均衡機制將將用戶的訪問均衡到不同服務器上,這個時候用戶的請求可能分發到不同的服務器上,從而導致用戶登錄保存Session是在一臺服務器上,而讀取Session是在另一臺服務器上因此會讀不到Session,
這種問題通常的做法是把Session存到一個公共的地方,讓每個Web服務,都去這個公共的地方存取Session,而Redis就可以是這個公共的地方,(資料庫、memecache等都可以各有優缺點),
4.2 哈希

在redis中,哈希型別是指Redis鍵值對中的值本身又是一個鍵值對結構,形如value=https://www.cnblogs.com/ruigedada/p/[{field1,value1},...{fieldN,valueN}]
4.2.1 內部實作
哈希型別的內部編碼有兩種:ziplist(壓縮串列),hashtable(哈希表),只有當存盤的資料量比較小的情況下,Redis 才使用壓縮串列來實作字典型別,具體需要滿足兩個條件:
-
哈希型別元素個數小于hash-max-ziplist-entries配置(默認512個)
-
所有值都小于hash-max-ziplist-value配置(默認64位元組)
ziplist使用更加緊湊的結構實作多個元素的連續存盤,所以在節省記憶體方面比hashtable更加優秀,當哈希型別無法滿足ziplist的條件時,Redis會使用hashtable作為哈希的內部實作,因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間復雜度為O(1)
| [field ...] | 洗掉一個或多個Hash的field | O(N) N是被洗掉的欄位數量, |
| HEXISTS key field | 判斷field是否存在于hash中 | O(1) |
| HGET key field | 獲取hash中field的值 | O(1) |
| HGETALL key | 從hash中讀取全部的域和值 | O(N) N是Hash的長度 |
| HINCRBY key field increment | 將hash中指定域的值增加給定的數字 | O(1) |
| HINCRBYFLOAT key field increment | 將hash中指定域的值增加給定的浮點數 | O(1) |
| HKEYS key | 獲取hash的所有欄位 | O(N) N是Hash的長度 |
| HLEN key | 獲取hash里所有欄位的數量 | O(1) |
| HMGET key field [field ...] | 獲取hash里面指定欄位的值 | O(N) N是請求的欄位數 |
| HMSET key field value [field value ...] | 設定hash欄位值 | O(N) N是設定的欄位數 |
| HSET key field value | 設定hash里面一個欄位的值 | O(1) |
| HSETNX key field value | 設定hash的一個欄位,只有當這個欄位不存在時有效 | O(1) |
| HSTRLEN key field | 獲取hash里面指定field的長度 | O(1) |
| HVALS key | 獲得hash的所有值 | O(N) N是Hash的長度 |
| HSCAN key cursor [MATCH pattern] [COUNT count] | 迭代hash里面的元素 |
4.2.2 應用場景
- Redis哈希物件常常用來快取一些對象資訊,如用戶資訊、商品資訊、配置資訊等,
-
原生字串每個屬性一個鍵,
set user:1:name Tom set user:1:age 15`優點:簡單直觀,每個屬性都支持更新操作,
缺點:占用過多的鍵,記憶體占用量較大,同時用戶資訊內聚性比較差,所以此種方案一般不會在生產環境使用,
-
序列化字串后,將用戶資訊序列化后用一個鍵保存
set user:1 serialize(userInfo)優點:簡化編程,如果合理的使用序列化可以提高記憶體的使用效率,
缺點:序列化和反序列化有一定的開銷,同時每次更新屬性都需要把全部資料取出進行反序列化,更新后再序列化到Redis中,
-
序列化字串后,將用戶資訊序列化后用一個鍵保存
hmset user:1 name Tom age 15優點:簡單直觀,如果使用合理可以減少記憶體空間的使用,
缺點:要控制哈希在ziplist和hashtable兩種內部編碼的轉換,hashtable會消耗更多記憶體,
-
購物車
-
計數器
4.3 串列List

串列(list)型別是用來存盤多個有序的字串,串列中的每個字串稱為元素(element),一個串列最多可以存盤232-1個元素,在Redis中,可以對串列兩端插入(push)和彈出(pop),還可以獲取指定范圍的元素串列、獲取指定索引下標的元素等,串列是一種比較靈活的資料結構,它可以充當堆疊和佇列的角色,在實際開發上有很多應用場景,
-
串列中的元素是有序的,這就意味著可以通過索引下標獲取某個元素或者某個范圍內的元素串列,
-
串列中的元素可以是重復的.
4.3.1 內部實作
在Redis3.2版本以前串列型別的內部編碼有兩種,
-
ziplist(壓縮串列):當串列的元素個數小于list-max-ziplist-entries配置(默認512個),同時串列中每個元素的值都小于list-max-ziplist-value配置時(默認64位元組),Redis會選用ziplist來作為串列的內部實作來減少記憶體的使用, -
linkedlist(鏈表):當串列型別無法滿足ziplist的條件時,Redis會使用linkedlist作為串列的內部實作,
而在Redis3.2版本開始對串列資料結構進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
| 命令 | 說明 | 時間復雜度 |
|---|---|---|
| BLPOP key [key ...] timeout | 洗掉,并獲得該串列中的第一元素,或阻塞,直到有一個可用 | O(1) |
| BRPOP key [key ...] timeout | 洗掉,并獲得該串列中的最后一個元素,或阻塞,直到有一個可用 | O(1) |
| BRPOPLPUSH source destination timeout | 彈出一個串列的值,將它推到另一個串列,并回傳它;或阻塞,直到有一個可用 | O(1) |
| LINDEX key index | 獲取一個元素,通過其索引串列 | O(N) |
| LINSERT key BEFORE | AFTER pivot value在串列中的另一個元素之前或之后插入一個元素 | O(N) |
| LLEN key | 獲得佇列(List)的長度 | O(1) |
| LPOP key | 從佇列的左邊出隊一個元素 | O(1) |
| LPUSH key value [value ...] | 從佇列的左邊入隊一個或多個元素 | O(1) |
| LPUSHX key value | 當佇列存在時,從隊到左邊入隊一個元素 | O(1) |
| LRANGE key start stop | 從串列中獲取指定回傳的元素 | O(S+N) |
| LREM key count value | 從串列中洗掉元素 | O(N) |
| LSET key index value | 設定佇列里面一個元素的值 | O(N) |
| LTRIM key start stop | 修剪到指定范圍內的清單 | O(N) |
| RPOP key | 從佇列的右邊出隊一個元 | O(1) |
| RPOPLPUSH source destination | 洗掉串列中的最后一個元素,將其追加到另一個串列 | O(1) |
| RPUSH key value [value ...] | 從佇列的右邊入隊一個元素 | O(1) |
| RPUSHX key value | 從佇列的右邊入隊一個元素,僅佇列存在時有效 | O(1) |
4.3.2 應用場景
-
訊息佇列
串列型別可以使用 rpush 實作先進先出的功能,同時又可以使用 lpop 輕松的彈出(查詢并洗掉)第一個元素,所以串列型別可以用來實作訊息佇列
-
文章(商品等)串列
4.4 集合set
Set 是一個無序并唯一的鍵值集合,它的存盤順序不會按照插入的先后順序進行存盤,
集合型別和串列型別的區別如下:
-
串列可以存盤重復元素,集合只能存盤非重復元素;
-
串列是按照元素的先后順序存盤元素的,而集合則是無序方式存盤元素的,
Set可以存盤232-1個元素,Redis除了支持集合內的增刪改查,同時還支持多個集合取交集、并集、差集,合理地使用好集合型別,能在實際開發中解決很多實際問題,
4.4.1 內部實作
集合型別的內部編碼有兩種:
-
intset(整數集合):當集合中的元素都是整數且元素個數小于set-maxintset-entries配置(默認512個)時,Redis會選用intset來作為集合的內部實作,從而減少記憶體的使用, -
hashtable(字典):當集合型別無法滿足intset的條件時,Redis會使用hashtable作為集合的內部實作,
4.4.2 使用場景
集合的主要幾個特性,無序、不可重復、支持并交差等操作,因此集合型別比較適合用來資料去重和保障資料的唯一性,還可以用來統計多個集合的交集、錯集和并集等,當我們存盤的資料是無序并且需要去重的情況下,比較適合使用集合型別進行存盤,
-
標簽系統
-
抽獎系統
4.5 有序集合 sorted set

有序集合型別 (Sorted Set或ZSet) 相比于集合型別多了一個排序屬性 score(分值),對于有序集合 ZSet 來說,每個存盤元素相當于有兩個值組成的,一個是有序結合的元素值,一個是排序值,有序集合保留了集合不能有重復成員的特性(分值可以重復),但不同的是,有序集合中的元素可以排序,
4.5.1 內部實作
有序集合是由ziplist或 skiplist組成的,
當資料比較少時,有序集合使用的是 ziplist 存盤的,有序集合使用 ziplist 格式存盤必須滿足以下兩個條件:
-
有序集合保存的元素個數要小于 128 個;
-
有序集合保存的所有元素成員的長度都必須小于 64 位元組,
4.5.2 使用場景
-
排行榜系統
-
電話號碼(姓名)排序
五、型別檢查與命令多型
Redis中用于操作鍵的命令基本上可以分為兩種型別:
-
其中一種命令可以對任何型別的鍵執行,比如DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等,舉個栗子,DEL命令可以用來洗掉三種不同型別的鍵:字串鍵、串列鍵、集合鍵
-
而另一種命令只能對特性型別執行,比如:
-
SET、GET、APPEND、STRLEN等命令只能對字串鍵執行
-
HDEL、HSET、HGET、HLEN等命令只能對哈希鍵執行
-
RPUSH、LPOP、LINSERT、LLEN等命令只能對串列鍵執行
-
SADD、SPOP、SINTER、SCARD等命令只能對集合鍵執行
-
ZADD、ZCARD、ZRANK、ZSCORE等命令只能對有序集合鍵執行
-
5.1 型別檢查和實作
為了確保只有指定型別的鍵可以執行某些特定的命令,在執行一個型別特定的命令前,Redis會先檢查輸入鍵的型別是否正確,然后再決定是否執行給定的命令,型別特定命令所進行的型別檢查是通過redisObject結構的type屬性來實作的:
-
在執行一個型別特定命令之前,服務器會先檢查輸入資料庫鍵的值物件是否為執行命令所需的型別,如果是的話,服務器就會執行指定的命令
-
否則,服務器將拒絕執行命令,并向客戶端回傳一個型別錯誤
5.2 多型命令的實作

Redis除了會根據值物件的型別來判斷鍵是否能夠執行指令外,還會根據值物件的編碼方式,選擇正確的命令實作代碼來執行命令,舉個栗子,串列物件有ziplist和linkedlist兩種編碼可用,其中前者使用壓縮串列API來實作串列命令,而后者使用雙端鏈表來實作串列命令
現在,考慮這樣一個情況,如果我們對一個鍵執行LLEN命令,那么服務器除了要確保執行命令的是串列鍵之外,還要根據鍵的值物件所使用的編碼方式來選擇正確的LLEN命令實作:
-
如果串列物件的編碼為ziplist,程式將使用ziplistLen函式來回傳串列的長度
-
如果串列物件的編碼為linkedlist,程式將使用listLength函式來回傳雙端鏈表的長度
借用面向物件方面的術語來說,我們可以認為LLEN命令時多型的,只要執行LLEN命令的是串列鍵,那么無論值物件時壓縮串列還是雙端鏈表,命令都可以正常執行,實際上,我們可以將DEL、EXPIRE、TYPE等命令也稱為多型命令,因為無論輸入的鍵是什么型別,這些命令都可以正確地執行,
DEL、EXPIRE等命令和LLEN等命令的區別在于,
-
前者是基于型別的多型——一個命令可以同時用于處理多種不同型別的鍵,
-
而后者是基于編碼的多型——一個命令可以同時用于處理多種不同編碼
六、記憶體回收
因為C語言并不具備自動回收記憶體的功能 ,所以Redis在自己對的物件系統中構建了一個參考計數技術來實作自動回收記憶體,通過這一機制,程式可以跟蹤物件的參考計數資訊,在適當的時候自動釋放物件并進行記憶體回收,每個物件的參考計數資訊由redisObject結構的refcount屬性記錄:
typedef struct redisObject {
unsigned type:4;/* Not used */
unsigned encoding:4;
unsigned lru:22;
//參考計數
int refcount;
void *ptr;
} robj;
-
在創建一個新物件時,參考計數的值會被初始化為1
-
當物件被一個新程式使用時,它的參考計數值會被增一
-
當物件不再被一個程式使用時,它的參考計數值會被減一
-
當物件的參考計數值變為0時,物件所占用的記憶體會被釋放
| 函式 | 作用 |
| incrRefCount | 將物件的參考計數值增一 |
| decrRefCount | 將物件的參考計數值減一,當物件的參考計數值等于0時,釋放物件 |
| resetRefCount | 將物件的參考計數值設定為0,但并不釋放物件,這個函式通常在需要重新設定物件的參考計數值時使用 |
- 物件的整個生命周期可以劃分為創建物件、操作物件、釋放物件
七 物件共享
除了用于實作參考計數記憶體回識訓制之外,物件的參考計數屬性還帶有物件共享的作用,

在Redis中,讓更多鍵共享一個值物件需要執行以下兩個步驟:
-
將資料庫鍵的值指標指向一個現有的值物件
-
將被共享的值物件的參考計數加1
目前來說,Redis會在初始化服務器時,創建一萬個字串物件,這些物件包含從0到9999的所有整數值,當服務器需要用到值為0到9999的字串時,服務器就會使用這些共享物件,而不是新創建物件,另外,創建共享字串的數量可以通過修改redis.h/OBJ_SHARED_INTEGERS來修改
為什么Redis不共享包含字串的物件?當服務器考慮將一個共享物件設定為鍵的值物件時,程式需要先檢查給定的共享物件和鍵想創建的目標物件是否相同,只有在共享物件和目標物件完全相同的情況下,程式才會將共享物件用作鍵的值物件,而一個共享物件保存的值越復雜,驗證共享物件和目標物件相同所需的復雜度就會越高,消耗CPU時間也越多:
-
如果共享物件是保存整數值的字串物件,那么驗證操作的時間復雜度為O(1)
-
如果共享物件是保存字串值的字串物件,那么驗證操作的復雜度為O(N)
-
如果共享物件是包含了多個值(或者物件的)物件,比如串列物件或者哈希物件,那么驗證操作的時間復雜度為O(N^2)
因此,盡管共享更復雜的物件可以節約更多記憶體,但受到CPU時間的限制,Redis只對包含整數值的字串物件進行共享
八、物件的空轉時長
除了前面介紹過的type、encoding、ptr和refcount四個屬性外,redisObject結構包含的最后一個屬性為lru屬性,該屬性記錄了物件最后一次被命令程式訪問的時間:
typedef struct redisObject {
unsigned type:4;
unsigned notused:2;
unsigned encoding:4;
unsigned lru:22;
int refcount;
void *ptr;
} robj;
OBJECT IDLETIME命令可以列印出給定鍵的空轉時長,這一空轉時長就是通過將當前時間減去鍵物件的lru時間計算得出,
OBJECT IDLETIME命令的實作是特殊的,這個命令在訪問鍵時不會修改lru屬性,除了可以被OBJECT IDLETIME命令列印出來之外,鍵的空轉時長還有另外一項作用:如果服務器打開了maxmemory選項,并且服務器用于回收記憶體的演算法為volatile-lru(僅對設定了過期時間的鍵采取LRU淘汰)或者allkeys-lru(對所有的鍵都采取LRU淘汰),那么當服務器占用的記憶體超過maxmemory選項所設定的上限值時,空轉時長較高的那部分鍵會優先被服務器釋放,從而回收記憶體
九、資料淘汰策略
9.1 過期淘汰策略
若果采用定時洗掉策略,當資料庫中含有太多的過期資料,則會占用太多CPU時間,影響服務器回應時間和吞吐量,甚至造成堵塞,
- Redis服務器實際使用的是惰性洗掉和定期洗掉兩種策略,通過配合使用這兩種策略,服務器可以很好地在合理使用CPU時間和避免浪費記憶體空間之間取得平衡,
9.1.3 惰性洗掉
Redis 4.0 增加了懶惰洗掉功能,懶惰洗掉需要使用異步執行緒對已洗掉的節點進行記憶體回收,這意味著 Redis 底層其實并不是單執行緒,它內部還有幾個額外的鮮為人知的輔助執行緒,
這幾個輔助執行緒在 Redis 內部有一個特別的名稱,就是“BIO”,全稱是 Background IO,意思是在背后默默干活的 IO 執行緒,
Redis 大佬 Antirez 實作懶惰洗掉時,它并不是一開始就想到了異步執行緒,它最初的嘗試是在主執行緒里,使用類似于字典漸進式搬遷的方式來實作漸進式洗掉回收,
異步執行緒釋放記憶體不用為每種資料結構適配一套漸進式釋放策略,也不用搞個自適應演算法來仔細控制回收頻率,只是將物件從全域字典中摘掉,然后往佇列里一扔,主執行緒就干別的去了,異步執行緒從佇列里取出物件來,直接走正常的同步釋放邏輯就可以了,
9.1.4 定期洗掉
-
定期洗掉策略每隔一段時間執行一次洗掉過期鍵操作,并通過限制洗掉操作執行的時長和頻率來減少洗掉操作對CPU時間的影響
-
除此之外,通過定期洗掉過期鍵,定期洗掉策略有效地減少了因為過期鍵而帶來的記憶體浪費
定期洗掉策略的難點是確定洗掉操作執行的時長和效率
Redis 會將每個設定了過期時間的 key 放入到一個獨立的字典中,默認每 100ms 進行一次過期掃描:
-
隨機抽取 20 個 key
-
洗掉這 20 個key中過期的key
-
如果過期的 key 比例超過 1/4,就重復步驟 1,繼續洗掉,
-
為什不掃描所有的 key?
Redis 是單執行緒,全部掃描豈不是卡死了,而且為了防止每次掃描過期的 key 比例都超過 1/4,導致不停回圈卡死執行緒,Redis 為每次掃描添加了上限時間,默認是 25ms,如果在同一時間出現大面積 key 過期,Redis 回圈多次掃描過期詞典,直到過期的 key 比例小于 1/4,這會導致卡頓,而且在高并發的情況下,可能會導致快取雪崩,
9.2 快取淘汰策略
可以設定記憶體最大使用量maxmemory,當記憶體使用量超出時,會施行資料淘汰策略,
Redis 具體有 6 種淘汰策略,配置 maxmemory-policy::
| 策略 | 描述 |
|---|---|
| volatile-lru | 從已設定過期時間的資料集中挑選最近最少使用的資料淘汰 |
| volatile-ttl | 從已設定過期時間的資料集中挑選將要過期的資料淘汰 |
| volatile-random | 從已設定過期時間的資料集中任意選擇資料淘汰 |
| allkeys-lru | 從所有資料集中挑選最近最少使用的資料淘汰 |
| allkeys-random | 從所有資料集中任意選擇資料進行淘汰 |
| volatile-lfu | 對有過期時間的key采用LFU淘汰演算法 |
| allkeys-lfu | 對全部key采用LFU淘汰演算法 |
| noeviction | 禁止驅逐資料 |
9.1.2 Redis的LRU演算法 (Least recently used) 最近最少使用
實作 LRU 演算法除了需要 key/value 字典外,還需要附加一個鏈表,鏈表中的元素按照一定的順序進行排列,當空間滿的時候,會踢掉鏈表尾部的元素,當字典的某個元素被訪問時,它在鏈表中的位置會被移動到表頭,所以鏈表的元素排列順序就是元素最近被訪問的時間順序,
Redis中的LRU與常規的LRU實作并不相同,常規LRU會準確的淘汰掉隊頭的元素,但是Redis的LRU并不維護佇列,只是根據配置的策略
-
要么從所有的key中隨機選擇N個(5,N可以配置)
-
要么從所有的設定了過期時間的key中選出N個鍵,然后再從這N個鍵中選出最久沒有使用的一個key進行淘汰,
-
Redis3.0之后又改善了演算法的性能,會提供一個待淘汰候選key的pool,里面默認有16個key,按照空閑時間排好序,更新時從Redis鍵空間隨機選擇N個key,分別計算它們的空閑時間idle,key只會在pool未滿或者空閑時間大于pool里最小的時,才會進入pool,然后從pool中選擇空閑時間最大的key淘汰掉,
9.1.3 LFU (Least frequently used) 最不經常使用
LFU是在Redis4.0后出現的,LRU的最近最少使用實際上并不精確,LFU 表示按最近的訪問頻率進行淘汰,它比 LRU 更加精準地表示了一個 key 被訪問的熱度,
在 LFU 模式下,lru 欄位 24 個 bit 用來存盤兩個值,分別是 ldt(last decrement time) 和 logc(logistic counter),
-
logc 是 8 個 bit,用來存盤訪問頻次,因為 8 個 bit 能表示的最大整數值為 255,存盤頻次肯定遠遠不夠,所以這 8 個 bit 存盤的是頻次的對數值,并且這個值還會隨時間衰減,如果它的值比較小,那么就很容易被回收,為了確保新創建的物件不被回收,新物件的這 8 個 bit 會初始化為一個大于零的值,默認是 LFU_INIT_VAL=5,
-
ldt 是 16 個位,用來存盤上一次 logc 的更新時間,因為只有 16 位,所以精度不可能很高,它取的是分鐘時間戳對 2^16 進行取模,大約每隔 45 天就會折返,
-
lfu-log-factor可以調整計數器counter的增長速度,lfu-log-factor越大,counter增長的越慢,lfu-decay-time是一個以分鐘為單位的數值,可以調整counter的減少速度
十、持久化
10.1 RDB持久化
因為Redis是記憶體資料庫,它將自己的資料庫狀態存盤在記憶體里面,所以如果不想辦法將存盤在記憶體中的資料庫狀態保存到磁盤中,那么一旦服務器行程退出,服務器中的資料庫狀態也會消失,為了解決這個問題,Redis提供了RDB持久化功能,可以將Redis記憶體中的資料庫狀態保存到磁盤中,避免資料意外丟失
10.1.1 是什么
在指定的時間間隔內將記憶體中的資料集快照寫?磁盤, 也就是?話講的 Snapshot 快照,它恢復時是將快照?件直接讀到記憶體?
10.1.2 持久化流程

10.1.3 save vs bgsave
-
SAVE命令會阻塞Redis服務器,直到RDB檔案創建完畢為止,在服務器行程阻塞期間,服務器不能處理任何命令請求
-
BGSAVE命令會派生出一個子行程,然后由子行程負責創建RDB檔案,服務器行程(父行程)繼續處理命令請求:
-
如果服務器開啟了AOF持久化功能,那么服務器就會優先使用AOF檔案來還原資料庫狀態
-
只有在AOF持久化功能處于關閉狀態時,服務器才會使用RDB檔案來還原資料庫狀態
10.1.4 BGSAVE命令執行時的服務器狀態
-
如果BGSAVE命令正在執行,那么客戶端發送的BGREWRITEAOF命令會被延遲到BGSAVE命令執行完畢之后再執行
-
如果BGREWRITEAOF命令正在執行,那么客戶端發送的BGSAVE命令會被服務器拒絕
10.1.5 自動間隔性保存
因為BGSAVE命令可以在不阻塞服務器的情況下執行,所以Redis允許用戶通過沒設定服務器配置的save選項,讓服務器每隔一段時間自動執行一次BGSAVE命令,用戶可以通過save選項設定多個保存條件,但只要其中一個條件被滿足,服務器就會執行BGSAVE命令,

10.1.6 優缺點
-
優點
l 適合大規模的資料恢復
l 對資料完整性和一致性要求不高更適合使用
l 節省磁盤空間
l 恢復速度快
-
缺點
l Fork的時候,記憶體中的資料被克隆了一份,大致2倍的膨脹性需要考慮
l 雖然Redis在fork時使用了寫時拷貝技術,但是如果資料龐大時還是比較消耗性能,
l 在備份周期在一定間隔時間做一次備份,所以如果Redis意外down掉的話,就會丟失最后一次快照后的所有修改,
10.2 AOF持久化
10.2.1 是什么
以日志的形式來記錄每個寫操作(增量保存),將Redis執行過的所有寫指令記錄下來(讀操作不記錄), 只許追加檔案但不可以改寫檔案,redis啟動之扯訓讀取該檔案重新構建資料,換言之,redis重啟的話就根據日志檔案的內容將寫指令從前到后執行一次以完成資料的恢復作業
10.2.2 持久化流程
(1)客戶端的請求寫命令會被append追加到AOF緩沖區內;
(2)AOF緩沖區根據AOF持久化策略[always,everysec,no]將操作sync同步到磁盤的AOF檔案中;
(3)AOF檔案大小超過重寫策略或手動重寫時,會對AOF檔案rewrite重寫,壓縮AOF檔案容量;
(4)Redis服務重啟時,會重新load加載AOF檔案中的寫操作達到資料恢復的目的;
10.2.3 重寫流程
如果Redis的AOF當前大小>= base_size +base_size*100% (默認)且當前大小>=64mb(默認)的情況下,Redis會對AOF進行重寫,
(1)bgrewriteaof觸發重寫,判斷是否當前有bgsave或bgrewriteaof在運行,如果有,則等待該命令結束后再繼續執行,
(2)主行程fork出子行程執行重寫操作,保證主行程不會阻塞,
(3)子行程遍歷redis記憶體中資料到臨時檔案,客戶端的寫請求同時寫入aof_buf緩沖區和aof_rewrite_buf重寫緩沖區保證原AOF檔案完整以及新AOF檔案生成期間的新的資料修改動作不會丟失,
(4)1).子行程寫完新的AOF檔案后,向主行程發信號,父行程更新統計資訊,2).主行程把aof_rewrite_buf中的資料寫入到新的AOF檔案,
(5)使用新的AOF檔案覆寫舊的AOF檔案,完成AOF重寫,
10.2.4 優缺點
-
優點
-
備份機制更穩健,丟失資料概率更低,
-
可讀的日志文本,通過操作AOF穩健,可以處理誤操作,
-
-
缺點
-
比起RDB占用更多的磁盤空間,
-
恢復備份速度要慢,
-
每次讀寫都同步的話,有一定的性能壓力,
-
存在個別Bug,造成恢復不能,
-
十一、檔案事件
Redis是基于Reactor模式開發了自己的網路事件處理器,這個處理器被稱為檔案事件處理器:
-
檔案事件處理器使用I/O多路復用程式來同時監聽多個套接字,并根據套接字目前執行的任務來為套接字關聯不同的事件處理器
-
當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件
雖然檔案事件處理器以單執行緒方式運行,但通過使用I/O多路復用程式來監聽多個套接字,檔案事件處理器既實作了高性能的網路通信模型,又可以很好地與Redis服務器中其他同樣以單執行緒方式運行的模塊進行對接,這保持了Redis內部單執行緒設計的簡單性
11.1檔案事件處理器的構成
檔案事件處理器的四個組成部分,它們分別是套接字、I/O多路復用程式、檔案事件分派器,以及事件處理器

盡管多個檔案事件可能會并發地出現,但I/O多路復用程式總是會將所有產生事件的套接字都放到一個佇列中,然后通過這個佇列,以有序、同步、每次一個套接字的方式向檔案事件分派器傳送套接字,當上一個套接字產生的實踐被處理完畢之后(該套接字為事件所關聯的事件處理器執行完畢),I/O多路復用程式才會繼續向檔案事件分派器傳送下一個套接字
-
select:執行緒不安全,如果任何一個sock(I/O stream)出現了資料,select 僅僅會回傳,但是并不會告訴你是那個sock上有資料,
-
poll :執行緒不安全,去掉了1024個鏈接的限制
-
epoll 現在是執行緒安全的, 不僅告訴你sock組里面資料,還會告訴你具體哪個sock有資料,你不用自己去找了
十二、主從復制
主機資料更新后根據配置和策略, 自動同步到備機的master/slaver機制,Master以寫為主,Slave以讀為主
12.1 新版復制功能的實作
為了解決舊版復制功能在處理斷線重復制情況時的低效問題,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操作,PSYNC命令具有完整同步和部分同步兩種模式:
-
其中完整重同步用于處理初次復制情況:通過讓主服務器創建并發送RDB檔案,以及向從服務器發送保存在緩沖區里面的寫命令來進行同步,
-
同步
-
從服務器向主服務器發送SYNC命令
-
收到SYNC命令的主服務器執行BGSAVE命令,在后臺生成一個RDB檔案,并使用一個緩沖區記錄從現在開始執行的所有命令
-
當主服務器的BGSAVE命令執行完畢時,主服務器會將BGSAVE命令生成的RDB檔案發送給從服務器,從服務器接收并載入這個RDB檔案,將自己的資料庫狀態更新至主服務器執行執行BGSAVE命令時的資料庫狀態
-
主服務器將記錄在緩沖區中的所有寫命令發送給從服務器,從服務器執行這些寫命令,將自己的資料庫狀態更新至主服務器資料庫當前所處的狀態
-
-
命令傳播
-
每當主服務器執行客戶端發送的寫命令時,主服務器的資料庫就有可能會被修改,并導致主從服務器狀態不再一致
-
主服務器需要對從服務器執行命令傳播操作,主從服務器會將自己的寫命令,即是造成主從服務器不一致的那條寫命令發送給從服務器執行,當從服務器執行了相同的寫命令后,主從服務器將再次回到一致的狀態
-
-
-
而部分重同步則用于處理斷線后重復制情況:當從服務器在斷線后重新連接主服務器時,如果條件允許,主服務器可以將主從服務器連接斷開期間執行的寫命令發送給從服務器,從服務器只要接收并執行這些寫命令,就可以將資料庫更新至主服務器當前所處的狀態,

部分重同步由以下三個部分構成:
-
主服務器的復制偏移量(replication offset)和從服務器的復制偏移量
-
主服務器每次向從服務器傳播N個位元組的資料時,就將自己的復制偏移量的值加上N
-
從服務器每次收到主服務器傳播來的N個位元組的資料時,就將自己的復制偏移量的值加上N
-
-
主服務器的復制積壓緩沖區(replication backlog)
-
復制積壓緩沖區是有主服務器維護的一個固定長度(fixed-size)先進先出(FIFO)佇列,默認大小為1MB,當主服務器進行命令傳播時,它不僅將命令發送給所有從服務器,還會將寫命令入隊到復制積壓緩沖區中,
-
主服務器的復制積壓緩沖區會保存著一部分最近傳播的寫命令,并且復制積壓緩沖區會為佇列的每個位元組記錄相應的復制偏移量
-
當從服務器重新連上主服務器時,從服務器通過PSYNC命令將自己的復制偏移量offset發送給主服務器,主服務器會根據這個復制偏移量來決定對從服務器執行何種同步操作:
-
如果offset偏移量之后的資料(也即是偏移量offset+1開始的資料)仍然存在于復制積壓緩沖區中,那么主服務器將對從服務器執行部分同步操作
-
相反,如果offset偏移量之后的資料已經不存在于復制積壓緩沖區,那么主服務器將對從服務器執行完整重同步操作
-
-
-
服務器的運行ID(run ID)
-
如果從服務器保存的運行ID和當前連接的主服務器的運行ID相同,那么說明從服務器斷線之前復制的就是當前連接的這個主服務器,主服務器可以繼續嘗試執行部分重同步操作
-
相反地,如果從服務器保存的運行ID和當前連接的主服務器的運行ID并不相同,那么說明從服務器斷線之前復制的主服務器并不是當前連接的這個主服務器,主服務器將對從服務器執行完整重同步操作
-
十三、Redis哨兵高可用框架
哨兵(sentinel)是特殊的Redis服務,不提供讀寫,主要用來監控Redis實體節點,
哨兵架構下的客戶端在第一次從哨兵獲得Redis主節點后,后續就直接訪問Redis的主節點,不需要每次都通過哨兵訪問Redis主節點,當Redis主節點有變化時,哨兵會第一時間感知到,并且將新的Redis主節點通知給客戶端(這里面Redis客戶端一般都實作了訂閱功能,訂閱sentinel發布的節點變動訊息),

十四 集群
Redis高可用集群是一個由多個主從節點群組成的分布式服務器群,它具有復制、高可用和分片特性,
Redis集群不需要sentinel哨兵也能完成節點移除和故障轉移的功能,只需要將每個節點設定成集群模式,這種集群模式沒有中心節點,可水平擴展,官方檔案稱可以線性擴展到上萬個節點(官方推薦不超過1000個節點),Redis集群的性能和高可用性均優于之前版本的哨兵模式,且集群配置簡單,高可用集群相較于哨兵集群,至少不會出現主節點下線后,整個集群在一段時間內處于不可用狀態,直到選舉出主節點,因為高可用集群有多個主節點,當我們需要向整個Redis服務寫入大批量資料時,資料會根據寫入的key算出一個hash值,將資料落地到不同的主節點上,所以當一個主節點下線后,落地到其他主節點的寫請求還是正常的,

十五 redis參考問題解決
15.1 快取穿透
-
問題描述
訪問一個資料庫中沒有的鍵
-
解決方案
-
對空值快取:如果一個查詢回傳的資料為空(不管是資料是否不存在),我們仍然把這個空結果(null)進行快取,設定空結果的過期時間會很短,最長不超過五分鐘
-
設定可訪問的名單(白名單):使用bitmaps型別定義一個可以訪問的名單,名單id作為bitmaps的偏移量,每次訪問和bitmap里面的id進行比較,如果訪問id不在bitmaps里面,進行攔截,不允許訪問,
-
采用布隆過濾器:布隆過濾器可以用于檢索一個元素是否在一個集合中,是一個很長的二進制向量(位圖)和一系列隨機映射函式(哈希函式),回傳存在不一定存在,回傳不存在則一定不存在
-
進行實時監控
-
15.2 快取擊穿
-
問題描述
- 大并發訪問一個已經過期的鍵
-
解決方案
-
預先設定熱門資料:在redis高峰訪問之前,把一些熱門資料提前存入到redis里面,加大這些熱門資料key的時長
-
實時調整:現場監控哪些資料熱門,實時調整key的過期時長
-
使用鎖

-
15.3 快取雪崩
-
問題描述
- 大并發訪問大量已過期的key
-
解決方案
-
構建多級快取架構
-
使用鎖或佇列:用加鎖或者佇列的方式保證來保證不會有大量的執行緒對資料庫一次性進行讀寫,從而避免失效時大量的并發請求落到底層存盤系統上,不適用高并發情況
-
設定過期標志更新快取:記錄快取資料是否過期(設定提前量),如果過期會觸發通知另外的執行緒在后臺去更新實際key的快取,
-
將快取失效時間分散開**:**
-
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/472933.html
標籤:NoSQL
上一篇:Mysql性能調優-工具篇
下一篇:如何使用 SQL 函式處理資料
