all is redis

1.你應該知道的redis幾個特性

(1).速度快html

  • 數據存儲在內存中
  • 底層C語言實現
  • 單線程框架
  • 源代碼集性能與優雅於一身

(2).基於鍵值對的NoSQL數據庫面試

  • 鍵只有字符串類型,值有string(字符串) hash(哈希) list(列表) set(集合) zset(有序集合) Bitmaps(位圖)
(3).持久化
(4).主從複製
(5).高可用與分佈式

2.Redis全稱REmote DIctionary Server正則表達式

遠程字典服務器redis

主流來講,Redis是單線程模型,但並非徹底只使用單線程

3.Redis 本質算法

開源
高性能
鍵值對數據庫
數據結構服務器
數據結構數據庫數據庫

4.redis特色
.存儲結構上特色
鍵是字符串類型,值能夠是字符串,鏈表,哈希,集合,有序集合
.內存存儲,持久化
內存存儲速度快,能夠一秒內讀寫超過十萬個鍵值
持久化能夠將內存中數據存儲到硬盤,方便下次構建數據庫
.功能豐富
做爲數據庫
做爲緩存
做爲隊列
.簡單穩定
語法簡單,命令少(100+)
穩定,源碼公開

5.Redis可執行文件說明
redis-server          Redis服務器
redis-cli                  Redis命令行客戶端
redis-benchmark   Redis性能測試工具
redis-check-aof     AOF文件修復工具
redis-check-dump   RDB文件檢查工具

6.系統級命令

1.keys pattern 支持正則表達式 O(n)數組

*  0到無窮個字符
? 一個字符
[] 括號內的任一個字符
\x  用於轉義字符

例子:
redis>set name Jack
redis>keys *

「name」緩存

用處:能夠經過關鍵字經過模式匹配找到想要的信息

2.exists key O(1)
判斷key是否存在,存在返回1,不然返回0

3.del key 。。。 O(n)
刪除一個多個鍵,返回被刪除鍵的個數

4.type key O(1)
判斷鍵的類型
返回值:
none (key不存在)
string (字符串)
list (列表)
set (集合)
zset (有序集)
hash (哈希表)

5.randomkey O(1)
隨機返回一個鍵
返回值:
當數據庫不爲空時,返回一個 key 。
當數據庫爲空時,返回 nil 。

6.expire key seconds   O(1)
以秒爲單位
超過期間,自動刪除
返回成功爲1,不然爲0

7. pexpire key milliseconds O(1)
力度更小的時間控制,以毫秒爲單位

8.expire key timestamp O(1)
給鍵設置Unix時間戳
成功返回1,不然爲0

例子:
redis> SET cache www.google.com

OK安全

redis> EXPIREAT cache 1355292000
# 這個 key 將在 2012.12.12 過時
(integer) 1

相似的還有 pexpireat key
milliseconds-timestamp


9.ttl key O(1)
以秒爲單位,返回剩餘時間
key不存在時,返回-2
key沒有設置剩餘時間,返回-1
不然返回key的剩餘時間
相似pttl,以毫秒爲單位


10.SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]

sort key alpha 以字母方式排序,默認以數字排序

系統命令的源碼全都在db.c裏

字符串類型,一個字符串類型鍵容許存儲的數據的最大容量是512MB
static int checkStringLength(redisClient *c, long long size) {
if (size > 512*1024*1024) {
addReplyError(c,"string exceeds maximum allowed size (512MB)");
return REDIS_ERR;
}
return REDIS_OK;
}

incr key 【遞增】 ++i/i++
有些公司會用incr來生成訂單號。【原子性遞增】


字符串的一些二進制運算
一:二進制運算

1. GetBit key offset
獲取二進制中offset這個位置的「0,1」狀態。

2. SetBit key offset value
設置二進制中offset這個位置的「0,1」狀態。

3. Bitcount key
獲取二進制中1的個數。

4. BitOp operation
二進制位的操做(^ & |)

二:Redis中二進制有兩個坑。

1. 你所輸入的數字所有用char類型表示, 2 => ascII形式展示【50】,
因此2是字符的「2」。
sample:
2
0 0 1 0
bitcount= 1
錯。
bitcount=3。

2. 通常來講2這個字符的二進制位,採用的順序是(從右到左。)
可是在redis是(從左到右)計數,0位是最左邊

2=> '2' => 50
=>
00110010 32+16+2

3. setbit num1 7 1 =>
00110 011  32+16+2+1 => 51 ('3') //redis中從右到左計數

一:深刻剖析String類型

1.Redis採用C語言編寫。

2.C語言是怎麼標識string的呢??? char[]

3. 若是說redis採用char[] 會有什麼樣的弊端

StrLen: 是否是要遍歷 char[] 數組 O(N)

Append:
是否是也須要遍歷char[],而後在char[]中追加。 O(N)

Redis的目標就是高性能,高併發。

4. Redis的解決方案呢??? 本身封裝了一下char[],封裝類型SDS (簡單對象類型)
struct sdshdr {
unsigned int len;  //記錄char[]實際使用的長度<=buf[].length
unsigned int free;  //free記錄着buf[]中未使用的坑位。
char buf[];
};

5. StrLen =>
return sh->len;  O(1)

6. 看看string類型如何包裝sds。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS;
int refcount;
//垃圾回收(GC)
void *ptr;
} robj;

7.redis內部數據結構

redisObject 是Redis 類型系統的核心,數據庫中的每一個鍵、值,以及Redis 自己處理的參數,
都表示爲這種數據類型。redisObject 的定義位於redis.h :

typedef struct redisObject {// 類型unsigned type:4;// 對齊位unsigned notused:2;// 編碼方式unsigned encoding:4;// LRU 時間(相對於server.lruclock)unsigned lru:22;// 引用計數int refcount;// 指向對象的值void *ptr;} robj;複製代碼

type 、encoding 和ptr 是最重要的三個屬性。
type 記錄了對象所保存的值的類型,它的值多是如下常量的其中一個(定義位於redis.h):

#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表複製代碼

encoding 記錄了對象所保存的值的編碼,它的值多是如下常量的其中一個(定義位於
redis.h):

#define REDIS_ENCODING_RAW 0 // 編碼爲字符串#define REDIS_ENCODING_INT 1 // 編碼爲整數#define REDIS_ENCODING_HT 2 // 編碼爲哈希表#define REDIS_ENCODING_ZIPMAP 3 // 編碼爲zipmap#define REDIS_ENCODING_LINKEDLIST 4 // 編碼爲雙端鏈表#define REDIS_ENCODING_ZIPLIST 5 // 編碼爲壓縮列表#define REDIS_ENCODING_INTSET 6 // 編碼爲整數集合#define REDIS_ENCODING_SKIPLIST 7 // 編碼爲跳躍表複製代碼

ptr 是一個指針,指向實際保存值的數據結構,這個數據結構由type 屬性和encoding 屬性決
定。
舉個例子, 若是一個redisObject 的type 屬性爲REDIS_LIST , encoding 屬性爲
REDIS_ENCODING_LINKEDLIST ,那麼這個對象就是一個Redis 列表,它的值保存在一個雙
端鏈表內,而ptr 指針就指向這個雙端鏈表;

不一樣鍵值類型底層數據結構以下bash


[string類型]

底層有兩種不一樣數據結構

  • long類型整數
  • 簡單動態字符串(sds)

字符串類型分別使用REDIS_ENCODING_INT 和REDIS_ENCODING_RAW 兩種編碼:
• REDIS_ENCODING_INT 使用long 類型來保存long 類型值。
• REDIS_ENCODING_RAW 則使用sdshdr 結構來保存sds (也便是char* )、long long 、
double 和long double 類型值。
換句話來講,在Redis 中,只有能表示爲long 類型的值,纔會以整數的形式保存,其餘類型
的整數、小數和字符串,都是用sdshdr 結構來保存。
新建立的字符串默認使用REDIS_ENCODING_RAW 編碼,在將字符串做爲鍵或者值保存進數據庫時,程序會嘗試將字符串轉爲REDIS_ENCODING_INT 編碼。

sds的實現

typedef char *sds;
struct sdshdr {
    int len;// buf 已佔用長度
    int free;// buf 剩餘可用長度
    char buf[];// 實際保存字符串數據的地方
};複製代碼

sds.c/sdsMakeRoomFor 函數描述了sdshdr 的這種 內存預分配優化策略,如下是這個函數的
僞代碼版本:

def sdsMakeRoomFor(sdshdr, required_len):# 預分配空間足夠,無須再進行空間分配if (sdshdr.free >= required_len):return sdshdr# 計算新字符串的總長度newlen = sdshdr.len + required_len# 若是新字符串的總長度小於SDS_MAX_PREALLOC# 那麼爲字符串分配2 倍於所需長度的空間# 不然就分配所需長度加上SDS_MAX_PREALLOC 數量的空間if newlen < SDS_MAX_PREALLOC:newlen *= 2else:newlen += SDS_MAX_PREALLOC# 分配內存newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)# 更新free 屬性newsh.free = newlen - sdshdr.len# 返回return newsh複製代碼

在目前版本的Redis 中,SDS_MAX_PREALLOC 的值爲1024 * 1024 ,也就是說,當大小小於
1MB 的字符串執行追加操做時,sdsMakeRoomFor 就爲它們分配多於所需大小一倍的空間;當
字符串的大小大於1MB ,那麼sdsMakeRoomFor 就爲它們額外多分配1MB 的空間。

Note: 這種分配策略會浪費內存嗎?
執行過APPEND 命令的字符串會帶有額外的預分配空間,這些預分配空間不會被釋放,除非
該字符串所對應的鍵被刪除,或者等到關閉Redis 以後,再次啓動時從新載入的字符串對象將
不會有預分配空間。
由於執行APPEND 命令的字符串鍵數量一般並很少,佔用內存的體積一般也不大,因此這一
般並不算什麼問題。
另外一方面,若是執行APPEND 操做的鍵不少,而字符串的體積又很大的話,那可能就須要修
改Redis 服務器,讓它定時釋放一些字符串鍵的預分配空間,從而更有效地使用內存。

Redis獲取字符串長度的複雜度是O(1)
對比C 字符串,sds 有如下特性:
– 能夠高效地執行長度計算(strlen);
– 能夠高效地執行追加操做(append);
– 二進制安全;
sds 會爲追加操做進行優化:加快追加操做的速度,並下降內存分配的次數,代價是多佔
用了一些內存,並且這些內存不會被主動釋放。

list列表結構

底層採用兩種數據結構:

  • 雙端鏈表(linkedlist)
  • 壓縮列表(ziplist)
由於 雙端鏈表佔用的內存比壓縮列表要多,因此當建立新的列表鍵時,列表會優先考慮使用壓
縮列表做爲底層實現,而且在有須要的時候,才從壓縮列表實現轉換到雙端鏈表實現。

除了實現list類型之外,雙端鏈表還被不少Redis 內部模塊所應用:
• 事務模塊使用雙端鏈表來按順序保存輸入的命令
• 服務器模塊使用雙端鏈表來保存多個客戶端
• 訂閱/發送模塊使用雙端鏈表來保存訂閱模式的多個客戶端
• 事件模塊使用雙端鏈表來保存時間事件(time event)

雙端鏈表的實現由listNode 和list 兩個數據結構構成,下圖展現了由這兩個結構組成的一
個雙端鏈表實例:


其中,listNode 是雙端鏈表的節點:

typedef struct listNode {// 前驅節點struct listNode *prev;// 後繼節點struct listNode *next;// 值void *value;} listNode;複製代碼

而list 則是雙端鏈表自己:

typedef struct list {// 表頭指針listNode *head;// 表尾指針listNode *tail;// 節點數量unsigned long len;// 複製函數void *(*dup)(void *ptr);// 釋放函數void (*free)(void *ptr);// 比對函數int (*match)(void *ptr, void *key);} list;複製代碼

注意,listNode 的value 屬性的類型是void * ,說明這個雙端鏈表對節點所保存的值的類型
不作限制。
對於不一樣類型的值,有時候須要不一樣的函數來處理這些值,所以,list 類型保留了三個函數指
針——dup 、free 和match ,分別用於處理值的複製、釋放和對比匹配。在對節點的值進行處理時,若是有給定這些函數,那麼它們就會被調用。舉個例子:當刪除一個listNode 時,若是包含這個節點的list 的list->free 函數不爲空,那麼刪除函數就會先調用list->free(listNode->value)清空節點的值,再執行餘下的刪除操做(好比說,釋放節點)。
從這兩個數據結構的定義上,也能夠它們的一些行爲和性能特徵:
• listNode 帶有prev 和next 兩個指針,所以,對鏈表的遍歷能夠在兩個方向上進行:從
表頭到表尾,或者從表尾到表頭。
• list 保存了head 和tail 兩個指針,所以,對鏈表的表頭和表尾進行插入的複雜度都爲O(1) —— 這是高效實現LPUSH 、RPOP 、RPOPLPUSH 等命令的關鍵
• list 帶有 保存節點數量的len 屬性,因此計算 鏈表長度的複雜度僅爲O(1) ,這也保證了LEN 命令不會成爲性能瓶頸。
Redis 爲雙端鏈表實現了一個迭代器,這個迭代器能夠從兩個方向對雙端鏈表進行迭代:
• 沿着節點的next 指針前進,從表頭向表尾迭代;
• 沿着節點的prev 指針前進,從表尾向表頭迭代;
如下是迭代器的數據結構定義:

typedef struct listIter {// 下一節點listNode *next;// 迭代方向int direction;} listIter;複製代碼

direction 記錄迭代應該從那裏開始:
• 若是值爲adlist.h/AL_START_HEAD ,那麼迭代器執行從表頭到表尾的迭代;
• 若是值爲adlist.h/AL_START_TAIL ,那麼迭代器執行從表尾到表頭的迭代;

雙端鏈表及其節點的性能特性以下
– 節點帶有前驅和後繼指針, 訪問前驅節點和後繼節點的複雜度爲O(1) ,而且對鏈表
的迭代能夠在從表頭到表尾和從表尾到表頭兩個方向進行;
– 鏈表帶有指向表頭和表尾的指針,所以 對錶頭和表尾進行處理的複雜度爲O(1)
– 鏈表帶有記錄節點數量的屬性,因此能夠 在O(1) 複雜度內返回鏈表的節點數量(長
度);

壓縮列表


ziplist不是結構體組成,建立邏輯很簡單,就是申請固定的包含頭尾節點的空間,而後初始化鏈表上下文

//定義由zlbytes,zltail跟zllen組成的壓縮鏈表的頭大小
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))

//建立一個壓縮鏈表,而且返回指向該鏈表的指針
unsigned char *ziplistNew(void) {
    //這裏之因此+1是由於尾元素佔用一個字節,這也是一個壓縮鏈表最小尺寸
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
    //分配內存
    unsigned char *zl = zmalloc(bytes);
    //設置鏈表大小
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    //設置最後一個元素的偏移量
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    //設置元素個數
    ZIPLIST_LENGTH(zl) = 0;
    //設置尾元素(上面只是申請空間)
    zl[bytes-1] = ZIP_END;
    return zl;
}複製代碼

由於ziplist header 部分的長度老是固定的(4 字節+ 4 字節+ 2 字節),所以將指針移動到表
頭節點的複雜度爲常數時間;除此以外,由於表尾節點的地址能夠經過zltail 計算得出,因
此將指針移動到表尾節點的複雜度也爲常數時間

由於ziplist 由連續的內存塊構成,在最壞狀況下,當ziplistPush 、ziplistDelete 這類對
節點進行增長或刪除的函數以後,程序須要執行一種稱爲連鎖更新的動做來維持ziplist 結構本
身的性質,因此這些函數的最壞複雜度都爲O(N2) 。不過,由於這種最壞狀況出現的機率並不
高,因此大能夠放心使用ziplist ,而沒必要太擔憂出現最壞狀況


encoding 和length 兩部分一塊兒決定了content 部分所保存的數據的類型(以及長度)。
其中,encoding 域的長度爲兩個bit ,它的值能夠是00 、01 、10 和11 :
• 00 、01 和10 表示content 部分保存着字符數組。
• 11 表示content 部分保存着整數

壓縮列表每一個元素的previous_entry_length字段存儲的是前一個元素的長度,所以壓縮列表的 前向遍歷相對簡單,表達式(p-previous_entry_length)便可獲取前一個元素的首地址,這裏不作詳述。 後向遍歷時,須要解碼當前元素,計算當前元素長度,才能獲取後一個元素首地址;ziplistNext函數實現以下

unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
    //zl參數無用;這裏只是爲了不警告
    ((void) zl);
 
    if (p[0] == ZIP_END) {
        return NULL;
    }
 
    p += zipRawEntryLength(p);
    if (p[0] == ZIP_END) {
        return NULL;
    }
 
    return p;
}複製代碼

壓縮列表內部節點長度是可變(按需所取)的,因此才能最大化的利用空間節省內存,壓縮列表是申請一大塊 連續內存存儲全部節點, 雙端列表內存是不連續的,因此容易形成內存碎片


哈希類型hash

底層有兩種類型

  • 壓縮列表(ziplist)
  • 字典(dict)

由於壓縮列表比字典更節省內存,因此程序在建立新Hash 值時,默認使用壓縮列表做爲底層
實現,當有須要時,程序纔會將底層實現從壓縮列表轉換到字典



上面的key value應該是field value

字典的主要用途有如下兩個:

1. 實現數據庫 鍵空間(key space)
2. 用做Hash 類型值的其中一種底層實現
數據庫中的鍵值對就由字典保存:每一個數據庫都有一個與之相對應的字典,這個字典被稱之爲鍵空間(key space),因此使用命令exist key 的複雜度是O(1)

Redis 選擇了高效且實現簡單的哈希表做爲字典的底層實現,dict.h/dict 給出了字典的定義:

typedef struct dict {
// 特定於類型的處理函數
dictType *type;
// 類型處理函數的私有數據
void *privdata;
// 哈希表(2 個)
dictht ht[2];
// 記錄rehash 進度的標誌,值爲-1 表示rehash 未進行
int rehashidx;
// 當前正在運做的安全迭代器數量
int iterators;
} dict;複製代碼
注意dict 類型使用了兩個指針分別指向兩個哈希表。其中,0 號哈希表(ht[0])是字典主要使用的哈希表,而1 號哈希表(ht[1])則只有在程序對0 號哈希表進行rehash 時才使用
字典所使用的哈希表實現由dict.h/dictht 類型定義:

typedef struct dictht {
// 哈希表節點指針數組(俗稱桶,bucket)
dictEntry **table;
// 指針數組的大小
unsigned long size;
// 指針數組的長度掩碼,用於計算索引值
unsigned long sizemask;
// 哈希表現有的節點數量
unsigned long used;
} dictht;複製代碼
table 屬性是一個數組,數組的每一個元素都是一個指向dictEntry 結構的指針。每一個dictEntry 都保存着一個鍵值對,以及一個指向另外一個dictEntry 結構的指針:

typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 鏈日後繼節點
struct dictEntry *next;
} dictEntry;複製代碼
next 屬性指向另外一個dictEntry 結構,多個dictEntry 能夠經過next 指針串連成鏈表,從
這裏能夠看出,dictht 使用鏈地址法來處理鍵碰撞:當多個不一樣的鍵擁有相同的哈希值時,哈
希表用一個鏈表將這些鍵鏈接起來。
整個字典結構能夠表示以下:


Redis 目前使用兩種不一樣的哈希算法:
1. MurmurHash2 32 bit 算法:這種算法的分佈率和速度都很是好,具體信息請參考MurmurHash
的主頁:http://code.google.com/p/smhasher/ 。
2. 基於djb 算法實現的一個大小寫無關散列算法: 具體信息請參考
http://www.cse.yorku.ca/~oz/hash.html 。
使用哪一種算法取決於具體應用所處理的數據:
• 命令表以及Lua 腳本緩存都用到了算法2 。
• 算法1 的應用則更加普遍:數據庫、集羣、哈希鍵、阻塞操做等功能都用到了這個算法

• 字典由鍵值對構成的抽象數據結構。
• Redis 中的數據庫和哈希鍵都基於字典來實現。
• Redis 字典的底層實現爲哈希表,每一個字典使用兩個哈希表,通常狀況下只使用0 號哈希
表,只有在rehash 進行時,纔會同時使用0 號和1 號哈希表。
• 哈希表使用鏈地址法來解決鍵衝突的問題。
• Rehash 能夠用於擴展或收縮哈希表。
• 對哈希表的rehash 是分屢次、漸進式地進行的。

集合set

底層兩種數據結構

  • 整數集合(intset)
  • 字典(dict)
使用字典存儲集合元素時,將 元素值設爲字典鍵字典值爲空

整數集合(intset)是集合鍵的底層實現之一,若是一個集合:

1. 只保存着整數元素;
2. 元素的數量很少;
那麼Redis 就會使用intset 來保存集合元素
如下是intset.h/intset 類型的定義:

typedef struct intset {
// 保存元素所使用的類型的長度
uint32_t encoding;
// 元素個數
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;複製代碼

encoding 的值能夠是如下三個常量的其中一個(定義位於intset.c ):

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))複製代碼

contents 數組是實際保存元素的地方,數組中的元素有如下兩個特性:
• 沒有重複元素;
• 元素在數組中從小到大排列;
contents 數組的int8_t 類型聲明比較容易讓人誤解,實際上,intset 並不使用int8_t 類型
來保存任何元素,結構中的這個類型聲明只是做爲一個佔位符使用:在對contents 中的元素
進行讀取或者寫入時,程序並非直接使用contents 來對元素進行索引,而是根據encoding
的值,對contents 進行類型轉換和指針運算,計算出元素在內存中的正確位置。在添加新元
素,進行內存分配時,分配的容量也是由encoding 的值決定

移動不只出如今升級(intsetUpgradeAndAdd)操做中,還出現其餘對contents 數組內
容進行增刪的操做上,好比intsetAdd 和intsetRemove ,由於這種移動操做須要處理intset
中的全部元素,因此這些函數的複雜度都不低於O(N) 。

• Intset 用於 有序無重複地保存多個整數值,它會根據元素的值,自動選擇該用什麼長度
的整數類型來保存元素。
• 當一個位長度更長的整數值添加到intset 時,須要對intset 進行升級,新intset 中每一個
元素的位長度都等於新添加值的位長度,但原有元素的值不變。
• 升級會引發整個intset 進行內存重分配,並移動集合中的全部元素,這個操做的複雜度
爲O(N) 。
• Intset 只支持升級,不支持降級。
• Intset 是 有序的,程序使用二分查找算法來實現查找操做, 複雜度爲O(lgN)

有序集合(zset)

有兩種底層數據結構

  • 壓縮列表(ziplist)
  • 跳躍表和字典結合(skiplist+dict)
在經過ZADD 命令添加第一個元素到空key 時,程序經過檢查輸入的第一個元素來決定該創
建什麼編碼的有序集。
若是第一個元素符合如下條件的話,就建立一個REDIS_ENCODING_ZIPLIST 編碼的有序集:
• 服務器屬性server.zset_max_ziplist_entries 的值大於0 (默認爲128 )。
• 元素的member 長度小於服務器屬性server.zset_max_ziplist_value 的值(默認爲64
)。
不然,程序就建立一個REDIS_ENCODING_SKIPLIST 編碼的有序集

對於一個REDIS_ENCODING_ZIPLIST 編碼的有序集,只要知足如下任一條件,就將它轉換爲
REDIS_ENCODING_SKIPLIST 編碼:
• ziplist 所保存的元素數量超過服務器屬性server.zset_max_ziplist_entries 的值
(默認值爲128 )
• 新添加元素的member 的長度大於服務器屬性server.zset_max_ziplist_value 的值
(默認值爲64 )

當使用REDIS_ENCODING_ZIPLIST 編碼時,有序集將元素保存到ziplist 數據結構裏面
其中, 每一個有序集元素以兩個相鄰的ziplist 節點表示第一個節點保存元素的member 域,
第二個元素保存元素的score 域
多個元素之間按score 值從小到大排序,若是兩個元素的score 相同,那麼按字典序對member進行對比,決定那個元素排在前面,那個元素排在後面


雖然元素是按score 域有序排序的,但對 ziplist 的節點指針只能 線性地移動,因此在
REDIS_ENCODING_ZIPLIST 編碼的有序集中, 查找某個給定元素的複雜度爲 O(N)
每次執行 添加/刪除/更新操做都須要執行一次查找元素的操做,所以這些函數的複雜度都不低
於O(N) ,至於這些操做的實際複雜度,取決於它們底層所執行的ziplist 操做。

當使用 REDIS_ENCODING_SKIPLIST 編碼時,有序集元素由redis.h/zset 結構來保存:

typedef struct zset {// 字典dict *dict;// 跳躍表zskiplist *zsl;} zset;複製代碼

zset 同時使用 字典和跳躍表兩個數據結構來保存有序集元素。
其中,元素的成員由一個redisObject 結構表示,而元素的score 則是一個double 類型的浮
點數,字典和跳躍表兩個結構經過將指針共同指向這兩個值來節約空間(不用每一個元素都複製
兩份)。
下圖展現了一個REDIS_ENCODING_SKIPLIST 編碼的有序集



經過 使用字典結構,並將member 做爲鍵,score 做爲值,有序集能夠在O(1) 複雜度內:
檢查給定member 是否存在於有序集(被不少底層函數使用);
取出member 對應的score 值(實現ZSCORE 命令)。
另外一方面,經過 使用跳躍表,可讓有序集支持如下兩種操做:
• 在O(logN) 指望時間、O(N) 最壞時間內 根據score 對member 進行定位(被不少底層
函數使用);
• 範圍性查找和處理操做,這是(高效地)實現ZRANGE 、ZRANK 和ZINTERSTORE
等命令的關鍵。
經過 同時使用字典和跳躍表,有序集能夠高效地實現按 成員查找和按 順序查找兩種操做。

修改版的跳躍表由redis.h/zskiplist 結構定義:

typedef struct zskiplist {// 頭節點,尾節點struct zskiplistNode *header, *tail;// 節點數量unsigned long length;// 目前表內節點的最大層數int level;} zskiplist;複製代碼
跳躍表的節點由redis.h/zskiplistNode 定義:

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

• 跳躍表是一種隨機化數據結構,它的查找、添加、刪除操做均可以在對數指望時間下完

成。
• 跳躍表目前在Redis 的惟一做用就是做爲有序集類型的底層數據結構(之一,另外一個構
成有序集的結構是字典)。
• 爲了適應自身的需求,Redis 基於William Pugh 論文中描述的跳躍表進行了修改,包括:
1. score 值可重複。
2. 對比一個元素須要同時檢查它的score 和memeber 。
3. 每一個節點帶有高度爲1 層的後退指針,用於從表尾方向向表頭方向迭代

高層的指針越過的元素數量大於等於低層的指針,爲了 提升查找的效率,程序老是從高層先開始訪問,而後隨着元素值範圍的縮小,慢慢下降層 次。

數據淘汰策略

redis 內存數據集大小上升到必定大小的時候,就會施行數據淘汰策略(回收策略)。redis 提供 6種數據淘汰策略:

  1. volatile-lru:從已設置過時時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
  2. volatile-ttl:從已設置過時時間的數據集(server.db[i].expires)中挑選將要過時的數據淘汰
  3. volatile-random:從已設置過時時間的數據集(server.db[i].expires)中任意選擇數據淘汰
  4. allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
  6. no-enviction(驅逐):禁止驅逐數據

內存回收

Redis中採用兩種算法進行內存回收, 引用計數算法以及 LRU算法

引用計數算法

Redis 的對象系統使用了引用計數技術來負責維持和銷燬對象,它的

運做機制以下:
• 每一個redisObject 結構都帶有一個refcount 屬性,指示這個對象被引用了多少次。
• 當新建立一個對象時,它的refcount 屬性被設置爲1
• 當對一個對象進行共享時,Redis 將這個對象的refcount 增一。
• 當使用完一個對象以後,或者取消對共享對象的引用以後,程序將對象的refcount 減一。
• 當對象的refcount 降至0 時,這個redisObject 結構,以及它所引用的數據結構的內
存,都會被釋放。

LRU算法

在內存有限的狀況下,當內存容量不足時,爲了保證程序的運行,選擇最近一段時間內,最久未使用的對象將其淘汰

【第二章 內存映射數據結構】


二者比較:
內存映射數據結構是一系列通過特殊編碼的字節序列,建立它們所消耗的內存一般比做用相似
的內部數據結構要少得多,若是使用得當,內存映射數據結構能夠爲用戶節省大量的內存。
不過,由於內存映射數據結構的編碼和操做方式要比內部數據結構要複雜得多,因此內存映射
數據結構所佔用的CPU 時間會比做用相似的內部數據結構要多


【第三章 Redis數據類型】

Redis
有一些對象在Redis 中很是常見,好比命令的返回值OK 、ERROR 、WRONGTYPE 等字符,另外,
一些小範圍的整數,好比個位、十位、百位的整數都很是常見
爲了利用這種常見狀況,Redis 在內部使用了一個Flyweight 模式:經過預分配一些常見的值
對象,並在多個數據結構之間共享這些對象,程序避免了重複分配的麻煩,也節約了一些CPU
時間。
Redis 預分配的值對象有以下這些:
• 各類命令的返回值,好比執行成功時返回的OK ,執行錯誤時返回的ERROR ,類型錯誤時
返回的WRONGTYPE ,命令入隊事務時返回的QUEUED ,等等。
• 包括0 在內, 小於redis.h/REDIS_SHARED_INTEGERS 的全部整數
(REDIS_SHARED_INTEGERS 的默認值爲10000)

若是某個命令的輸入值是一個小於REDIS_SHARED_INTEGERS 的整數對象,那麼當這個對象要被保存進數據
庫時,Redis 就會釋放原來的值,並將值的指針指向共享對象

Note: 共享對象只能被帶指針的數據結構使用。
須要提醒的一點是,共享對象只能被字典和雙端鏈表這類能帶有指針的數據結構使用。
像整數集合和壓縮列表這些只能保存字符串、整數等字面值的內存數據結構,就不能使用共享
對象。



• Redis 使用本身實現的對象機制來實現類型判斷、命令多態和基於引用計數的垃圾回收。
• 一種Redis 類型的鍵能夠有多種底層實現。
• Redis 會預分配一些經常使用的數據對象,並經過共享這些對象來減小內存佔用,和避免頻繁
地爲小對象分配內存

redis事務

事務提供了一種「將多個命令打包,而後一次性、按順序地執行」的機制,而且事務在執行的期
間不會主動中斷——服務器在執行完事務中的全部命令以後,纔會繼續處理其餘客戶端的其餘
命令

MULTI 命令的執行標記着事務的開始:
redis> MULTI
OK
這個命令惟一作的就是,將客戶端的REDIS_MULTI 選項打開,讓客戶端從非事務狀態切換到事
務狀態

當客戶端進入事務狀態以後,服務器在收到來自客戶端的命令時,不會當即執行命令,
而是將這些命令所有放進一個事務隊列裏,事務隊列是一個數組,每一個數組項是都包含三個屬性:
1. 要執行的命令(cmd)。
2. 命令的參數(argv)。
3. 參數的個數(argc)

舉個例子,若是客戶端執行如下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那麼程序將爲客戶端建立如下事務隊列:


其實並非全部的命令都會被放進事務隊列,其中的例外就是EXEC 、DISCARD 、MULTI
和WATCH 這四個命令——當這四個命令從客戶端發送到服務器時,它們會像客戶端處於非
事務狀態同樣,直接被服務器執行
DISCARD 命令用於取消一個事務,它清空客戶端的整個事務隊列,而後將客戶端從事務狀態
調整回非事務狀態,最後返回字符串OK 給客戶端,說明事務已被取消。
Redis 的事務是不可嵌套的,當客戶端已經處於事務狀態,而客戶端又再向服務器發送MULTI
時,服務器只是簡單地向客戶端發送一個錯誤,而後繼續等待其餘命令的入隊。MULTI 命令
的發送不會形成整個事務失敗,也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行,在事務狀態下發送WATCH 命令會引起一個
錯誤,但它不會形成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理MULTI
的狀況同樣)。

WATCH 命令的實現
在每一個表明數據庫的redis.h/redisDb 結構類型中,都保存了一個watched_keys 字典,字典
的鍵是這個數據庫被監視的鍵,而字典的值則是一個鏈表,鏈表中保存了全部監視這個鍵的客
戶端。


在任何對數據庫鍵空間(key space)進行修改的命令成功執行以後(好比FLUSHDB 、SET
、DEL 、LPUSH 、SADD 、ZREM ,諸如此類),multi.c/touchWatchKey 函數都會被調用
——它檢查數據庫的watched_keys 字典,看是否有客戶端在監視已經被命令修改的鍵,若是
有的話,程序將全部監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS 選項打開

當客戶端發送EXEC 命令、觸發事務執行時,服務器會對客戶端的狀態進行檢查:
• 若是客戶端的REDIS_DIRTY_CAS 選項已經被打開,那麼說明被客戶端監視的鍵至少有一
個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端
返回空回覆,表示事務執行失敗。
• 若是REDIS_DIRTY_CAS 選項沒有被打開,那麼說明全部監視鍵都安全,服務器正式執行
事務。

最後,當一個客戶端結束它的事務時,不管事務是成功執行,仍是失敗,watched_keys 字典
中和這個客戶端相關的資料都會被清除

在傳統的關係式數據庫中,經常用ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)

單個Redis 命令的執行是原子性的,但Redis 沒有在事務上增長任何維持原子性的機制,因此
Redis 事務的執行並非原子性的
若是一個事務隊列中的全部命令都被成功地執行,那麼稱這個事務執行成功。
另外一方面,若是Redis 服務器進程在執行事務的過程當中被中止——好比接到KILL 信號、宿主
機器停機,等等,那麼事務執行失敗。
當事務失敗時, Redis 也不會進行任何的重試或者回滾動做

Redis 的一致性問題能夠分爲三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。
入隊錯誤
在命令入隊的過程當中, 若是客戶端向服務器發送了錯誤的命令, 好比命令的參數數量
不對,等等,那麼服務器將向客戶端返回一個出錯信息,而且將客戶端的事務狀態設爲
REDIS_DIRTY_EXEC 。
當客戶端執行EXEC 命令時,Redis 會拒絕執行狀態爲REDIS_DIRTY_EXEC 的事務,並返回失
敗信息。
所以,帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。

執行錯誤
若是命令在事務執行的過程當中發生錯誤,好比說,對一個不一樣類型的key 執行了錯誤的操做,
那麼Redis 只會將錯誤包含在事務的結果中,這不會引發事務中斷或整個失敗,不會影響已執
行事務命令的結果,也不會影響後面要執行的事務命令,因此它對事務的一致性也沒有影響。

Redis 進程被終結
若是Redis 服務器進程在執行事務的過程當中被其餘進程終結,或者被管理員強制殺死,那麼根
據Redis 所使用的持久化模式,可能有如下狀況出現:
• 內存模式:若是Redis 沒有采起任何持久化機制,那麼重啓以後的數據庫老是空白的,所
以數據老是一致的。
• RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存RDB 的工做,只有在事務執
行以後,保存RDB 的工做纔有可能開始。因此當RDB 模式下的Redis 服務器進程在事
務中途被殺死時,事務內執行的命令,無論成功了多少,都不會被保存到RDB 文件裏。
恢復數據庫須要使用現有的RDB 文件,而這個RDB 文件的數據保存的是最近一次的數
據庫快照(snapshot),因此它的數據可能不是最新的,但只要RDB 文件自己沒有由於
其餘問題而出錯,那麼還原後的數據庫就是一致的。
• AOF 模式:由於保存AOF 文件的工做在後臺線程進行,因此即便是在事務執行的中途,
保存AOF 文件的工做也能夠繼續進行,所以,根據事務語句是否被寫入並保存到AOF
文件,有如下兩種狀況發生:
1)若是事務語句未寫入到AOF 文件,或AOF 未被SYNC 調用保存到磁盤,那麼當進
程被殺死以後,Redis 能夠根據最近一次成功保存到磁盤的AOF 文件來還原數據庫,只
要AOF 文件自己沒有由於其餘問題而出錯,那麼還原後的數據庫老是一致的,但其中的
數據不必定是最新的。
2)若是事務的部分語句被寫入到AOF 文件,而且AOF 文件被成功保存,那麼不完整的
事務執行信息就會遺留在AOF 文件裏,當重啓Redis 時,程序會檢測到AOF 文件並不
完整,Redis 會退出,並報告錯誤。須要使用redis-check-aof 工具將部分紅功的事務命令
移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直
到事務執行以前爲止)。

隔離性(Isolation)
Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執
行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。

持久性(Durability)
由於事務不過是用隊列包裹起了一組Redis 命令,並無提供任何額外的持久性功能,因此事
務的持久性由Redis 所使用的持久化模式決定:
• 在單純的內存模式下,事務確定是不持久的。
• 在RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,所
以RDB 模式下的Redis 事務也是不持久的。
• 在AOF 的「老是SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用fsync
或fdatasync 將事務數據寫入到AOF 文件。可是,這種保存是由後臺線程進行的,主
線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段
很是小的間隔,因此這種模式下的事務也是不持久的。
其餘AOF 模式也和「老是SYNC 」模式相似,因此它們都是不持久的

• 事務提供了一種將多個命令打包,而後一次性、有序地執行的機制。
• 事務在執行過程當中不會被中斷,全部事務命令執行完以後,事務才能結束。
• 多個命令會被入隊到事務隊列中,而後按先進先出(FIFO)的順序執行。
• 帶WATCH 命令的事務會將客戶端和被監視的鍵在數據庫的watched_keys 字典中進行關
聯,當鍵被修改時,程序會將全部監視被修改鍵的客戶端的REDIS_DIRTY_CAS 選項打開。
• 只有在客戶端的REDIS_DIRTY_CAS 選項未被打開時,才能執行事務,不然事務直接返回
失敗。
• Redis 的事務保證了ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和
持久性(D)

redis頻道與訂閱

每一個Redis 服務器進程都維持着一個表示服務器狀態的redis.h/redisServer 結構,結構的
pubsub_channels 屬性是一個字典,這個字典就用於保存訂閱頻道的信息:

struct redisServer {
// ...
dict *pubsub_channels;
// ...
};複製代碼

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


當客戶端調用SUBSCRIBE 命令時,程序就將客戶端和要訂閱的頻道在pubsub_channels 字
典中關聯起來。
瞭解了pubsub_channels 字典的結構以後,解釋PUBLISH 命令的實現就很是簡單了:當調
用PUBLISH channel message 命令,程序首先根據channel 定位到字典的鍵,而後將信息發
送給字典值鏈表中的全部客戶端。
使用UNSUBSCRIBE 命令能夠退訂指定的頻道,這個命令執行的是訂閱的反操做:它從
pubsub_channels 字典的給定頻道(鍵)中,刪除關於當前客戶端的信息,這樣被退訂頻道的
信息就不會再發送給這個客戶端。

struct redisServer {
// ...
list *pubsub_patterns;
// ...
};複製代碼

訂閱模式
PSUBSCRIBE
redisServer.pubsub_patterns 屬性是一個鏈表,鏈表中保存着全部和模式相關的信息:

鏈表中的每一個節點都包含一個redis.h/pubsubPattern 結構

typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;複製代碼

client 屬性保存着訂閱模式的客戶端,而pattern 屬性則保存着被訂閱的模式。
每當調用PSUBSCRIBE 命令訂閱一個模式時,程序就建立一個包含客戶端信息和被訂閱模式的
pubsubPattern 結構,並將該結構添加到redisServer.pubsub_patterns 鏈表中。
做爲例子,下圖展現了一個包含兩個模式的pubsub_patterns 鏈表,其中client123 和
client256 都正在訂閱tweet.shop.* 模式


經過遍歷整個pubsub_patterns 鏈表,程序能夠檢查全部正在被訂閱的模式,以及訂閱這些模
式的客戶端。

退訂模式
使用PUNSUBSCRIBE 命令能夠退訂指定的模式,這個命令執行的是訂閱模式的反操做:程序
會刪除redisServer.pubsub_patterns 鏈表中,全部和被退訂模式相關聯的pubsubPattern
結構,這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。

• 訂閱信息由服務器進程維持的redisServer.pubsub_channels 字典保存,字典的鍵爲被
訂閱的頻道,字典的值爲訂閱頻道的全部客戶端。
• 當有新消息發送到頻道時,程序遍歷頻道(鍵)所對應的(值)全部客戶端,而後將消息
發送到全部訂閱頻道的客戶端上。
• 訂閱模式的信息由服務器進程維持的redisServer.pubsub_patterns 鏈表保存,鏈表的
每一個節點都保存着一個pubsubPattern 結構,結構中保存着被訂閱的模式,以及訂閱該
模式的客戶端。程序經過遍歷鏈表來查找某個頻道是否和某個模式匹配。
• 當有新消息發送到頻道時,除了訂閱頻道的客戶端會收到消息以外,全部訂閱了匹配頻
道的模式的客戶端,也一樣會收到消息。
• 退訂頻道和退訂模式分別是訂閱頻道和訂閱模式的反操做。

慢日誌

每條慢查詢日誌都以一個slowlog.h/slowlogEntry 結構定義:
typedef struct slowlogEntry {
// 命令參數
robj **argv;
// 命令參數數量
int argc;
// 惟一標識符
long long id;
// 執行命令消耗的時間,以納秒(1 / 1,000,000,000 秒)爲單位
long long duration;
// 命令執行時的時間
time_t time;
} slowlogEntry;

記錄服務器狀態的redis.h/redisServer 結構裏保存了幾個和慢查詢有關的屬性:
struct redisServer {
// ... other fields
// 保存慢查詢日誌的鏈表
list *slowlog;
// 慢查詢日誌的當前id 值
long long slowlog_entry_id;
// 慢查詢時間限制
long long slowlog_log_slower_than;
// 慢查詢日誌的最大條目數量
unsigned long slowlog_max_len;
// ... other fields
};
slowlog 屬性是一個鏈表,鏈表裏的每一個節點保存了一個慢查詢日誌結構,全部日誌按添加時
間重新到舊排序,新的日誌在鏈表的左端,舊的日誌在鏈表的右端。
slowlog_entry_id 在建立每條新的慢查詢日誌時增一,用於產生慢查詢日誌的ID (這個ID
在執行SLOWLOG RESET 以後會被重置)。
slowlog_log_slower_than 是用戶指定的命令執行時間上限,執行時間大於等於這個值的命令
會被慢查詢日誌記錄。
slowlog_max_len 慢查詢日誌的最大數量,當日志數量等於這個值時,添加一條新日誌會形成
最舊的一條日誌被刪除。

在每次執行命令以前,Redis 都會用一個參數記錄命令執行前的時間,在命令執行完以後,再
計算一次當前時間,而後將兩個時間值相減,得出執行命令所耗費的時間值duration ,並將
duration 傳給slowlogPushEntryIfNeed 函數。

針對慢查詢日誌有三種操做,分別是查看、清空和獲取日誌數量:
• 查看日誌:在日誌鏈表中遍歷指定數量的日誌節點,複雜度爲O(N) 。
• 清空日誌:釋放日誌鏈表中的全部日誌節點,複雜度爲O(N) 。
• 獲取日誌數量:獲取日誌的數量等同於獲取server.slowlog 鏈表的數量,複雜度爲
O(1) 。

• Redis 用一個鏈表以FIFO 的順序保存着全部慢查詢日誌。
• 每條慢查詢日誌以一個慢查詢節點表示,節點中記錄着執行超時的命令、命令的參數、命
令執行時的時間,以及執行命令所消耗的時間等信息。

【第五章 內部運做機制】

Redis 中的每一個數據庫,都由一個redis.h/redisDb 結構表示:
typedef struct redisDb {
// 保存着數據庫以整數表示的號碼
int id;
// 保存着數據庫中的全部鍵值對數據
// 這個屬性也被稱爲鍵空間(key space)
dict *dict;
// 保存着鍵的過時信息
dict *expires;
// 實現列表阻塞原語,如BLPOP
// 在列表類型一章有詳細的討論
dict *blocking_keys;
dict *ready_keys;
// 用於實現WATCH 命令
// 在事務章節有詳細的討論
dict *watched_keys;
} redisDb;

redisDb 結構的id 域保存着數據庫的號碼。
這個號碼很容易讓人將它和切換數據庫的SELECT 命令聯繫在一塊兒,可是,實際上,id 屬性
並非用來實現SELECT 命令,而是給Redis 內部程序使用的。
當Redis 服務器初始化時, 它會建立出redis.h/REDIS_DEFAULT_DBNUM 個數據庫, 並
將全部數據庫保存到redis.h/redisServer.db 數組中, 每一個數據庫的id 爲從0 到
REDIS_DEFAULT_DBNUM - 1 的值。
當執行SELECT number 命令時,程序直接使用redisServer.db[number] 來切換數據庫。
可是,一些內部程序,好比AOF 程序、複製程序和RDB 程序,須要知道當前數據庫的號碼,
若是沒有id 域的話,程序就只能在當前使用的數據庫的指針,和redisServer.db 數組中所
有數據庫的指針進行對比,以此來弄清楚本身正在使用的是那個數據庫。

在數據庫中,全部鍵的過時時間都被保存在redisDb 結構的expires 字典裏:

typedef struct redisDb {
// ...
dict *expires;
// ...
} redisDb;複製代碼

expires 字典的鍵是一個指向dict 字典(鍵空間)裏某個鍵的指針,而字典的值則是鍵所指
向的數據庫鍵的到期時間,這個值以long long 類型表示。
下圖展現了一個含有三個鍵的數據庫,其中number 和book 兩個鍵帶有過時時間:


Redis 有四個命令能夠設置鍵的生存時間(能夠存活多久)和過時時間(何時到期):
• EXPIRE 以秒爲單位設置鍵的生存時間;
• PEXPIRE 以毫秒爲單位設置鍵的生存時間;
• EXPIREAT 以秒爲單位,設置鍵的過時UNIX 時間戳;
• PEXPIREAT 以毫秒爲單位,設置鍵的過時UNIX 時間戳。
雖然有那麼多種不一樣單位和不一樣形式的設置方式,可是expires 字典的值只保存「以毫秒爲單
位的過時UNIX 時間戳」 ,這就是說,經過進行轉換,全部命令的效果最後都和PEXPIREAT
命令的效果同樣。

舉個例子,從EXPIRE 命令到PEXPIREAT 命令的轉換能夠用僞代碼表示以下:
def EXPIRE(key, sec):
# 將TTL 從秒轉換爲毫秒
ms = sec_to_ms(sec)
# 獲取以毫秒計算的當前UNIX 時間戳
ts_in_ms = get_current_unix_timestamp_in_ms()
# 毫秒TTL 加上毫秒時間戳,就是key 到期的時間戳
PEXPIREAT(ms + ts_in_ms, key)
其餘函數的轉換方式也是相似的。
做爲例子,下圖展現了一個expires 字典示例,字典中number 鍵的過時時間是2013 年2 月
10 日(農曆新年),而book 鍵的過時時間則是2013 年2 月14 日(情人節):


經過expires 字典,能夠用如下步驟檢查某個鍵是否過時:
1. 檢查鍵是否存在於expires 字典:若是存在,那麼取出鍵的過時時間;
2. 檢查當前UNIX 時間戳是否大於鍵的過時時間:若是是的話,那麼鍵已通過期;不然,
鍵未過時。
能夠用僞代碼來描述這一過程:

def is_expired(key):
# 取出鍵的過時時間
key_expire_time = expires.get(key)
# 若是過時時間不爲空,而且當前時間戳大於過時時間,那麼鍵已通過期
if expire_time is not None and current_timestamp() > key_expire_time:
return True
# 不然,鍵未過時或沒有設置過時時間
return False複製代碼

咱們知道了過時時間保存在expires 字典裏,又知道了該如何斷定一個鍵是否過時,如今剩下
的問題是,

過時鍵刪除

若是一個鍵是過時的,那它何時會被刪除

這個問題有三種可能的答案:
1. 定時刪除:在設置鍵的過時時間時,建立一個定時事件,當過時時間到達時,由事件處理
器自動執行鍵的刪除操做。
2. 惰性刪除:聽任鍵過時無論,可是在每次從dict 字典中取出鍵值時,要檢查鍵是否過
期,若是過時的話,就刪除它,並返回空;若是沒過時,就返回鍵值。
3. 按期刪除:每隔一段時間,對expires 字典進行檢查,刪除裏面的過時鍵。

定時刪除
定時刪除策略對內存是最友好的:由於它保證過時鍵會在第一時間被刪除,過時鍵所消耗的內
存會當即被釋放。
這種策略的缺點是,它對CPU 時間是最不友好的:由於刪除操做可能會佔用大量的CPU 時間
——在內存不緊張、可是CPU 時間很是緊張的時候(好比說,進行交集計算或排序的時候),
將CPU 時間花在刪除那些和當前任務無關的過時鍵上,這種作法毫無疑問會是低效的。
除此以外,目前Redis 事件處理器對時間事件的實現方式——無序鏈表,查找一個時間複雜度
爲O(N) ——並不適合用來處理大量時間事件。

惰性刪除
惰性刪除對CPU 時間來講是最友好的:它只會在取出鍵時進行檢查,這能夠保證刪除操做只
會在非作不可的狀況下進行——而且刪除的目標僅限於當前處理的鍵,這個策略不會在刪除其
他無關的過時鍵上花費任何CPU 時間。
惰性刪除的缺點是,它對內存是最不友好的:若是一個鍵已通過期,而這個鍵又仍然保留在數
據庫中,那麼dict 字典和expires 字典都須要繼續保存這個鍵的信息,只要這個過時鍵不被
刪除,它佔用的內存就不會被釋放。
在使用惰性刪除策略時,若是數據庫中有很是多的過時鍵,但這些過時鍵又正好沒有被訪問的
話,那麼它們就永遠也不會被刪除(除非用戶手動執行),這對於性能很是依賴於內存大小的
Redis 來講,確定不是一個好消息。
舉個例子,對於一些按時間點來更新的數據,好比日誌(log),在某個時間點以後,對它們的訪
問就會大大減小,若是大量的這些過時數據積壓在數據庫裏面,用戶覺得它們已通過期了(已
經被刪除了),但實際上這些鍵卻沒有真正的被刪除(內存也沒有被釋放),那結果確定是很是
糟糕。

按期刪除
從上面對定時刪除和惰性刪除的討論來看,這兩種刪除方式在單一使用時都有明顯的缺陷:定
時刪除佔用太多CPU 時間,惰性刪除浪費太多內存。
按期刪除是這兩種策略的一種折中:
• 它每隔一段時間執行一次刪除操做,並經過限制刪除操做執行的時長和頻率,籍此來減
少刪除操做對CPU 時間的影響。
• 另外一方面,經過按期刪除過時鍵,它有效地減小了因惰性刪除而帶來的內存浪費。

Redis 使用的過時鍵刪除策略是惰性刪除加上按期刪除,這兩個策略相互配合,能夠很好地在
合理利用CPU 時間和節約內存空間之間取得平衡。

實現過時鍵惰性刪除策略的核心是db.c/expireIfNeeded 函數——全部命令在讀取或寫入數
據庫以前,程序都會調用expireIfNeeded 對輸入鍵進行檢查,並將過時鍵刪除

對過時鍵的按期刪除由redis.c/activeExpireCycle 函執行:每當Redis 的例行處理程序
serverCron 執行時,activeExpireCycle 都會被調用——這個函數在規定的時間限制內,盡
可能地遍歷各個數據庫的expires 字典,隨機地檢查一部分鍵的過時時間,並刪除其中的過時
鍵。

過時鍵對AOF 、RDB 和複製的影響

更新後的RDB 文件
在建立新的RDB 文件時,程序會對鍵進行檢查,過時的鍵不會被寫入到更新後的RDB 文件
中。
所以,過時鍵對更新後的RDB 文件沒有影響

AOF 文件
在鍵已通過期,可是尚未被惰性刪除或者按期刪除以前,這個鍵不會產生任何影響,AOF 文
件也不會由於這個鍵而被修改。
當過時鍵被惰性刪除、或者按期刪除以後,程序會向AOF 文件追加一條DEL 命令,來顯式地
記錄該鍵已被刪除。
舉個例子,若是客戶端使用GET message 試圖訪問message 鍵的值,但message 已通過期了,
那麼服務器執行如下三個動做:
1. 從數據庫中刪除message ;
2. 追加一條DEL message 命令到AOF 文件;
3. 向客戶端返回NIL

AOF 重寫
和RDB 文件相似,當進行AOF 重寫時,程序會對鍵進行檢查,過時的鍵不會被保存到重寫
後的AOF 文件。
所以,過時鍵對重寫後的AOF 文件沒有影響。

複製
當服務器帶有附屬節點時,過時鍵的刪除由主節點統一控制:
• 若是服務器是主節點,那麼它在刪除一個過時鍵以後,會顯式地向全部附屬節點發送一
個DEL 命令。
• 若是服務器是附屬節點,那麼當它碰到一個過時鍵的時候,它會向程序返回鍵已過時的
回覆,但並不真正的刪除過時鍵。由於程序只根據鍵是否已通過期、而不是鍵是否已經被
刪除來決定執行流程,因此這種處理並不影響命令的正確執行結果。當接到從主節點發來
的DEL 命令以後,附屬節點纔會真正的將過時鍵刪除掉。
附屬節點不自主對鍵進行刪除是爲了和主節點的數據保持絕對一致,由於這個緣由,當一個過
期鍵還存在於主節點時,這個鍵在全部附屬節點的副本也不會被刪除。
這種處理機制對那些使用大量附屬節點,而且帶有大量過時鍵的應用來講,可能會形成一部分
內存不能當即被釋放,可是,由於過時鍵一般很快會被主節點發現並刪除,因此這實際上也算
不上什麼大問題。


數據庫空間的收縮和擴展
由於數據庫空間是由字典來實現的,因此數據庫空間的擴展/收縮規則和字典的擴展/收縮規則
徹底同樣,具體的信息能夠參考《字典》章節。
由於對字典進行收縮的時機是由使用字典的程序決定的, 因此Redis 使用
redis.c/tryResizeHashTables 函數來檢查數據庫所使用的字典是否須要進行收縮:每次
redis.c/serverCron 函數運行的時候,這個函數都會被調用。
tryResizeHashTables 函數的完整定義以下
void tryResizeHashTables(void) {
int j;
for (j = 0; j < server.dbnum; j++) {
// 縮小鍵空間字典
if (htNeedsResize(server.db[j].dict))
dictResize(server.db[j].dict);
// 縮小過時時間字典
if (htNeedsResize(server.db[j].expires))
dictResize(server.db[j].expires);
}
}

• 數據庫主要由dict 和expires 兩個字典構成,其中dict 保存鍵值對,而expires 則
保存鍵的過時時間。
• 數據庫的鍵老是一個字符串對象,而值能夠是任意一種Redis 數據類型,包括字符串、哈
希、集合、列表和有序集。
• expires 的某個鍵和dict 的某個鍵共同指向同一個字符串對象,而expires 鍵的值則
是該鍵以毫秒計算的UNIX 過時時間戳。
• Redis 使用惰性刪除和按期刪除兩種策略來刪除過時的鍵。
• 更新後的RDB 文件和重寫後的AOF 文件都不會保留已通過期的鍵。
• 當一個過時鍵被刪除以後,程序會追加一條新的DEL 命令到現有AOF 文件末尾。
• 當主節點刪除一個過時鍵以後,它會顯式地發送一條DEL 命令到全部附屬節點。
• 附屬節點即便發現過時鍵,也不會自做主張地刪除它,而是等待主節點發來DEL 命令,
這樣能夠保證主節點和附屬節點的數據老是一致的。
• 數據庫的dict 字典和expires 字典的擴展策略和普通字典同樣。它們的收縮策略是:當
節點的填充百分比不足10% 時,將可用節點數量減小至大於等於當前已用節點數量。

持久化

在Redis 運行時,RDB 程序將當前內存中的數據庫快照保存到磁盤文件中,在Redis 重啓動
時,RDB 程序能夠經過載入RDB 文件來還原數據庫的狀態

RDB 功能最核心的是rdbSave 和rdbLoad 兩個函數,前者用於生成RDB 文件到磁盤,然後
者則用於將RDB 文件中的數據從新載入到內存中:


rdbSave 函數負責將內存中的數據庫數據以RDB 格式保存到磁盤中,若是RDB 文件已存在,
那麼新的RDB 文件將替換已有的RDB 文件。
在保存RDB 文件期間,主進程會被阻塞,直到保存完成爲止。
SAVE 和BGSAVE 兩個命令都會調用rdbSave 函數,但它們調用的方式各有不一樣:
• SAVE 直接調用rdbSave ,阻塞Redis 主進程,直到保存完成爲止。在主進程阻塞期間,
服務器不能處理客戶端的任何請求。
• BGSAVE 則fork 出一個子進程,子進程負責調用rdbSave ,並在保存完成以後向主
進程發送信號,通知保存已完成。由於rdbSave 在子進程被調用,因此Redis 服務器在
BGSAVE 執行期間仍然能夠繼續處理客戶端的請求

SAVE 、BGSAVE 、AOF 寫入和BGREWRITEAOF
除了瞭解RDB 文件的保存方式以外,咱們可能還想知道,兩個RDB 保存命令可否同時使用?
它們和AOF 保存工做是否衝突?

SAVE
前面提到過,當SAVE 執行時,Redis 服務器是阻塞的,因此當SAVE 正在執行時,新的
SAVE 、BGSAVE 或BGREWRITEAOF 調用都不會產生任何做用。
只有在上一個SAVE 執行完畢、Redis 從新開始接受請求以後,新的SAVE 、BGSAVE 或
BGREWRITEAOF 命令纔會被處理。
另外,由於AOF 寫入由後臺線程完成,而BGREWRITEAOF 則由子進程完成,因此在SAVE
執行的過程當中,AOF 寫入和BGREWRITEAOF 能夠同時進行。
BGSAVE
在執行SAVE 命令以前,服務器會檢查BGSAVE 是否正在執行當中,若是是的話,服務器就
不調用rdbSave ,而是向客戶端返回一個出錯信息,告知在BGSAVE 執行期間,不能執行
SAVE 。
這樣作能夠避免SAVE 和BGSAVE 調用的兩個rdbSave 交叉執行,形成競爭條件。
另外一方面,當BGSAVE 正在執行時,調用新BGSAVE 命令的客戶端會收到一個出錯信息,告
知BGSAVE 已經在執行當中。
BGREWRITEAOF 和BGSAVE 不能同時執行:
• 若是BGSAVE 正在執行,那麼BGREWRITEAOF 的重寫請求會被延遲到BGSAVE 執
行完畢以後進行,執行BGREWRITEAOF 命令的客戶端會收到請求被延遲的回覆。
• 若是BGREWRITEAOF 正在執行,那麼調用BGSAVE 的客戶端將收到出錯信息,表示
這兩個命令不能同時執行。
BGREWRITEAOF 和BGSAVE 兩個命令在操做方面並無什麼衝突的地方,不能同時執行
它們只是一個性能方面的考慮:併發出兩個子進程,而且兩個子進程都同時進行大量的磁盤寫
入操做,這怎麼想都不會是一個好主意。

載入
當Redis 服務器啓動時,rdbLoad 函數就會被執行,它讀取RDB 文件,並將文件中的數據庫
數據載入到內存中。
在載入期間,服務器每載入1000 個鍵就處理一次全部已到達的請求,不過只有PUBLISH 、
SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五個命令的請求會被正確地處理,
其餘命令一概返回錯誤。等到載入完成以後,服務器纔會開始正常處理全部命令。

Note: 發佈與訂閱功能和其餘數據庫功能是徹底隔離的,前者不寫入也不讀取數據庫,因此
在服務器載入期間,訂閱與發佈功能仍然能夠正常使用,而沒必要擔憂對載入數據的完整性產生
影響。

另外,由於AOF 文件的保存頻率一般要高於RDB 文件保存的頻率,因此通常來講,AOF 文
件中的數據會比RDB 文件中的數據要新。
所以,若是服務器在啓動時,打開了AOF 功能,那麼程序優先使用AOF 文件來還原數據。只
有在AOF 功能未打開的狀況下,Redis 纔會使用RDB 文件來還原數據

前面介紹了保存和讀取RDB 文件的兩個函數,如今,是時候介紹RDB 文件自己了。
一個RDB 文件能夠分爲如下幾個部分:



REDIS
文件的最開頭保存着REDIS 五個字符,標識着一個RDB 文件的開始。
在讀入文件的時候,程序能夠經過檢查一個文件的前五個字節,來快速地判斷該文件是否有可
能是RDB 文件。
RDB-VERSION
一個四字節長的以字符表示的整數,記錄了該文件所使用的RDB 版本號。
目前的RDB 文件版本爲0006 。
由於不一樣版本的RDB 文件互不兼容,因此在讀入程序時,須要根據版原本選擇不一樣的讀入方
式。
DB-DATA
這個部分在一個RDB 文件中會出現任意屢次,每一個DB-DATA 部分保存着服務器上一個非空數
據庫的全部數據。

• rdbSave 會將數據庫數據保存到RDB 文件,並在保存完成以前阻塞調用者。
• SAVE 命令直接調用rdbSave ,阻塞Redis 主進程;BGSAVE 用子進程調用rdbSave ,
主進程仍可繼續處理命令請求。
• SAVE 執行期間,AOF 寫入能夠在後臺線程進行,BGREWRITEAOF 能夠在子進程進
行,因此這三種操做能夠同時進行。
• 爲了不產生競爭條件,BGSAVE 執行時,SAVE 命令不能執行。
• 爲了不性能問題,BGSAVE 和BGREWRITEAOF 不能同時執行。
• 調用rdbLoad 函數載入RDB 文件時,不能進行任何和數據庫相關的操做,不過訂閱與
發佈方面的命令能夠正常執行,由於它們和數據庫不相關聯。
• RDB 文件的組織方式以下:


• 鍵值對在RDB 文件中的組織方式以下:


RDB 文件使用不一樣的格式來保存不一樣類型的值。

Redis 分別提供了RDB 和AOF 兩種持久化機制:
• RDB 將數據庫的快照(snapshot)以二進制的方式保存到磁盤中。
• AOF 則以協議文本的方式,將全部對數據庫進行過寫入的命令(及其參數)記錄到AOF
文件,以此達到記錄數據庫狀態的目的。

Redis 將全部對數據庫進行過寫入的命令(及其參數)記錄到AOF 文件,以此達到記錄數據庫
狀態的目的,爲了方便起見,咱們稱呼這種記錄過程爲同步

爲了處理的方便,AOF 文件使用網絡通信協議的格式來保存這些命令

緩存追加
當命令被傳播到AOF 程序以後,程序會根據命令以及命令的參數,將命令從字符串對象轉換
回原來的協議文本。
好比說,若是AOF 程序接受到的三個參數分別保存着SET 、KEY 和VALUE 三個字符串,那麼
它將生成協議文本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。
協議文本生成以後,它會被追加到redis.h/redisServer 結構的aof_buf 末尾。
redisServer 結構維持着Redis 服務器的狀態,aof_buf 域則保存着全部等待寫入到AOF 文
件的協議文本:
struct redisServer {
// 其餘域...
sds aof_buf;
// 其餘域...
};
至此,追加命令到緩存的步驟執行完畢

文件寫入和保存
每當服務器常規任務函數被執行、或者事件處理器被執行時,aof.c/flushAppendOnlyFile 函
數都會被調用,這個函數執行如下兩個工做:
WRITE:根據條件,將aof_buf 中的緩存寫入到AOF 文件。
SAVE:根據條件,調用fsync 或fdatasync 函數,將AOF 文件保存到磁盤中。
兩個步驟都須要根據必定的條件來執行,而這些條件由AOF 所使用的保存模式來決定,如下
小節就來介紹AOF 所使用的三種保存模式,以及在這些模式下,步驟WRITE 和SAVE 的調
用條件。

AOF 保存模式
Redis 目前支持三種AOF 保存模式,它們分別是:
1. AOF_FSYNC_NO :不保存。
2. AOF_FSYNC_EVERYSEC :每一秒鐘保存一次。
3. AOF_FSYNC_ALWAYS :每執行一個命令保存一次。
如下三個小節將分別討論這三種保存模式。

不保存
在這種模式下,每次調用flushAppendOnlyFile 函數,WRITE 都會被執行,但SAVE 會被
略過。
在這種模式下,SAVE 只會在如下任意一種狀況中被執行:
• Redis 被關閉
• AOF 功能被關閉
• 系統的寫緩存被刷新(多是緩存已經被寫滿,或者按期保存操做被執行)
這三種狀況下的SAVE 操做都會引發Redis 主進程阻塞。

每一秒鐘保存一次
在這種模式中,SAVE 原則上每隔一秒鐘就會執行一次,由於SAVE 操做是由後臺子線程調用
的,因此它不會引發服務器主進程阻塞。
注意,在上一句的說明裏面使用了詞語「原則上」 ,在實際運行中,程序在這種模式下對fsync
或fdatasync 的調用並非每秒一次,它和調用flushAppendOnlyFile 函數時Redis 所處的
狀態有關。
每當flushAppendOnlyFile 函數被調用時,可能會出現如下四種狀況:
• 子線程正在執行SAVE ,而且:
1. 這個SAVE 的執行時間未超過2 秒,那麼程序直接返回,並不執行WRITE 或新的
SAVE 。
2. 這個SAVE 已經執行超過2 秒,那麼程序執行WRITE ,但不執行新的SAVE 。
注意,由於這時WRITE 的寫入必須等待子線程先完成(舊的)SAVE ,所以這裏
WRITE 會比平時阻塞更長時間。
• 子線程沒有在執行SAVE ,而且:
3. 上次成功執行SAVE 距今不超過1 秒,那麼程序執行WRITE ,但不執行SAVE 。
4. 上次成功執行SAVE 距今已經超過1 秒,那麼程序執行WRITE 和SAVE


根據以上說明能夠知道,在「每一秒鐘保存一次」模式下,若是在狀況1 中發生故障停機,那麼
用戶最多損失小於2 秒內所產生的全部數據。
若是在狀況2 中發生故障停機,那麼用戶損失的數據是能夠超過2 秒的。
Redis 官網上所說的,AOF 在「每一秒鐘保存一次」時發生故障,只丟失1 秒鐘數據的說法,實
際上並不許確。

每執行一個命令保存一次
在這種模式下,每次執行完一個命令以後,WRITE 和SAVE 都會被執行。
另外,由於SAVE 是由Redis 主進程執行的,因此在SAVE 執行期間,主進程會被阻塞,不能
接受命令請求。

AOF 保存模式對性能和安全性的影響
在上一個小節,咱們簡短地描述了三種AOF 保存模式的工做方式,如今,是時候研究一下這
三個模式在安全性和性能方面的區別了。
對於三種AOF 保存模式,它們對服務器主進程的阻塞狀況以下:
1. 不保存(AOF_FSYNC_NO):寫入和保存都由主進程執行,兩個操做都會阻塞主進程。
2. 每一秒鐘保存一次(AOF_FSYNC_EVERYSEC):寫入操做由主進程執行,阻塞主進程。保存
操做由子線程執行,不直接阻塞主進程,但保存操做完成的快慢會影響寫入操做的阻塞
時長。
3. 每執行一個命令保存一次(AOF_FSYNC_ALWAYS):和模式1 同樣。
由於阻塞操做會讓Redis 主進程沒法持續處理請求,因此通常說來,阻塞操做執行得越少、完
成得越快,Redis 的性能就越好。

模式1 的保存操做只會在AOF 關閉或Redis 關閉時執行,或者由操做系統觸發,在通常狀況
下,這種模式只須要爲寫入阻塞,所以它的寫入性能要比後面兩種模式要高,固然,這種性能
的提升是以下降安全性爲代價的:在這種模式下,若是運行的中途發生停機,那麼丟失數據的
數量由操做系統的緩存沖洗策略決定。
模式2 在性能方面要優於模式3 ,而且在一般狀況下,這種模式最多丟失很少於2 秒的數據,
因此它的安全性要高於模式1 ,這是一種兼顧性能和安全性的保存方案。
模式3 的安全性是最高的,但性能也是最差的,由於服務器必須阻塞直到命令信息被寫入並保
存到磁盤以後,才能繼續處理請求。
綜合起來,三種AOF 模式的操做特性能夠總結以下:


AOF 文件保存了Redis 的數據庫狀態,而文件裏面包含的都是符合Redis 通信協議格式的命
令文本。
這也就是說,只要根據AOF 文件裏的協議,從新執行一遍裏面指示的全部命令,就能夠還原
Redis 的數據庫狀態了。

Redis 須要對AOF 文件進行重寫(rewrite):建立一個新的AOF 文件
來代替原有的AOF 文件,新AOF 文件和原有AOF 文件保存的數據庫狀態徹底同樣,但新
AOF 文件的體積小於等於原有AOF 文件的體積

所謂的「重寫」實際上是一個有歧義的詞語,實際上,AOF 重寫並不須要對原有的AOF 文件進行
任何寫入和讀取,它針對的是數據庫中鍵的當前值。
根據鍵的類型,使用適當的寫入命令來重現鍵的當前值,這就是AOF 重寫的實現原理。
AOF 後臺重寫
上一節展現的AOF 重寫程序能夠很好地完成建立一個新AOF 文件的任務,可是,在執行這
個程序的時候,調用者線程會被阻塞。
很明顯,做爲一種輔佐性的維護手段,Redis 不但願AOF 重寫形成服務器沒法處理請求,因此
Redis 決定將AOF 重寫程序放到(後臺)子進程裏執行,這樣處理的最大好處是:
1. 子進程進行AOF 重寫期間,主進程能夠繼續處理命令請求。
2. 子進程帶有主進程的數據副本,使用子進程而不是線程,能夠在避免鎖的狀況下,保證數
據的安全性。
不過,使用子進程也有一個問題須要解決:由於子進程在進行AOF 重寫期間,主進程還須要
繼續處理命令,而新的命令可能對現有的數據進行修改,這會讓當前數據庫的數據和重寫後的
AOF 文件中的數據不一致。
爲了解決這個問題,Redis 增長了一個AOF 重寫緩存,這個緩存在fork 出子進程以後開始啓
用,Redis 主進程在接到新的寫命令以後,除了會將這個寫命令的協議內容追加到現有的AOF
文件以外,還會追加到這個緩存中
AOF 後臺重寫的觸發條件
AOF 重寫能夠由用戶經過調用BGREWRITEAOF 手動觸發。
另外,服務器在AOF 功能開啓的狀況下,會維持如下三個變量:
• 記錄當前AOF 文件大小的變量aof_current_size 。
• 記錄最後一次AOF 重寫以後,AOF 文件大小的變量aof_rewirte_base_size 。
• 增加百分比變量aof_rewirte_perc 。
每次當serverCron 函數執行時,它都會檢查如下條件是否所有知足,若是是的話,就會觸發
自動的AOF 重寫:
1. 沒有BGSAVE 命令在進行。
2. 沒有BGREWRITEAOF 在進行。
3. 當前AOF 文件大小大於server.aof_rewrite_min_size (默認值爲1 MB)。
4. 當前AOF 文件大小和最後一次AOF 重寫後的大小之間的比率大於等於指定的增加百分
比。
默認狀況下,增加百分比爲100% ,也便是說,若是前面三個條件都已經知足,而且當前AOF
文件大小比最後一次AOF 重寫時的大小要大一倍的話,那麼觸發自動AOF 重寫。

• AOF 文件經過保存全部修改數據庫的命令來記錄數據庫的狀態。
• AOF 文件中的全部命令都以Redis 通信協議的格式保存。
• 不一樣的AOF 保存模式對數據的安全性、以及Redis 的性能有很大的影響。
• AOF 重寫的目的是用更小的體積來保存數據庫狀態,整個重寫過程基本上不影響Redis
主進程處理命令請求。
• AOF 重寫是一個有歧義的名字,實際的重寫工做是針對數據庫的當前值來進行的,程序
既不讀寫、也不使用原有的AOF 文件。
• AOF 能夠由用戶手動觸發,也能夠由服務器自動觸發。

事件是Redis 服務器的核心,它處理兩項重要的任務:
1. 處理文件事件:在多個客戶端中實現多路複用,接受它們發來的命令請求,並將命令的執
行結果返回給客戶端。
2. 時間事件:實現服務器常規操做(server cron job)。

Redis 將這類由於對套接字進行多路複用而產生的事件稱爲文件事件(file event),文件事件可
以分爲讀事件和寫事件兩類

讀事件
讀事件標誌着客戶端命令請求的發送狀態。
當一個新的客戶端鏈接到服務器時,服務器會給爲該客戶端綁定讀事件,直到客戶端斷開鏈接
以後,這個讀事件纔會被移除。

寫事件
寫事件標誌着客戶端對命令結果的接收狀態。
和客戶端自始至終都關聯着讀事件不一樣,服務器只會在有命令結果要傳回給客戶端時,纔會爲
客戶端關聯寫事件,而且在命令結果傳送完畢以後,客戶端和寫事件的關聯就會被移除

由於在同一次文件事件處理器的調用中,單個客戶端只能執行其中一種事件(要麼讀,要麼寫,
但不能又讀又寫),當出現讀事件和寫事件同時就緒的狀況時,事件處理器優先處理讀事件。
這也就是說,當服務器有命令結果要返回客戶端,而客戶端又有新命令請求進入時,服務器先
處理新命令請求。

時間事件記錄着那些要在指定時間點運行的事件,多個時間事件以無序鏈表的形式保存在服務
器狀態中。
每一個時間事件主要由三個屬性組成:
• when :以毫秒格式的UNIX 時間戳爲單位,記錄了應該在什麼時間點執行事件處理函數。
• timeProc :事件處理函數。
• next 指向下一個時間事件,造成鏈表。
根據timeProc 函數的返回值,能夠將時間事件劃分爲兩類:

• 若是事件處理函數返回ae.h/AE_NOMORE ,那麼這個事件爲單次執行事件:該事件會在指
定的時間被處理一次,以後該事件就會被刪除,再也不執行。
• 若是事件處理函數返回一個非AE_NOMORE 的整數值,那麼這個事件爲循環執行事件:該
事件會在指定的時間被處理,以後它會按照事件處理函數的返回值,更新事件的when 屬
性,讓這個事件在以後的某個時間點再次運行,並以這種方式一直更新並運行下去。

能夠用僞代碼來表示這兩種事件的處理方式:
def handle_time_event(server, time_event):
# 執行事件處理器,並獲取返回值
# 返回值能夠是AE_NOMORE ,或者一個表示毫秒數的非符整數值
retval = time_event.timeProc()
if retval == AE_NOMORE:
# 若是返回AE_NOMORE ,那麼將事件從鏈表中刪除,再也不執行
server.time_event_linked_list.delete(time_event)
else:
# 不然,更新事件的when 屬性
# 讓它在當前時間以後的retval 毫秒以後再次運行
time_event.when = unix_ts_in_ms() + retval

當時間事件處理器被執行時,它遍歷全部鏈表中的時間事件,檢查它們的到達事件(when 屬
性),並執行其中的已到達事件:
def process_time_event(server):
# 遍歷時間事件鏈表
for time_event in server.time_event_linked_list:
# 檢查事件是否已經到達
if time_event.when >= unix_ts_in_ms():
# 處理已到達事件
handle_time_event(server, time_event)
Note: 無序鏈表並不影響時間事件處理器的性能
在目前的版本中,正常模式下的Redis 只帶有serverCron 一個時間事件,而在benchmark 模
式下,Redis 也只使用兩個時間事件。
在這種狀況下,程序幾乎是將無序鏈表退化成一個指針來使用,因此使用無序鏈表來保存時間
事件,並不影響事件處理器的性能。

時間事件應用實例:服務器常規操做
對於持續運行的服務器來講,服務器須要按期對自身的資源和狀態進行必要的檢查和整理,從
而讓服務器維持在一個健康穩定的狀態,這類操做被統稱爲常規操做(cron job)。
在Redis 中,常規操做由redis.c/serverCron 實現,它主要執行如下操做:
• 更新服務器的各種統計信息,好比時間、內存佔用、數據庫佔用狀況等。
• 清理數據庫中的過時鍵值對。
• 對不合理的數據庫進行大小調整。
• 關閉和清理鏈接失效的客戶端。
• 嘗試進行AOF 或RDB 持久化操做。
• 若是服務器是主節點的話,對附屬節點進行按期同步。
• 若是處於集羣模式的話,對集羣進行按期同步和鏈接測試。
Redis 將serverCron 做爲時間事件來運行,從而確保它每隔一段時間就會自動運行一次,又
由於serverCron 須要在Redis 服務器運行期間一直按期運行,因此它是一個循環時間事件:
serverCron 會一直按期執行,直到服務器關閉爲止。
在Redis 2.6 版本中,程序規定serverCron 每隔10 毫秒就會被運行一次。從Redis 2.8 開始,
10 毫秒是serverCron 運行的默認間隔,而具體的間隔能夠由用戶本身調整

實際處理時間事件的時間,一般會比時間事件所預約的時間要晚,至於延遲的
時間有多長,取決於時間事件執行以前,執行文件事件所消耗的時間
文件事件先於時間事件處理,根據狀況,若是處理文件事件耗費了很是多的時間,serverCron 被推遲到一兩秒以後才能執行,也是有可能的。

• Redis 的事件分爲時間事件和文件事件兩類。
• 文件事件分爲讀事件和寫事件兩類:讀事件實現了命令請求的接收,寫事件實現了命令
結果的返回。
• 時間事件分爲單次執行事件和循環執行事件,服務器常規操做serverCron 就是循環事
件。
• 文件事件和時間事件之間是合做關係:一種事件會等待另外一種事件完成以後再執行,不
會出現搶佔狀況。
• 時間事件的實際執行時間一般會比預約時間晚一些。

從啓動Redis 服務器,到服務器能夠接受外來客戶端的網絡鏈接這段時間,Redis 須要執行一
系列初始化操做。
整個初始化過程能夠分爲如下六個步驟:
1. 初始化服務器全局狀態。
2. 載入配置文件。
3. 建立daemon 進程。
4. 初始化服務器功能模塊。
5. 載入數據。
6. 開始事件循環。

1. 初始化服務器全局狀態
redis.h/redisServer 結構記錄了和服務器相關的全部數據,這個結構主要包含如下信息:
• 服務器中的全部數據庫。
• 命令表:在執行命令時,根據字符來查找相應命令的實現函數。
• 事件狀態。
• 服務器的網絡鏈接信息:套接字地址、端口,以及套接字描述符。
• 全部已鏈接客戶端的信息。
• Lua 腳本的運行環境及相關選項。
• 實現訂閱與發佈(pub/sub)功能所需的數據結構。
• 日誌(log)和慢查詢日誌(slowlog)的選項和相關信息。
• 數據持久化(AOF 和RDB)的配置和狀態。
• 服務器配置選項:好比要建立多少個數據庫,是否將服務器進程做爲daemon 進程來運
行,最大鏈接多少個客戶端,壓縮結構(zip structure)的實體數量,等等。
• 統計信息:好比鍵有多少次命令、不命中,服務器的運行時間,內存佔用,等等。
在這一步,程序建立一個redisServer 結構的實例變量server 用做服務器的全局狀態,並將
server 的各個屬性初始化爲默認值。

2. 載入配置文件
在初始化服務器的上一步中,程序爲server 變量(也便是服務器狀態)的各個屬性設置了默
認值,但這些默認值有時候並非最合適的:
• 用戶可能想使用AOF 持久化,而不是默認的RDB 持久化。
• 用戶可能想用其餘端口來運行Redis ,以免端口衝突。
• 用戶可能不想使用默認的16 個數據庫,而是分配更多或更少數量的數據庫。
• 用戶可能想對默認的內存限制措施和回收策略作調整。
等等。
爲了讓使用者按本身的要求配置服務器,Redis 容許用戶在運行服務器時,提供相應的配置文
件(config file)或者顯式的選項(option),Redis 在初始化完server 變量以後,會讀入配置
文件和選項,而後根據這些配置來對server 變量的屬性值作相應的修改:
1. 若是單純執行redis-server 命令,那麼服務器以默認的配置來運行Redis 。
2. 另外一方面,若是給Redis 服務器送入一個配置文件,那麼Redis 將按配置文件的設置來
更新服務器的狀態。
好比說,經過命令redis-server /etc/my-redis.conf ,Redis 會根據my-redis.conf
文件的內容來對服務器狀態作相應的修改。
3. 除此以外,還能夠顯式地給服務器傳入選項,直接修改服務器配置。
舉個例子,經過命令redis-server --port 10086 ,可讓Redis 服務器端口變動爲
10086 。
4. 固然,同時使用配置文件和顯式選項也是能夠的,若是文件和選項有衝突的地方,那麼優
先使用選項所指定的配置值。
舉個例子,若是運行命令redis-server /etc/my-redis.conf --port 10086 ,而且
my-redis.conf 也指定了port 選項,那麼服務器將優先使用--port 10086 (其實是
選項指定的值覆蓋了配置文件中的值)

3. 建立daemon 進程
Redis 默認以daemon 進程的方式運行。
當服務器初始化進行到這一步時,程序將建立daemon 進程來運行Redis ,並建立相應的pid
文件。

4. 初始化服務器功能模塊
在這一步,初始化程序完成兩件事:
• 爲server 變量的數據結構子屬性分配內存。
• 初始化這些數據結構。
爲數據結構分配內存,並初始化這些數據結構,等同於對相應的功能進行初始化。
好比說,當爲訂閱與發佈所需的鏈表分配內存以後,訂閱與發佈功能就處於就緒狀態,隨時可
覺得Redis 所用了。
在這一步,程序完成的主要動做以下:
• 初始化Redis 進程的信號功能。
• 初始化日誌功能。
• 初始化客戶端功能。
• 初始化共享對象。
• 初始化事件功能。
• 初始化數據庫。
• 初始化網絡鏈接。
• 初始化訂閱與發佈功能。
• 初始化各個統計變量。
• 關聯服務器常規操做(cron job)到時間事件,關聯客戶端應答處理器到文件事件。
• 若是AOF 功能已打開,那麼打開或建立AOF 文件。
• 設置內存限制。
• 初始化Lua 腳本環境。
• 初始化慢查詢功能。
• 初始化後臺操做線程
雖然全部功能已經就緒,但這時服務器的數據庫仍是一片空白,程序還須要將服務器上一次執
行時記錄的數據載入到當前服務器中,服務器的初始化纔算真正完成。

5.載入數據
在這一步,程序須要將持久化在RDB 或者AOF 文件裏的數據,載入到服務器進程裏面。
若是服務器有啓用AOF 功能的話,那麼使用AOF 文件來還原數據;不然,程序使用RDB 文
件來還原數據。
當執行完這一步時,服務器打印出一段載入完成信息:
[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds

6. 開始事件循環
到了這一步,服務器的初始化已經完成,程序打開事件循環,開始接受客戶端鏈接。
如下是服務器在這一步打印的信息:
[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379
如下是初始化完成以後,服務器狀態和各個模塊之間的關係圖:




Redis 以多路複用的方式來處理多個客戶端,爲了讓多個客戶端之間獨立分開、不互相干擾,
服務器爲每一個已鏈接客戶端維持一個redisClient 結構,從而單獨保存該客戶端的狀態信息。

redisClient 結構主要包含如下信息:
• 套接字描述符。
• 客戶端正在使用的數據庫指針和數據庫號碼。
• 客戶端的查詢緩存(query buffer)和回覆緩存(reply buffer)。
• 一個指向命令函數的指針,以及字符串形式的命令、命令參數和命令個數,這些屬性會在
命令執行時使用。
• 客戶端狀態:記錄了客戶端是否處於SLAVE 、MONITOR 或者事務狀態。
• 實現事務功能(好比MULTI 和WATCH)所需的數據結構。
• 實現阻塞功能(好比BLPOP 和BRPOPLPUSH)所需的數據結構。
• 實現訂閱與發佈功能(好比PUBLISH 和SUBSCRIBE)所需的數據結構。
• 統計數據和選項:客戶端建立的時間,客戶端和服務器最後交互的時間,緩存的大小,等
等。

命令的請求、處理和結果返回
當客戶端連上服務器以後,客戶端就能夠向服務器發送命令請求了。
從客戶端發送命令請求,到命令被服務器處理、並將結果返回客戶端,整個過程有如下步驟:
1. 客戶端經過套接字向服務器傳送命令協議數據。
2. 服務器經過讀事件來處理傳入數據,並將數據保存在客戶端對應redisClient 結構的查
詢緩存中。
3. 根據客戶端查詢緩存中的內容,程序從命令表中查找相應命令的實現函數。
4. 程序執行命令的實現函數,修改服務器的全局狀態server 變量,並將命令的執行結果保
存到客戶端redisClient 結構的回覆緩存中,而後爲該客戶端的fd 關聯寫事件。
5. 當客戶端fd 的寫事件就緒時,將回復緩存中的命令結果傳回給客戶端。至此,命令執行
完畢。

命令請求實例:SET 的執行過程
爲了更直觀地理解命令執行的整個過程,咱們用一個實際執行SET 命令的例子來說解命令執
行的過程。

假設如今客戶端C1 是鏈接到服務器S 的一個客戶端,當用戶執行命令SET YEAR 2013 時,客
戶端調用寫入函數,將協議內容*3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n" 寫
入鏈接到服務器的套接字中。
當S 的文件事件處理器執行時,它會察覺到C1 所對應的讀事件已經就緒,因而它將協議文本
讀入,並保存在查詢緩存。
經過對查詢緩存進行分析(parse),服務器在命令表中查找SET 字符串所對應的命令實現函數,
最終定位到t_string.c/setCommand 函數,另外,兩個命令參數YEAR 和2013 也會以字符串
的形式保存在客戶端結構中。
接着,程序將客戶端、要執行的命令、命令參數等送入命令執行器:執行器調用setCommand
函數,將數據庫中YEAR 鍵的值修改成2013 ,而後將命令的執行結果保存在客戶端的回覆緩存
中,併爲客戶端fd 關聯寫事件,用於將結果回寫給客戶端。
由於YEAR 鍵的修改,其餘和數據庫命名空間相關程序,好比AOF 、REPLICATION 還有事
務安全性檢查(是否修改了被WATCH 監視的鍵?)也會被觸發,當這些後續程序也執行完畢之
後,命令執行器退出,服務器其餘程序(好比時間事件處理器)繼續運行。
當C1 對應的寫事件就緒時,程序就會將保存在客戶端結構回覆緩存中的數據回寫給客戶端,
當客戶端接收到數據以後,它就將結果打印出來,顯示給用戶看。
以上就是SET YEAR 2013 命令執行的整個過程

• 服務器通過初始化以後,才能開始接受命令。
• 服務器初始化能夠分爲六個步驟:
1. 初始化服務器全局狀態。
2. 載入配置文件。
3. 建立daemon 進程。
4. 初始化服務器功能模塊。
5. 載入數據。
6. 開始事件循環。
• 服務器爲每一個已鏈接的客戶端維持一個客戶端結構,這個結構保存了這個客戶端的全部
狀態信息。
• 客戶端向服務器發送命令,服務器接受命令而後將命令傳給命令執行器,執行器執行給
定命令的實現函數,執行完成以後,將結果保存在緩存,最後回傳給客戶端。

redis分佈式鎖

set key value [EX seconds] [PX milliseconds] [NX|XX]

1.設置nx防止死鎖

2.爲了防止A意外釋放B的鎖,val的值能夠設置成該機器的惟一標識,例如時間+請求號。在解鎖時須要校驗是否是解鎖的請求來自於同一個服務器,若是不是說明這是別人的鎖不能解

面試題:

zhuanlan.zhihu.com/p/32540678


1.哨兵 集羣含義

哨兵的做用就是監控Redis系統的運行情況。它的功能包括如下兩個。

(1)監控主數據庫和從數據庫是否正常運行。
(2)主數據庫出現故障時自動將從數據庫轉換爲主數據庫


主從模式指的是使用一個redis實例做爲主機,其他的實例做爲備份機。主機和從機的數據徹底一致,主機支持數據的寫入和讀取等各項操做,而從機則只支持與主機數據的同步和讀取,也就是說,客戶端能夠將數據寫入到主機,由主機自動將數據的寫入操做同步到從機。主從模式很好的解決了數據備份問題,而且因爲主從服務數據幾乎是一致的,於是能夠將寫入數據的命令發送給主機執行,而讀取數據的命令發送給不一樣的從機執行,從而達到讀寫分離的目的


 redis主從模式解決了數據備份和單例可能存在的性能問題,可是其也引入了新的問題。因爲主從模式配置了三個redis實例,而且每一個實例都使用不一樣的ip(若是在不一樣的機器上)和端口號,根據前面所述,主從模式下能夠將讀寫操做分配給不一樣的實例進行從而達到提升系統吞吐量的目的,但也正是由於這種方式形成了使用上的不便,由於每一個客戶端鏈接redis實例的時候都是指定了ip和端口號的,若是所鏈接的redis實例由於故障下線了,而主從模式也沒有提供必定的手段通知客戶端另外可鏈接的客戶端地址,於是須要手動更改客戶端配置從新鏈接。另外,主從模式下,若是主節點因爲故障下線了,那麼從節點由於沒有主節點而同步中斷,於是須要人工進行故障轉移工做。

爲了解決這兩個問題,在2.8版本以後redis正式提供了sentinel(哨兵)架構

對於一組主從節點,sentinel只是在其外部額外添加的一組用於監控做用的redis實例。在主從節點和sentinel節點集合配置好以後,sentinel節點之間會相互發送消息,以檢測其他sentinel節點是否正常工做,而且sentinel節點也會向主從節點發送消息,以檢測監控的主從節點是否正常工做。sentinel架構的主要做用是解決主從模式下主節點的故障轉移工做的,選取新的主節點


redis集羣是在redis 3.0版本推出的一個功能,其有效的解決了redis在分佈式方面的需求。當遇到單機內存,併發和流量瓶頸等問題時,可採用Cluster方案達到負載均衡的目的。而且從另外一方面講,redis中sentinel有效的解決了故障轉移的問題,也解決了主節點下線客戶端沒法識別新的可用節點的問題,可是若是是從節點下線了,sentinel是不會對其進行故障轉移的,而且鏈接從節點的客戶端也沒法獲取到新的可用從節點,而這些問題在Cluster中都獲得了有效的解決。

集羣就是將數據均勻分配給不一樣redis實例,防止實例掉線,在對這些實例設置從節點,從而達到高可用高併發


my.oschina.net/zhangxufeng…

相關文章
相關標籤/搜索