38 張圖詳解 Redis:核心架構(gòu)、發(fā)布訂閱機(jī)制、九大數(shù)據(jù)類型底層原理、RDB和AOF 持久化、高可用架構(gòu)、性能問題排查和調(diào)優(yōu)
現(xiàn)在就讓我們從Redis 的視角去了解她的核心知識(shí)點(diǎn)與架構(gòu)設(shè)計(jì).......
核心架構(gòu)
當(dāng)你熟悉我的整體架構(gòu)和每個(gè)模塊,遇到問題才能直擊本源,直搗黃龍,一笑破蒼穹。
我的核心模塊如圖 1-10。
圖1-10
- Client 客戶端,官方提供了 C 語(yǔ)言開發(fā)的客戶端,可以發(fā)送命令,性能分析和測(cè)試等。
- 網(wǎng)絡(luò)層事件驅(qū)動(dòng)模型,基于 I/O 多路復(fù)用,封裝了一個(gè)短小精悍的高性能 ae 庫(kù),全稱是 a simple event-driven programming library。
a.在 ae 這個(gè)庫(kù)里面,我通過 aeApiState 結(jié)構(gòu)體對(duì) epoll、select、kqueue、evport四種 I/O 多路復(fù)用的實(shí)現(xiàn)進(jìn)行適配,讓上層調(diào)用方感知不到在不同操作系統(tǒng)實(shí)現(xiàn) I/O 多路復(fù)用的差異。
b.Redis 中的事件可以分兩大類:一類是網(wǎng)絡(luò)連接、讀、寫事件;另一類是時(shí)間事件,比如定時(shí)執(zhí)行 rehash 、RDB 內(nèi)存快照生成,過期鍵值對(duì)清理操作。
- 命令解析和執(zhí)行層,負(fù)責(zé)執(zhí)行客戶端的各種命令,比如 SET、DEL、GET等。
- 內(nèi)存分配和回收,為數(shù)據(jù)分配內(nèi)存,提供不同的數(shù)據(jù)結(jié)構(gòu)保存數(shù)據(jù)。
- 持久化層,提供了 RDB 內(nèi)存快照文件 和 AOF 兩種持久化策略,實(shí)現(xiàn)數(shù)據(jù)可靠性。
- 高可用模塊,提供了副本、哨兵、集群實(shí)現(xiàn)高可用。
- 監(jiān)控與統(tǒng)計(jì),提供了一些監(jiān)控工具和性能分析工具,比如監(jiān)控內(nèi)存使用、基準(zhǔn)測(cè)試、內(nèi)存碎片、bigkey 統(tǒng)計(jì)、慢指令查詢等。
數(shù)據(jù)存儲(chǔ)原理
在掌握存儲(chǔ)原理之前,先看一下全局架構(gòu)圖,后邊慢慢分析他們的作用。
如圖 1-11 是由 redisDb、dict、dictEntry、redisObejct 關(guān)系圖:
圖1-11
redisServer
每個(gè)被啟動(dòng)的服務(wù)我都會(huì)抽象成一個(gè) redisServer,源碼定在server.h 的redisServer 結(jié)構(gòu)體。結(jié)構(gòu)體字段很多,不再一一列舉,部分核心字段如下。
truct redisServer {
pid_t pid; /* 主進(jìn)程 pid. */
pthread_t main_thread_id; /* 主線程 id */
char *configfile; /*redis.conf 文件絕對(duì)路徑*/
redisDb *db; /* 存儲(chǔ)鍵值對(duì)數(shù)據(jù)的 redisDb 實(shí)例 */
int dbnum; /* DB 個(gè)數(shù) */
dict *commands; /* 當(dāng)前實(shí)例能處理的命令表,key 是命令名,value 是執(zhí)行命令的入口 */
aeEventLoop *el;/* 事件循環(huán)處理 */
int sentinel_mode; /* true 則表示作為哨兵實(shí)例啟動(dòng) */
/* 網(wǎng)絡(luò)相關(guān) */
int port;/* TCP 監(jiān)聽端口 */
list *clients; /* 連接當(dāng)前實(shí)例的客戶端列表 */
list *clients_to_close; /* 待關(guān)閉的客戶端列表 */
client *current_client; /* 當(dāng)前執(zhí)行命令的客戶端*/
};這個(gè)結(jié)構(gòu)體包含了存儲(chǔ)鍵值對(duì)的數(shù)據(jù)庫(kù)實(shí)例、redis.conf 文件路徑、命令列表、加載的 Modules、網(wǎng)絡(luò)監(jiān)聽、客戶端列表、RDB AOF 加載信息、配置信息、RDB 持久化、主從復(fù)制、客戶端緩存、數(shù)據(jù)結(jié)構(gòu)壓縮、pub/sub、Cluster、哨兵等一系列 Redis 實(shí)例運(yùn)行的必要信息。
接下來(lái)我們分別看下他們之間的關(guān)系和作用。
redisDb
其中redisDb *db指針非常重要,它指向了一個(gè)長(zhǎng)度為 dbnum(默認(rèn) 16)的 redisDb 數(shù)組,它是整個(gè)存儲(chǔ)的核心,我就是用這玩意來(lái)存儲(chǔ)鍵值對(duì)。
typedef struct redisDb {
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
long long avg_ttl;
unsigned long expires_cursor;
list *defrag_later;
clusterSlotToKeyMapping *slots_to_keys;
} redisDb;dict 和 expires
- dict 和 expires 是最重要的兩個(gè)屬性,底層數(shù)據(jù)結(jié)構(gòu)是字典,分別用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)和 key 的過期時(shí)間。
- expires,底層數(shù)據(jù)結(jié)構(gòu)是 dict 字典,存儲(chǔ)每個(gè) key 的過期時(shí)間。
dict
Redis 使用 dict 結(jié)構(gòu)來(lái)保存所有的鍵值對(duì)(key-value)數(shù)據(jù),這是一個(gè)散列表,所以 key 查詢時(shí)間復(fù)雜度是 O(1) 。
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx;
int16_t pauserehash;
signed char ht_size_exp[2];
};dict 的結(jié)構(gòu)體里,有 dictType *type,**ht_table[2],long rehashidx 三個(gè)很重要的結(jié)構(gòu)。
- type 存儲(chǔ)了 hash 函數(shù),key 和 value 的復(fù)制等函數(shù);
- ht_table[2],長(zhǎng)度為 2 的數(shù)組,默認(rèn)使用 ht_table[0] 存儲(chǔ)鍵值對(duì)數(shù)據(jù)。我會(huì)使用 ht_table[1] 來(lái)配合實(shí)現(xiàn)漸進(jìn)式 reahsh 操作。
- rehashidx 是一個(gè)整數(shù)值,用于標(biāo)記是否正在執(zhí)行 rehash 操作,-1 表示沒有進(jìn)行 rehash。如果正在執(zhí)行 rehash,那么其值表示當(dāng)前 rehash 操作執(zhí)行的 ht_table[1] 中的 dictEntry 數(shù)組的索引。
重點(diǎn)關(guān)注 ht_table 數(shù)組,數(shù)組每個(gè)位置叫做哈希桶,就是這玩意保存了所有鍵值對(duì),每個(gè)哈希桶的類型是 dictEntry。
MySQL:“Redis 支持那么多的數(shù)據(jù)類型,哈希桶咋保存?”
他的玄機(jī)就在 dictEntry 中,每個(gè) dict 有兩個(gè) ht_table,用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)和實(shí)現(xiàn)漸進(jìn)式 rehash。
dictEntry 結(jié)構(gòu)如下。
typedef struct dictEntry {
void *key;
union {
// 指向?qū)嶋H value 的指針
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 散列表沖突生成的鏈表
struct dictEntry *next;
void *metadata[];
} dictEntry;- *key 指向鍵值對(duì)的鍵的指針,指向一個(gè) sds 對(duì)象,key 都是 string 類型。
- v 是鍵值對(duì)的 value 值,是個(gè) union(聯(lián)合體),當(dāng)它的值是 uint64_t、int64_t 或 double 數(shù)字類型時(shí),就不再需要額外的存儲(chǔ),這有利于減少內(nèi)存碎片。(為了節(jié)省內(nèi)存操碎了心)當(dāng)值為非數(shù)字類型,就是用 val 指針存儲(chǔ)。
- *next指向另一個(gè) dictEntry 結(jié)構(gòu), 多個(gè) dictEntry 可以通過 next 指針串連成鏈表, 從這里可以看出, ht_table 使用鏈地址法來(lái)處理鍵碰撞:當(dāng)多個(gè)不同的鍵擁有相同的哈希值時(shí),哈希表用一個(gè)鏈表將這些鍵連接起來(lái)。
redisObject
dictEntry 的 *val 指針指向的值實(shí)際上是一個(gè) redisObject 結(jié)構(gòu)體,這是一個(gè)非常重要的結(jié)構(gòu)體。
我的 key 是字符串類型,而 value 可以是 String、Lists、Set、Sorted Set、Hashes 等數(shù)據(jù)類型。
鍵值對(duì)的值都被包裝成 redisObject 對(duì)象, redisObject 在 server.h 中定義。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;- type:記錄了對(duì)象的類型,string、set、hash 、Lis、Sorted Set 等,根據(jù)該類型來(lái)確定是哪種數(shù)據(jù)類型,這樣我才知道該使用什么指令執(zhí)行嘛。
- encoding:編碼方式,表示 ptr 指向的數(shù)據(jù)類型具體數(shù)據(jù)結(jié)構(gòu),即這個(gè)對(duì)象使用了什么數(shù)據(jù)結(jié)構(gòu)作為底層實(shí)現(xiàn)保存數(shù)據(jù)。同一個(gè)對(duì)象使用不同編碼內(nèi)存占用存在明顯差異,節(jié)省內(nèi)存,這玩意功不可沒。
- lru:LRU_BITS:LRU 策略下對(duì)象最后一次被訪問的時(shí)間,如果是 LFU 策略,那么低 8 位表示訪問頻率,高 16 位表示訪問時(shí)間。
- refcount :表示引用計(jì)數(shù),由于 C 語(yǔ)言并不具備內(nèi)存回收功能,所以 Redis 在自己的對(duì)象系統(tǒng)中添加了這個(gè)屬性,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為 0 時(shí),則表示該對(duì)象已經(jīng)不被任何對(duì)象引用,則可以進(jìn)行垃圾回收了。
- ptr 指針:指向值的指針,對(duì)象的底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)。
數(shù)據(jù)類型底層數(shù)據(jù)結(jié)構(gòu)
我是 Redis,給開發(fā)者提供了 String(字符串)、Hashes(散列表)、Lists(列表)、Sets(無(wú)序集合)、Sorted Sets(可根據(jù)范圍查詢的排序集合)、Bitmap(位圖)、HyperLogLog、Geospatial (地理空間)和 Stream(流)等數(shù)據(jù)類型。
數(shù)據(jù)類型的使用技法和以及每種數(shù)據(jù)類型底層實(shí)現(xiàn)原理是你核心筑基必經(jīng)之路,好好修煉。
五種基本數(shù)據(jù)類型 String、List、Set、Zset、Hash。數(shù)據(jù)類型與底層數(shù)據(jù)結(jié)構(gòu)的關(guān)系如下所示。
2-55
String 字符串
我并沒有直接使用 C 語(yǔ)言的字符串,而是自己搞了一個(gè) SDS 結(jié)構(gòu)體來(lái)表示字符串。SDS 的全稱是 Simple Dynamic String,中文叫做“簡(jiǎn)單動(dòng)態(tài)字符串”。
圖2-2
O(1) 時(shí)間復(fù)雜度獲取字符串長(zhǎng)度
SDS 中 len 保存了字符串的長(zhǎng)度,實(shí)現(xiàn)了**O(1) 時(shí)間復(fù)雜度獲取字符串長(zhǎng)度。
二進(jìn)制安全
SDS 不僅可以存儲(chǔ) String 類型數(shù)據(jù),還能存儲(chǔ)二進(jìn)制數(shù)據(jù)。SDS 并不是通過“\0” 來(lái)判斷字符串結(jié)束,用的是 len 標(biāo)志結(jié)束,所以可以直接將二進(jìn)制數(shù)據(jù)存儲(chǔ)。
空間預(yù)分配
在需要對(duì) SDS 的空間進(jìn)行擴(kuò)容時(shí),不僅僅分配所需的空間,還會(huì)分配額外的未使用空間。
通過預(yù)分配策略,減少了執(zhí)行字符串增長(zhǎng)所需的內(nèi)存重新分配次數(shù),降低由于字符串增加操作的性能損耗。
惰性空間釋放
當(dāng)對(duì) SDS 進(jìn)行縮短操作時(shí),程序并不會(huì)回收多余的內(nèi)存空間,如果后面需要 append 追加操作,則直接使用 buf 數(shù)組 alloc - len中未使用的空間。
通過惰性空間釋放策略,避免了減小字符串所需的內(nèi)存重新分配操作,為未來(lái)增長(zhǎng)操作提供了優(yōu)化。
Lists(列表)
在 C 語(yǔ)言中,并沒有現(xiàn)成的鏈表結(jié)構(gòu),所以 antirez 為我專門設(shè)計(jì)了一套實(shí)現(xiàn)方式。
關(guān)于 List 類型的底層數(shù)據(jù)結(jié)構(gòu),可謂英雄輩出,antirez 大佬一直在優(yōu)化,創(chuàng)造了多種數(shù)據(jù)結(jié)構(gòu)來(lái)保存。
從一開始早期版本使用 linkedlist(雙端列表)和 ziplist(壓縮列表)作為 List 的底層實(shí)現(xiàn),到 Redis 3.2 引入了由 linkedlist + ziplist 組成的 quicklist,再到 7.0 版本的時(shí)候使用 listpack 取代 ziplist。
linkedlist
在 Redis 3.2 版本之前,List 的底層數(shù)據(jù)結(jié)構(gòu)由 linkedlist 或者 ziplist 實(shí)現(xiàn),優(yōu)先使用 ziplist 存儲(chǔ)。
當(dāng)列表對(duì)象滿足以下兩個(gè)條件的時(shí)候,List 將使用 ziplist 存儲(chǔ),否則使用 linkedlist。
- List 的每個(gè)元素的占用的字節(jié)小于 64 字節(jié)。
- List 的元素?cái)?shù)量小于 512 個(gè)。
linkedlist 的結(jié)構(gòu)如圖 2-5 所示。
圖 2-5
Redis 的鏈表實(shí)現(xiàn)的特性總結(jié)如下。
- 雙端:鏈表節(jié)點(diǎn)帶有 prev 和 next 指針,獲取某個(gè)節(jié)點(diǎn)的前置節(jié)點(diǎn)和后繼節(jié)點(diǎn)的復(fù)雜度都是 O(1)。
- 無(wú)環(huán):表頭節(jié)點(diǎn)的 prev 指針和尾節(jié)點(diǎn)的 next 指針都指向 NULL,對(duì)鏈表的訪問以 NULL 為結(jié)束。
- 帶表頭指針和表尾指針:通過 list 結(jié)構(gòu)的 head 指針和 tail 指針,程序獲取鏈表的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)的復(fù)雜度為 O(1)。
- 使用 list 結(jié)構(gòu)的 len 屬性來(lái)對(duì)記錄節(jié)點(diǎn)數(shù)量,獲取鏈表中節(jié)點(diǎn)數(shù)量的復(fù)雜度為 O(1)。
ziplist(壓縮列表)
MySQL:為啥還設(shè)計(jì)了 ziplist 呢?
- 普通的 linkedlist 有 prev、next 兩個(gè)指針,當(dāng)存儲(chǔ)數(shù)據(jù)很小的情況下,指針占用的空間會(huì)超過數(shù)據(jù)占用的空間,這就離譜了,是可忍孰不可忍。
- linkedlist 是鏈表結(jié)構(gòu),在內(nèi)存中不是連續(xù)的,遍歷的效率低下。
為了解決上面兩個(gè)問題,antirez 創(chuàng)造了 ziplist 壓縮列表,是一種內(nèi)存緊湊的數(shù)據(jù)結(jié)構(gòu),占用一塊連續(xù)的內(nèi)存空間,提升內(nèi)存使用率。
當(dāng)一個(gè)列表只有少量數(shù)據(jù)的時(shí)候,并且每個(gè)列表項(xiàng)要么是小整數(shù)值,要么就是長(zhǎng)度比較短的字符串,那么我就會(huì)使用 ziplist 來(lái)做 List 的底層實(shí)現(xiàn)。
ziplist 中可以包含多個(gè) entry 節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以存放整數(shù)或者字符串,結(jié)構(gòu)如圖 2-6 所示。
圖 2-6
- zlbytes,占用 4 個(gè)字節(jié),記錄了整個(gè) ziplist 占用的總字節(jié)數(shù)。
- zltail,占用 4 個(gè)字節(jié),指向最后一個(gè) entry 偏移量,用于快速定位最后一個(gè) entry。
- zllen,占用 2 字節(jié),記錄 entry 總數(shù)。
- entry,列表元素。
- zlend,ziplist 結(jié)束標(biāo)志,占用 1 字節(jié),值等于 255。
連鎖更新
每個(gè) entry 都用 prevlen 記錄了上一個(gè) entry 的長(zhǎng)度,從當(dāng)前 entry B 前面插入一個(gè)新的 entry A 時(shí),會(huì)導(dǎo)致 B 的 prevlen 改變,也會(huì)導(dǎo)致 entry B 大小發(fā)生變化。entry B 后一個(gè) entry C 的 prevlen 也需要改變。以此類推,就可能造成了連鎖更新。
圖 2-8
連鎖更新會(huì)導(dǎo)致 ziplist 的內(nèi)存空間需要多次重新分配,直接影響 ziplist 的查詢性能。于是乎在 Redis 3.2 版本引入了 quicklist。
quicklist
quicklist 是綜合考慮了時(shí)間效率與空間效率引入的新型數(shù)據(jù)結(jié)構(gòu)。結(jié)合了原先 linkedlist 與 ziplist 各自的優(yōu)勢(shì),本質(zhì)還是一個(gè)鏈表,只不過鏈表的每個(gè)節(jié)點(diǎn)是一個(gè) ziplist。
結(jié)合 quicklist 和 quicklistNode定義,quicklist 鏈表結(jié)構(gòu)如下圖所示。
圖 2-9
MySQL:“搞了半天還是沒能解決連鎖更新的問題嘛”
別急,飯要一口口吃,路要一步步走,步子邁大了容易扯著蛋。
畢竟還是使用了 ziplist,本質(zhì)上無(wú)法避免連鎖更新的問題,于是乎在 5.0 版本設(shè)計(jì)出另一個(gè)內(nèi)存緊湊型數(shù)據(jù)結(jié)構(gòu) listpack,于 7.0 版本替換掉 ziplist。
listpack
MySQL:“l(fā)istpack 是啥?”
listpack 也是一種緊湊型數(shù)據(jù)結(jié)構(gòu),用一塊連續(xù)的內(nèi)存空間來(lái)保存數(shù)據(jù),并且使用多種編碼方式來(lái)表示不同長(zhǎng)度的數(shù)據(jù)來(lái)節(jié)省內(nèi)存空間。
先看 listpack 的整體結(jié)構(gòu)。
圖 2-10
一共四部分組成,tot-bytes、num-elements、elements、listpack-end-byte。
- tot-bytes,也就是 total bytes,占用 4 字節(jié),記錄 listpack 占用的總字節(jié)數(shù)。
- num-elements,占用 2 字節(jié),記錄 listpack elements 元素個(gè)數(shù)。
- elements,listpack 元素,保存數(shù)據(jù)的部分。
- listpack-end-byte,結(jié)束標(biāo)志,占用 1 字節(jié),值固定為 255。
MySQL:“好家伙,這跟 ziplist 有啥區(qū)別?別以為換了個(gè)名字,換個(gè)馬甲我就不認(rèn)識(shí)了”
聽我說完!確實(shí)有點(diǎn)像,listpack 也是由元數(shù)據(jù)和數(shù)據(jù)自身組成。最大的區(qū)別是 elements 部分,為了解決 ziplist 連鎖更新的問題,element 不再像 ziplist 的 entry 保存前一項(xiàng)的長(zhǎng)度。
圖 2-11
Sets(無(wú)序集合)
Sets 是 String 類型的無(wú)序集合,集合中的元素是唯一的,集合中不會(huì)出現(xiàn)重復(fù)的數(shù)據(jù)。
Java 的 HashSet 底層是用 HashMap 實(shí)現(xiàn),Sets 的底層數(shù)據(jù)結(jié)構(gòu)也是用 Hashtable(散列表)實(shí)現(xiàn),散列表的 key 存的是 Sets 集合元素的 value,散列表的 value 則指向 NULL。。
不同的是,當(dāng)元素內(nèi)容都是 64 位以內(nèi)的十進(jìn)制整數(shù)的時(shí)候,并且元素個(gè)數(shù)不超過 set-max-intset-entries 配置的值(默認(rèn) 512)的時(shí)候,會(huì)使用更加省內(nèi)存的 intset(整形數(shù)組)來(lái)存儲(chǔ)。
圖2-15
關(guān)于散列表結(jié)構(gòu)我會(huì)在專門的章節(jié)介紹,先看 intset 結(jié)構(gòu),結(jié)構(gòu)體定義在源碼 intset.h中。
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;- length,記錄整數(shù)集合存儲(chǔ)的元素個(gè)數(shù),其實(shí)就是 contents 數(shù)組的長(zhǎng)度。
- contents,真正存儲(chǔ)整數(shù)集合的數(shù)組,是一塊連續(xù)內(nèi)存區(qū)域。每個(gè)元素都是數(shù)組的一個(gè)數(shù)組元素,數(shù)組中的元素會(huì)按照值的大小從小到大有序排列存儲(chǔ),并且不會(huì)有重復(fù)元素。
- encoding,編碼格式,決定數(shù)組類型,一共有三種不同的值。
圖2-16
Hash(散列表)
Redis Hash(散列表)是一種 field-value pairs(鍵值對(duì))集合類型,類似于 Python 中的字典、Java 中的 HashMap。
Redis 的散列表 dict 由數(shù)組 + 鏈表構(gòu)成,數(shù)組的每個(gè)元素占用的槽位叫做哈希桶,當(dāng)出現(xiàn)散列沖突的時(shí)候就會(huì)在這個(gè)桶下掛一個(gè)鏈表,用“拉鏈法”解決散列沖突的問題。
簡(jiǎn)單地說就是將一個(gè) key 經(jīng)過散列計(jì)算均勻的映射到散列表上。
圖 2-18
Hashes 數(shù)據(jù)類型底層存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)實(shí)際上有兩種。
- dict 結(jié)構(gòu)。
- 在 7.0 版本之前使用 ziplist,之后被 listpack 代替。
listpack 數(shù)據(jù)結(jié)構(gòu)在之前的已經(jīng)介紹過, 接下來(lái)帶你揭秘 dict 到底長(zhǎng)啥樣。
Redis 數(shù)據(jù)庫(kù)就是一個(gè)全局散列表。正常情況下,我只會(huì)使用 ht_table[0]散列表,圖 2-20 是一個(gè)沒有進(jìn)行 rehash 狀態(tài)下的字典。
圖 2-20
- dictType *type,存放函數(shù)的結(jié)構(gòu)體,定義了一些函數(shù)指針,可以通過設(shè)置自定義函數(shù),實(shí)現(xiàn) dict 的 key 和 value 存放任何類型的數(shù)據(jù)。
- 重點(diǎn)看 dictEntry **ht_table[2],存放了兩個(gè) dictEntry 的二級(jí)指針,指針分別指向了一個(gè) dictEntry 指針的數(shù)組。
- ht_used[2],記錄每個(gè)散列表使用了多少槽位(比如數(shù)組長(zhǎng)度 32,使用了 12)。
- rehashidx,用于標(biāo)記是否正在執(zhí)行 rehash 操作,-1 表示沒有進(jìn)行 rehash。如果正在執(zhí)行 rehash,那么其值表示當(dāng)前 rehash 操作執(zhí)行的 ht_table[0] 散列表 dictEntry 數(shù)組的索引。
- pauserehash 表示 rehash 的狀態(tài),大于 0 時(shí)表示 rehash 暫停了,小于 0 表示出錯(cuò)了。
Sorted Sets(有序集合)
Sorted Sets 與 Sets 類似,是一種集合類型,集合中不會(huì)出現(xiàn)重復(fù)的數(shù)據(jù)(member)。區(qū)別在于 Sorted Sets 元素由兩部分組成,分別是 member 和 score。member 會(huì)關(guān)聯(lián)一個(gè) double 類型的分?jǐn)?shù)(score),sorted sets 默認(rèn)會(huì)根據(jù)這個(gè) score 對(duì) member 進(jìn)行從小到大的排序,如果 member 關(guān)聯(lián)的分?jǐn)?shù) score 相同,則按照字符串的字典順序排序。
2-24
常見的使用場(chǎng)景:
- 排行榜,比如維護(hù)大型在線游戲中根據(jù)分?jǐn)?shù)排名的 Top 10 有序列表。
- 速率限流器,根據(jù)排序集合構(gòu)建滑動(dòng)窗口速率限制器。
- 延遲隊(duì)列,score 存儲(chǔ)過期時(shí)間,從小到大排序,最靠前的就是最先到期的數(shù)據(jù)。
Sorted Sets 底層有兩種方式來(lái)存儲(chǔ)數(shù)據(jù)。
- 在 7.0 版本之前是 ziplist,之后被 listpack 代替,使用條件是集合元素個(gè)數(shù)小于等于 zset-max-listpack-entries 配置值(默認(rèn) 128),且 member 占用字節(jié)大小小于 zset-max-listpack-value 配置值(默認(rèn) 64)時(shí)使用 listpack 存儲(chǔ),member 和 score 緊湊排列作為 listpack 的一個(gè)元素進(jìn)行存儲(chǔ)。
- 不滿足上述條件,使用 skiplist + dict(散列表) 組合方式存儲(chǔ),數(shù)據(jù)會(huì)插入 skiplist 的同時(shí)也會(huì)向 dict(散列表)中插入數(shù)據(jù) ,是一種用空間換時(shí)間的思路。散列表的 key 存放的是元素的 member,value 存儲(chǔ)的是 member 關(guān)聯(lián)的 score。
skiplist + dict
MySQL:“說說什么是跳表吧”
實(shí)質(zhì)就是一種可以進(jìn)行二分查找的有序鏈表。跳表在原有的有序鏈表上面增加了多級(jí)索引,通過索引來(lái)實(shí)現(xiàn)快速查找。
查找數(shù)據(jù)總是從最高層開始比較,如果節(jié)點(diǎn)保存的值比待查數(shù)據(jù)小,跳表就繼續(xù)訪問該層的下一個(gè)節(jié)點(diǎn);
如果碰到比待查數(shù)據(jù)值大的節(jié)點(diǎn)時(shí),那就跳到當(dāng)前節(jié)點(diǎn)的下一層的鏈表繼續(xù)查找。
比如現(xiàn)在想查找 17,查找的路徑如下圖紅色指向的方向進(jìn)行。
圖 2-27
- 從 level 1 開始,17 與 6 比較,值大于節(jié)點(diǎn),繼續(xù)與下一個(gè)節(jié)點(diǎn)比較。
- 與 26 比較,17 < 26,回到原節(jié)點(diǎn),跳到當(dāng)前節(jié)點(diǎn)的 level 0 層鏈表,與下一個(gè)節(jié)點(diǎn)比較,找到目標(biāo) 17。
MySQL:采用 listpack 存儲(chǔ)數(shù)據(jù)的 Sorted Sets 怎么實(shí)現(xiàn)呢?
我們知道,listpack 是一塊由多個(gè)數(shù)據(jù)項(xiàng)組成的連續(xù)內(nèi)存。而 sorted set 每一項(xiàng)元素是由 member 和 score 兩部分組成。
采用 listpack 存儲(chǔ)插入一個(gè)(member、score)數(shù)據(jù)對(duì)的時(shí)候,每個(gè) member/score 數(shù)據(jù)對(duì)緊湊排列存儲(chǔ)。
listpack 最大的優(yōu)勢(shì)就是節(jié)省內(nèi)存,查找元素的話只能按順序查找,時(shí)間復(fù)雜度是 O(n)。
正是如此,在少量數(shù)據(jù)的情況下,才能做到既能節(jié)省內(nèi)存,又不會(huì)影響性能。
每一步查找前進(jìn)兩個(gè)數(shù)據(jù)項(xiàng),也就是跨越一個(gè) member/score 數(shù)據(jù)對(duì)。
圖 2-30
streams(流)
Stream 是 Redis 5.0 版本專門為消息隊(duì)列設(shè)計(jì)的數(shù)據(jù)類型,借鑒了 Kafka 的 Consume Group 設(shè)計(jì)思路,提供了消費(fèi)組概念。
同時(shí)提供了消息的持久化和主從復(fù)制機(jī)制,客戶端可以訪問任何時(shí)刻的數(shù)據(jù),并且能記住每一個(gè)客戶端的訪問位置,從而保證消息不丟失。
以下幾個(gè)是 Stream 類型的主要特性。
- 使用 Radix Tree 和 listpack 結(jié)構(gòu)來(lái)存儲(chǔ)消息。
- 消息 ID 序列化生成。
- 借鑒 Kafka Consume Group 的概念,多個(gè)消費(fèi)者劃分到不同的 Consume Group 中,消費(fèi)同一個(gè) Streams,同一個(gè) Consume Group 的多個(gè)消費(fèi)者可以一起并行但不重復(fù)消費(fèi),提升消費(fèi)能力。
- 支持多播(多對(duì)多),阻塞和非阻塞讀取。
- ACK 確認(rèn)機(jī)制,保證了消息至少被消費(fèi)一次。
- 可設(shè)置消息保存上限閾值,我會(huì)把歷史消息丟棄,防止內(nèi)存占用過大。
Stream 流就像是一個(gè)僅追加內(nèi)容的消息鏈表,把消息一個(gè)個(gè)串起來(lái),每個(gè)消息都有一個(gè)唯一的 ID 和消息內(nèi)容,消息內(nèi)容則由多個(gè) field/value 鍵值對(duì)組成。底層使用 Radix Tree 和 listpack 數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)。
為了便于理解,我畫了一張圖,并對(duì) Radix Tree 的存儲(chǔ)數(shù)據(jù)做了下變形,使用列表來(lái)體現(xiàn) Stream 中消息的邏輯有序性。
圖 2-31
Geo(地理空間)
Redis 老兄,產(chǎn)品經(jīng)理跟我說,他有一個(gè) idea,想為廣大少男少女提供一個(gè)連接彼此的機(jī)會(huì)。
所謂花有重開日,人無(wú)再少年,為了讓處于這美好年齡的少男少女,能在以每一個(gè)十二時(shí)辰里邂逅那個(gè) ta”。想開發(fā)一款 APP,用戶登錄登錄后,基于地理位置發(fā)現(xiàn)附近的那個(gè) Ta 鏈接彼此。
Sorted Sets 集合中,每個(gè)元素由兩部分組成,分別是 member 和 score??梢愿鶕?jù)權(quán)重分?jǐn)?shù)對(duì) member 排序,這樣看起來(lái)就滿足我的需求了。比如,member 存儲(chǔ) “女神 ID”,score 是該女神的經(jīng)緯度信息。
圖 2-40
還有一個(gè)問題,Sorted Set 元素的權(quán)重值是一個(gè)浮點(diǎn)數(shù),經(jīng)緯度是經(jīng)度、緯度兩個(gè)值,咋辦呢?如何將經(jīng)緯度轉(zhuǎn)換成一個(gè)浮點(diǎn)數(shù)呢?
思路對(duì)了,為了實(shí)現(xiàn)對(duì)經(jīng)緯度比較,Redis 采用業(yè)界廣泛使用的 GeoHash 編碼,分別對(duì)經(jīng)度和緯度編碼,最后再把經(jīng)緯度各自的編碼組合成一個(gè)最終編碼。
這樣就實(shí)現(xiàn)了將經(jīng)緯度轉(zhuǎn)換成一個(gè)值,而 Redis 的 GEO 類型的底層數(shù)據(jù)結(jié)構(gòu)用的就是 Sorted Set來(lái)實(shí)現(xiàn)。
Geohash 算法就是將經(jīng)緯度編碼,將二維變一維,給地址位置分區(qū)的一種算法,核心思想是區(qū)間二分:將地球編碼看成一個(gè)二維平面,然后將這個(gè)平面遞歸均分為更小的子塊。
一共可以分為三步。
- 將經(jīng)緯度變成一個(gè) N 位二進(jìn)制。
- 將經(jīng)緯度的二進(jìn)制合并。
- 按照 Base32 進(jìn)行編碼。
Bitmap(位圖)
Redis Bitmap(位圖)是 Redis 提供的一種特殊的數(shù)據(jù)結(jié)構(gòu),用于處理位級(jí)別的數(shù)據(jù)。
實(shí)際上是在 String 類型上定義的面向 bit 位的操作,將位圖存儲(chǔ)在字符串中,每個(gè)字符代表 8 位二進(jìn)制,是一個(gè)由二進(jìn)制位(bit)組成的數(shù)組,其中的每一位只能是 0 或 1。
String 數(shù)據(jù)類型最大容量是 512MB,所以一個(gè) Bitmap 最多可設(shè)置 2^32 個(gè)不同位。
Bitmap 的底層數(shù)據(jù)結(jié)構(gòu)用的是 String 類型的 SDS 數(shù)據(jù)結(jié)構(gòu)來(lái)保存位數(shù)組,Redis 把每個(gè)字節(jié)數(shù)組的 8 個(gè) bit 位利用起來(lái),每個(gè) bit 位 表示一個(gè)元素的二值狀態(tài)(不是 0 就是 1)。
可以將 Bitmap 看成是一個(gè) bit 為單位的數(shù)組,數(shù)組的每個(gè)單元只能存儲(chǔ) 0 或者 1,數(shù)組的每個(gè) bit 位下標(biāo)在 Bitmap 中叫做 offset 偏移量。
為了直觀展示,我們可以理解成 buf 數(shù)組的每個(gè)槽位中的字節(jié)用一行表示,每一行有 8 個(gè) bit 位,8 個(gè)格子分別表示這個(gè)字節(jié)中的 8 個(gè) bit 位,如下圖所示:
2-44
8 個(gè) bit 組成一個(gè) Byte,所以 Bitmap 會(huì)極大地節(jié)省存儲(chǔ)空間。 這就是 Bitmap 的優(yōu)勢(shì)。
HyperlogLogs(基數(shù)統(tǒng)計(jì))
在移動(dòng)互聯(lián)網(wǎng)的業(yè)務(wù)場(chǎng)景中,數(shù)據(jù)量很大,系統(tǒng)需要保存這樣的信息:一個(gè) key 關(guān)聯(lián)了一個(gè)數(shù)據(jù)集合,同時(shí)對(duì)這個(gè)數(shù)據(jù)集合做統(tǒng)計(jì)做一個(gè)報(bào)表給運(yùn)營(yíng)人員看。
比如:
- 統(tǒng)計(jì)一個(gè) APP 的日活、月活數(shù)。
- 統(tǒng)計(jì)一個(gè)頁(yè)面的每天被多少個(gè)不同賬戶訪問量(Unique Visitor,UV)。
- 統(tǒng)計(jì)用戶每天搜索不同詞條的個(gè)數(shù)。
- 統(tǒng)計(jì)注冊(cè) IP 數(shù)。
通常情況下,系統(tǒng)面臨的用戶數(shù)量以及訪問量都是巨大的,比如百萬(wàn)、千萬(wàn)級(jí)別的用戶數(shù)量,或者千萬(wàn)級(jí)別、甚至億級(jí)別的訪問信息,咋辦呢?
Redis:“這些就是典型的基數(shù)統(tǒng)計(jì)應(yīng)用場(chǎng)景,基數(shù)統(tǒng)計(jì):統(tǒng)計(jì)一個(gè)集合中不重復(fù)元素,這被稱為基數(shù)?!?/p>
HyperLogLog 是一種概率數(shù)據(jù)結(jié)構(gòu),用于估計(jì)集合的基數(shù)。每個(gè) HyperLogLog 最多只需要花費(fèi) 12KB 內(nèi)存,在標(biāo)準(zhǔn)誤差 0.81%的前提下,就可以計(jì)算 2 的 64 次方個(gè)元素的基數(shù)。
HyperLogLog 的優(yōu)點(diǎn)在于它所需的內(nèi)存并不會(huì)因?yàn)榧系拇笮《淖儯瑹o(wú)論集合包含的元素有多少個(gè),HyperLogLog 進(jìn)行計(jì)算所需的內(nèi)存總是固定的,并且是非常少的。
Redis 內(nèi)部使用字符串位圖來(lái)存儲(chǔ) HyperLogLog 所有桶的計(jì)數(shù)值,一共分了 2^14 個(gè)桶,也就是 16384 個(gè)桶。每個(gè)桶中是一個(gè) 6 bit 的數(shù)組。
這段代碼描述了 Redis HyperLogLog 數(shù)據(jù)結(jié)構(gòu)的頭部定義(hyperLogLog.c 中的 hllhdr 結(jié)構(gòu)體)。以下是關(guān)于這個(gè)數(shù)據(jù)結(jié)構(gòu)的各個(gè)字段的解釋。
struct hllhdr {
char magic[4];
uint8_t encoding;
uint8_t notused[3];
uint8_t card[8];
uint8_t registers[];
};- **magic[4]**:這個(gè)字段是一個(gè) 4 字節(jié)的字符數(shù)組,用來(lái)表示數(shù)據(jù)結(jié)構(gòu)的標(biāo)識(shí)符。在 HyperLogLog 中,它的值始終為"HYLL",用來(lái)標(biāo)識(shí)這是一個(gè) HyperLogLog 數(shù)據(jù)結(jié)構(gòu)。
- encoding:這是一個(gè) 1 字節(jié)的字段,用來(lái)表示 HyperLogLog 的編碼方式。它可以取兩個(gè)值之一:
a.HLL_DENSE:表示使用稠密表示方式。
b.HLL_SPARSE:表示使用稀疏表示方式。
- **notused[3]**:這是一個(gè) 3 字節(jié)的字段,目前保留用于未來(lái)的擴(kuò)展,要求這些字節(jié)的值必須為零。
- **card[8]**:這是一個(gè) 8 字節(jié)的字段,用來(lái)存儲(chǔ)緩存的基數(shù)(基數(shù)估計(jì)的值)。
- **egisters[]**:這個(gè)字段是一個(gè)可變長(zhǎng)度的字節(jié)數(shù)組,用來(lái)存儲(chǔ) HyperLogLog 的數(shù)據(jù)。
4-45
Redis 對(duì) HyperLogLog 的存儲(chǔ)進(jìn)行了優(yōu)化,在計(jì)數(shù)比較小的時(shí)候,存儲(chǔ)空間采用系數(shù)矩陣,占用空間很小。
只有在計(jì)數(shù)很大,稀疏矩陣占用的空間超過了閾值才會(huì)轉(zhuǎn)變成稠密矩陣,占用 12KB 空間。
Bloom Filter(布隆過濾器)
當(dāng)你遇到數(shù)據(jù)量大,又需要去重的時(shí)候就可以考慮布隆過濾器,如下場(chǎng)景:
- 解決 Redis 緩存穿透問題。
- 郵件過濾,使用布隆過濾器實(shí)現(xiàn)郵件黑名單過濾。
- 爬蟲爬過的網(wǎng)站過濾,爬過的網(wǎng)站不再爬取。
- 推薦過的新聞不再推薦。
布隆過濾器 (Bloom Filter)是由 Burton Howard Bloom 于 1970 年提出,它是一種 space efficient 的概率型數(shù)據(jù)結(jié)構(gòu),用于判斷一個(gè)元素是否在集合中。
是一種空間效率高、時(shí)間復(fù)雜度低的數(shù)據(jù)結(jié)構(gòu),用于檢查一個(gè)元素是否存在于一個(gè)集合中。它通常用于快速判斷某個(gè)元素是否可能存在于一個(gè)大型數(shù)據(jù)集中,而無(wú)需實(shí)際存儲(chǔ)整個(gè)數(shù)據(jù)集。
布隆過濾器客戶以保證某個(gè)數(shù)據(jù)不存在時(shí),那么這個(gè)數(shù)據(jù)一定不存在;當(dāng)給出的響應(yīng)是存在,這個(gè)數(shù)可能不存在。
Redis 的 Bloom Filter 實(shí)現(xiàn)基于一個(gè)位數(shù)組(bit array)和一組不同的哈希函數(shù)。
- 首先分配一塊內(nèi)存空間做 bit 數(shù)組,這個(gè)位數(shù)組的長(zhǎng)度是固定的,通常由用戶指定,決定了 Bloom Filter 的容量。每個(gè)位都初始為 0。
- 添加元素時(shí),采用 k 個(gè)相互獨(dú)立的 Hash 函數(shù)對(duì)這個(gè) key 計(jì)算,這些哈希函數(shù)應(yīng)該是獨(dú)立的,均勻分布的,以減小沖突的可能性,然后將元素 Hash 值所映射的 K 個(gè)位置全部設(shè)置為 1。
- 檢測(cè) key 是否存在,仍然用這 k 個(gè) Hash 函數(shù)對(duì) key 計(jì)算出 k 哈希值,哈希值映射的 k 個(gè) 位置,如果位置全部為 1,則表明 key 可能存在,否則不存在。
2-46
高可用架構(gòu)
我是一個(gè)基于內(nèi)存的數(shù)據(jù)庫(kù),名字叫 Redis。我對(duì)數(shù)據(jù)讀寫操作的速度快到令人發(fā)指,很多程序員把我當(dāng)做緩存使用系統(tǒng),用于提高系統(tǒng)讀取響應(yīng)性能。
然而,快是需要付出代價(jià)的:內(nèi)存無(wú)法持久化,一旦斷電或者宕機(jī),我保存在內(nèi)存中的數(shù)據(jù)將全部丟失。
我有兩大殺手锏,實(shí)現(xiàn)了數(shù)據(jù)持久化,做到宕機(jī)快速恢復(fù),不丟數(shù)據(jù)穩(wěn)如狗,避免從數(shù)據(jù)庫(kù)中慢慢恢復(fù)數(shù)據(jù),他們分別是 RDB 快照和 AOF(Append Only File)。
RDB 快照
RDB 內(nèi)存快照,指的就是 Redis 內(nèi)存中的某一刻的數(shù)據(jù)。
好比時(shí)間定格在某一刻,當(dāng)我們拍照時(shí),把某一刻的瞬間畫面定格記錄下來(lái)。
我跟這個(gè)類似,就是把某一刻的數(shù)據(jù)以文件的形式“拍”下來(lái),寫到磁盤上。這個(gè)文件叫做 RDB 文件,是 Redis Database 的縮寫。
我只需要定時(shí)執(zhí)行 RDB 內(nèi)存快照,就不必每次執(zhí)行寫指令都寫磁盤,既實(shí)現(xiàn)了快,還實(shí)現(xiàn)了持久化。
RDB內(nèi)存快照
當(dāng)在進(jìn)行宕機(jī)后重啟數(shù)據(jù)恢復(fù)時(shí),直接將磁盤的 RDB 文件讀入內(nèi)存即可。
MySQL:“實(shí)際生產(chǎn)環(huán)境中,程序員通常給你配置 6GB 的內(nèi)存,將這么大的內(nèi)存數(shù)據(jù)生成 RDB 快照文件落到磁盤的過程會(huì)持續(xù)比較長(zhǎng)的時(shí)間。
你如何做到繼續(xù)處理寫指令請(qǐng)求,又保證 RDB 與內(nèi)存中的數(shù)據(jù)的一致性呢?”
作為唯快不破的 NoSQL 數(shù)據(jù)庫(kù)扛把子,我在對(duì)內(nèi)存數(shù)據(jù)做快照的時(shí)候,并不會(huì)暫停寫操作(讀操作不會(huì)造成數(shù)據(jù)的不一致)。
我使用了操作系統(tǒng)的多進(jìn)程寫時(shí)復(fù)制技術(shù) COW(Copy On Write) 來(lái)實(shí)現(xiàn)快照持久化。
在持久化時(shí)我會(huì)調(diào)用操作系統(tǒng) glibc 函數(shù)fork產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來(lái)處理,主進(jìn)程繼續(xù)處理客戶端請(qǐng)求。
子進(jìn)程剛剛產(chǎn)生時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段,你可以將父子進(jìn)程想象成成一個(gè)連體嬰兒,共享身體。
這是 Linux 操作系統(tǒng)的機(jī)制,為了節(jié)約內(nèi)存資源,所以盡可能讓它們共享起來(lái)。在進(jìn)程分離的一瞬間,內(nèi)存的增長(zhǎng)幾乎沒有明顯變化。
bgsave 子進(jìn)程可以共享主線程的所有內(nèi)存數(shù)據(jù),所以能讀取主線程的數(shù)據(jù)并寫入 RDB 文件。
如果主線程對(duì)這些數(shù)據(jù)是讀操作,那么主線程和 bgsave子進(jìn)程互不影響。
當(dāng)主線程要修改某個(gè)鍵值對(duì)時(shí),這個(gè)數(shù)據(jù)會(huì)把發(fā)生變化的數(shù)據(jù)復(fù)制一份,生成副本。
接著,bgsave 子進(jìn)程會(huì)把這個(gè)副本數(shù)據(jù)寫到 RDB 文件,從而保證了數(shù)據(jù)一致性。
圖 3-2 寫時(shí)復(fù)制技術(shù)保證快照期間數(shù)據(jù)客修改
AOF
針對(duì) RDB 不適合實(shí)時(shí)持久化等問題,我提供 AOF 持久化方式來(lái)破解。
AOF (Append Only File)持久化記錄的是服務(wù)器接收的每個(gè)寫操作,在服務(wù)器啟動(dòng)執(zhí)行重放還原數(shù)據(jù)集。
AOF 采用的是寫后日志模式,即先寫內(nèi)存,后寫日志。
AOF寫后日志
當(dāng)我接收到 set key MageByte 命令將數(shù)據(jù)寫到內(nèi)存后, 會(huì)按照如下格式寫入 AOF 文件。
- *3:表示當(dāng)前指令分為三個(gè)部分,每個(gè)部分都是 $ + 數(shù)字開頭,緊跟后面是該部分具體的指令、鍵、值。
- 數(shù)字:表示這部分的命令、鍵、值多占用的字節(jié)大小。比如 $3表示這部分包含 3 個(gè)字節(jié),也就是 SET 指令。
AOF 日志格式
為了解決 AOF 文件體積膨脹的問題,創(chuàng)造我的 antirez 老哥設(shè)計(jì)了一個(gè)殺手锏——AOF 重寫機(jī)制,對(duì)文件進(jìn)行瘦身。
例如,使用 INCR counter 實(shí)現(xiàn)一個(gè)自增計(jì)數(shù)器,初始值 1,遞增 1000 次的最終目標(biāo)是 1000,在 AOF 中保存著 1000 次指令。
在重寫的時(shí)候并不需要其中的 999 個(gè)寫操作,重寫機(jī)制有多變一功能,將舊日志中的多條指令,重寫后就變成了一條指令。
其原理就是開辟一個(gè)子進(jìn)程將內(nèi)存中的數(shù)據(jù)轉(zhuǎn)換成一系列 Redis 的寫操作指令,寫到一個(gè)新的 AOF 日志文件中。再將操作期間發(fā)生的增量 AOF 日志追加到這個(gè)新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。
AOF重寫機(jī)制(糾錯(cuò):3條變一條)
每次 AOF 重寫時(shí),Redis 會(huì)先執(zhí)行一個(gè)內(nèi)存拷貝,讓 bgrewriteaof 子進(jìn)程擁有此時(shí)的 Redis 內(nèi)存快照,子進(jìn)程遍歷 Redis 中的全部鍵值對(duì),生成重寫記錄。
使用兩個(gè)日志保證在重寫過程中,新寫入的數(shù)據(jù)不會(huì)丟失,并且保持?jǐn)?shù)據(jù)一致性。
AOF 重寫過程
antirez 在 4.0 版本中給我提供了一個(gè)混合使用 AOF 日志和 RDB 內(nèi)存快照的方法。簡(jiǎn)單來(lái)說,RDB 內(nèi)存快照以一定的頻率執(zhí)行,在兩次快照之間,使用 AOF 日志記錄這期間的所有寫操作。
如此一來(lái),快照就不需要頻繁執(zhí)行,避免了 fork 對(duì)主線程的性能影響,AOF 不再是全量日志,而是生成 RDB 快照時(shí)間的增量 AOF 日志,這個(gè)日志就會(huì)很小,都不需要重寫了。
等到,第二次做 RDB 全量快照,就可以清空舊的 AOF 日志,恢復(fù)數(shù)據(jù)的時(shí)候就不需要使用 AOF 日志了。
主從復(fù)制
Chaya:“李老師,有了 RDB 內(nèi)存快照和 AOF 再也不怕宕機(jī)丟失數(shù)據(jù)了,但是 Redis 實(shí)例宕機(jī)了辦?如何實(shí)現(xiàn)高可用呢?“
李老師愣了一會(huì)兒,又趕緊補(bǔ)充道:“依然記得那晚我和我的戀人 Chaya 鴛語(yǔ)輕傳,香風(fēng)急促,朱唇緊貼。香肌如雪,羅裳慢解春光泄。含香玉體說溫存,多少風(fēng)和月。今宵魚水和諧,抖顫顫,春潮難歇。千聲呢喃,百聲喘吁,數(shù)番愉悅?!?/p>
可是這時(shí)候 Redis 忽然宕機(jī)了,無(wú)法對(duì)外提供服務(wù),電話連環(huán) call,豈不是折煞人也。
Redis:“你還念上詩(shī)歌了,莫怕,為了你們的幸福。我提供了主從模式,通過主從復(fù)制,將數(shù)據(jù)冗余一份復(fù)制到其他 Redis 服務(wù)器,實(shí)現(xiàn)高可用。你們放心的說溫存,說風(fēng)月?!?/p>
既然一臺(tái)宕機(jī)了無(wú)法提供服務(wù),那多臺(tái)呢?是不是就可以解決了。
前者稱為 mater (master),后者稱為 slave (slave),數(shù)據(jù)的復(fù)制是單向的,只能由 mater 到 slave。
默認(rèn)情況下,每臺(tái) Redis 服務(wù)器都是 mater;且一個(gè) mater 可以有多個(gè) slave (或沒有 slave),但一個(gè) slave 只能有一個(gè) mater。
3-1
主從庫(kù)第一次復(fù)制過程大體可以分為 3 個(gè)階段:連接建立階段(即準(zhǔn)備階段)、mater 同步數(shù)據(jù)到 slave 階段、發(fā)送同步期間接收到的新寫命令到 slave 階段。
直接上圖,從整體上有一個(gè)全局觀的感知,后面具體介紹。
Redis全量同步
哨兵集群
主從復(fù)制架構(gòu)面臨一個(gè)嚴(yán)峻問題,master 掛了,無(wú)法執(zhí)行寫操作,無(wú)法自動(dòng)選擇一個(gè) Slave 切換為 Master,也就是無(wú)法故障自動(dòng)切換。
李老師:“還記得那晚與我女友 Chaya 約會(huì),眼前是橡樹的綠葉,白色的竹籬笆。好想告訴我的她,這里像幅畫。一起手牽手么么噠……(此處省略 10000 字)。
Redis 忽然宕機(jī),我總不能推開 Chaya,停止甜蜜,然后打開電腦手工進(jìn)行主從切換,再通知其他程序員把地址重新改成新 Master 信息上線?”。
Redis:“如此一折騰恐怕李老師已被 Chaya 切換成前男友了,心里的雨傾盆地下,萬(wàn)萬(wàn)使不得。所以必須有一個(gè)高可用的方案,為此,我提供一個(gè)高可用方案——哨兵(Sentinel)“。
先來(lái)看看哨兵是什么?搭建哨兵集群的方法我就不細(xì)說了,假設(shè)三個(gè)哨兵組成一個(gè)哨兵集群,三個(gè)數(shù)據(jù)節(jié)點(diǎn)構(gòu)成一個(gè)一主兩從的 Redis 主從架構(gòu)。
3-17
Redis 哨兵集群高可用方法,有三種角色,分別是 master,slave,sentinel。
- setinel 節(jié)點(diǎn)之間互相通信,組成一個(gè)集群視線哨兵高可用,選舉出一個(gè) leader 執(zhí)行故障轉(zhuǎn)移。
- master 與 slave 之間通信,組成主從復(fù)制架構(gòu)。
- sentinel 與 master/ slave 通信,是為了對(duì)該主從復(fù)制架構(gòu)進(jìn)行管理:監(jiān)視(Monitoring)、通知(Notification)、自動(dòng)故障切換(Automatic Failover)、配置提供者(Configuration Provider)。
Redis Cluster
使用 Redis Cluster 集群,主要解決了大數(shù)據(jù)量存儲(chǔ)導(dǎo)致的各種慢問題,同時(shí)也便于橫向拓展。
兩種方案對(duì)應(yīng)著 Redis 數(shù)據(jù)增多的兩種拓展方案:垂直擴(kuò)展(scale up)、水平擴(kuò)展(scale out)。
- 垂直拓展:升級(jí)單個(gè) Redis 的硬件配置,比如增加內(nèi)存容量、磁盤容量、使用更強(qiáng)大的 CPU。
- 水平拓展:橫向增加 Redis 實(shí)例個(gè)數(shù),每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分?jǐn)?shù)據(jù)。
比如需要一個(gè)內(nèi)存 24 GB 磁盤 150 GB 的服務(wù)器資源,有以下兩種方案。
3-24
在面向百萬(wàn)、千萬(wàn)級(jí)別的用戶規(guī)模時(shí),橫向擴(kuò)展的 Redis 切片集群會(huì)是一個(gè)非常好的選擇。
Redis Cluster 在 Redis 3.0 及以上版本提供,是一種分布式數(shù)據(jù)庫(kù)方案,通過分片(sharding)來(lái)進(jìn)行數(shù)據(jù)管理(分治思想的一種實(shí)踐),并提供復(fù)制和故障轉(zhuǎn)移功能。
Redis Cluster 并沒有使用一致性哈希算法,而是將數(shù)據(jù)劃分為 16384 的 slots ,每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分 slots,slot 的信息存儲(chǔ)在每個(gè)節(jié)點(diǎn)中。
它是去中心化的,如圖 3-25 所示,該集群有三個(gè) Redis mater 節(jié)點(diǎn)組成(省略每個(gè) master 對(duì)應(yīng)的的 slave 節(jié)點(diǎn)),每個(gè)節(jié)點(diǎn)負(fù)責(zé)整個(gè)集群的一部分?jǐn)?shù)據(jù),每個(gè)節(jié)點(diǎn)負(fù)責(zé)的數(shù)據(jù)多少可以不一樣。
圖 3-25
三個(gè)節(jié)點(diǎn)相互連接組成一個(gè)對(duì)等的集群,它們之間通過 Gossip協(xié)議相互交互集群信息,最后每個(gè)節(jié)點(diǎn)都保存著其他節(jié)點(diǎn)的 slots 分配情況。
Chaya:“Redis Cluster 如何實(shí)現(xiàn)自動(dòng)故障轉(zhuǎn)移呢?”
簡(jiǎn)而概之的說,Redis Cluster 會(huì)經(jīng)歷以下三個(gè)步驟實(shí)現(xiàn)自動(dòng)故障轉(zhuǎn)移實(shí)現(xiàn)高可用。
- 故障檢測(cè):集群中每個(gè)節(jié)點(diǎn)都會(huì)定期通過 Gossip 協(xié)議向其他節(jié)點(diǎn)發(fā)送 PING 消息,檢測(cè)各個(gè)節(jié)點(diǎn)的狀態(tài)(在線狀態(tài)、疑似下線狀態(tài) PFAIL、已下線狀態(tài) FAIL)。并通過 Gossip 協(xié)議來(lái)廣播自己的狀態(tài)以及自己對(duì)整個(gè)集群認(rèn)知的改變。
- master 選舉:使用從當(dāng)前故障 master 的所有 slave 選舉一個(gè)提升為 master。
- 故障轉(zhuǎn)移:取消與舊 master 的主從復(fù)制關(guān)系,將舊 master 負(fù)責(zé)的槽位信息指派到當(dāng)前 master,更新 Cluster 狀態(tài)并寫入數(shù)據(jù)文件,通過 gossip 協(xié)議向集群廣播發(fā)送 CLUSTERMSG_TYPE_PONG消息,把最新的信息傳播給其他節(jié)點(diǎn),其他節(jié)點(diǎn)收到該消息后更新自身的狀態(tài)信息或與新 master 建立主從復(fù)制關(guān)系。
發(fā)布訂閱
Redis 發(fā)布訂閱(Pus/Sub)是一種消息通信模式:發(fā)送者通過 PUBLISH發(fā)布消息,訂閱者通過 SUBSCRIBE 訂閱或通過UNSUBSCRIBE 取消訂閱。
發(fā)布到訂閱模式主要包含三個(gè)部分組成。
- 發(fā)布者(Publisher),發(fā)送消息到頻道中,每次只能往一個(gè)頻道發(fā)送一條消息。
- 訂閱者(Subscriber),可以同時(shí)訂閱多個(gè)頻道。
- 頻道(Channel),將發(fā)布者發(fā)布的消息轉(zhuǎn)發(fā)給當(dāng)前訂閱此頻道的訂閱者。
碼哥寫好了一篇技術(shù)文章則通過 “ChannelA” 發(fā)布消息,消息的訂閱者就會(huì)收到“關(guān)注碼哥字節(jié),提升技術(shù)”的消息。
圖4-13
Chaya:“說了這么多,Redis 發(fā)布訂閱能在什么場(chǎng)景發(fā)揮作用呢?”
哨兵間通信
哨兵集群中,每個(gè)哨兵節(jié)點(diǎn)利用 Pub/Sub 發(fā)布訂閱實(shí)現(xiàn)哨兵之間的相互發(fā)現(xiàn)彼此和找到 Slave。
哨兵與 Master 建立通信后,利用 master 提供發(fā)布/訂閱機(jī)制在__sentinel__:hello發(fā)布自己的信息,比如 IP、端口……,同時(shí)訂閱這個(gè)頻道來(lái)獲取其他哨兵的信息,就這樣實(shí)現(xiàn)哨兵間通信。
消息隊(duì)列
之前碼哥跟大家分享過如何利用 Redis List 與 Stream 實(shí)現(xiàn)消息隊(duì)列。我們也可以利用 Redis 發(fā)布/訂閱機(jī)制實(shí)現(xiàn)輕量級(jí)簡(jiǎn)單的 MQ 功能,實(shí)現(xiàn)上下游解耦,需要注意點(diǎn)是 Redis 發(fā)布訂閱的消息不會(huì)被持久化,所以新訂閱的客戶端將收不到歷史消息。
Redis I/O 多線程模型
Redis 官方在 2020 年 5 月正式推出 6.0 版本,引入了 I/O 多線程模型。
謝霸哥:“為什么之前是單線程模型?為什么 6.0 引入了 I/O 多線程模型?主要解決了什么問題?”
今天,咱們就詳細(xì)的聊下 I/O 多線程模型帶來(lái)的效果到底是黛玉騎鬼火,該強(qiáng)強(qiáng),該弱弱;還是猶如光明頂身懷絕技的的張無(wú)忌,招招都是必殺技。
命令執(zhí)行階段,每一條命令并不會(huì)立馬被執(zhí)行,而是進(jìn)入一個(gè)一個(gè) socket 隊(duì)列,當(dāng) socket 事件就緒則交給事件分發(fā)器分發(fā)到對(duì)應(yīng)的事件處理器處理,單線程模型的命令處理如下圖所示。
圖 4-23
謝霸哥:“為什么 Redis6.0 之前是單線程模型?”
以下是官方關(guān)于為什么 6.0 之前一直使用單線程模型的回答。
- Redis 的性能瓶頸主要在于內(nèi)存和網(wǎng)絡(luò) I/O,CPU 不會(huì)是性能瓶頸所在。
- Redis 通過使用 pipelining 每秒可以處理 100 萬(wàn)個(gè)請(qǐng)求,應(yīng)用程序的所時(shí)候用的大多數(shù)命令時(shí)間復(fù)雜度主要使用 O(N) 或 O(log(N)) 的,它幾乎不會(huì)占用太多 CPU。
- 單線程模型的代碼可維護(hù)性高。多線程模型雖然在某些方面表現(xiàn)優(yōu)異,但是它卻引入了程序執(zhí)行順序的不確定性,帶來(lái)了并發(fā)讀寫的一系列問題,增加了系統(tǒng)復(fù)雜度、同時(shí)可能存在線程切換、甚至加鎖解鎖、死鎖造成的性能損耗。
需要注意的是,Redis 多 IO 線程模型只用來(lái)處理網(wǎng)絡(luò)讀寫請(qǐng)求,對(duì)于 Redis 的讀寫命令,依然是單線程處理。
這是因?yàn)?,網(wǎng)絡(luò) I/O 讀寫是瓶頸,可通過多線程并行處理可提高性能。而繼續(xù)使用單線程執(zhí)行讀寫命令,不需要為了保證 Lua 腳本、事務(wù)、等開發(fā)多線程安全機(jī)制,實(shí)現(xiàn)更簡(jiǎn)單。
謝霸哥:“碼哥,你真是斑馬的腦袋,說的頭頭是道?!?/p>
我謝謝您嘞,主線程與 I/O 多線程共同協(xié)作處理命令的架構(gòu)圖如下所示。
圖 4-24
性能排查和優(yōu)化
Redis 通常是我們業(yè)務(wù)系統(tǒng)中一個(gè)重要的組件,比如:緩存、賬號(hào)登錄信息、排行榜等。一旦 Redis 請(qǐng)求延遲增加,可能就會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)“雪崩”。
今天碼哥跟大家一起來(lái)分析下 Redis 突然變慢了我們?cè)撛趺崔k?如何確定 Redis 性能出問題了,出現(xiàn)問題要如何調(diào)優(yōu)解決。
性能基線測(cè)量
張無(wú)劍:“那么,我們?nèi)绾闻卸?Redis 真的變慢呢?”
因此,我們需要對(duì)當(dāng)前環(huán)境下的Redis 基線性能進(jìn)行測(cè)量,即在系統(tǒng)低壓力、無(wú)干擾的條件下,獲取其基本性能水平。
當(dāng)你觀察到 Redis 運(yùn)行時(shí)延遲超過基線性能的兩倍以上時(shí),可以明確判定 Redis 性能已經(jīng)下降。
redis-cli 可執(zhí)行腳本提供了 –intrinsic-latency 選項(xiàng),用來(lái)監(jiān)測(cè)和統(tǒng)計(jì)測(cè)試期間內(nèi)的最大延遲(以毫秒為單位),這個(gè)延遲可以作為 Redis 的基線性能。
需要注意的是,你需要在運(yùn)行 Redis 的服務(wù)器上執(zhí)行,而不是在客戶端中執(zhí)行。
./redis-cli --intrinsic-latency 100
Max latency so far: 4 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 41 microseconds.
Max latency so far: 57 microseconds.
Max latency so far: 78 microseconds.
Max latency so far: 170 microseconds.
Max latency so far: 342 microseconds.
Max latency so far: 3079 microseconds.
45026981 total runs (avg latency: 2.2209 microseconds / 2220.89 nanoseconds per run).
Worst run took 1386x longer than the average latency.注意:參數(shù)100是測(cè)試將執(zhí)行的秒數(shù)。我們運(yùn)行測(cè)試的時(shí)間越長(zhǎng),我們就越有可能發(fā)現(xiàn)延遲峰值。
通常運(yùn)行 100 秒通常是合適的,足以發(fā)現(xiàn)延遲問題了,當(dāng)然我們可以選擇不同時(shí)間運(yùn)行幾次,避免誤差。
我運(yùn)行的最大延遲是 3079 微秒,所以基線性能是 3079 (3 毫秒)微秒。
需要注意的是,我們要在 Redis 的服務(wù)端運(yùn)行,而不是客戶端。這樣,可以避免網(wǎng)絡(luò)對(duì)基線性能的影響。
慢指令監(jiān)控
Chaya:“知道了性能基線后,有什么監(jiān)控手段知道有慢指令呢?”
我們要避免使用時(shí)間復(fù)雜度為 O(n)的指令,盡可能使用O(1)和O(logN)的指令。
涉及到集合操作的復(fù)雜度一般為O(N),比如集合全量查詢HGETALL、SMEMBERS,以及集合的聚合操作 SORT、LREM、 SUNION 等。
Chaya:“代碼不是我寫的,不知道有沒有人用了慢指令,有沒有監(jiān)控呢?”
有兩種方式可以排查到。
- 使用 Redis 慢日志功能查出慢命令。
- latency-monitor(延遲監(jiān)控)工具。
性能問題排查清單
Redis 的指令由單線程執(zhí)行,如果主線程執(zhí)行的操作時(shí)間太長(zhǎng),就會(huì)導(dǎo)致主線程阻塞。一起分析下都有哪些情況會(huì)導(dǎo)致 Redis 性能問題,我們又該如何解決。
1. 網(wǎng)絡(luò)通信導(dǎo)致的延遲
客戶端使用 TCP/IP 連接或 Unix 域連接連接到 Redis。1 Gbit/s 網(wǎng)絡(luò)的典型延遲約為 200 us。
redis 客戶端執(zhí)行一條命令分 4 個(gè)過程:
發(fā)送命令-〉 命令排隊(duì) -〉 命令執(zhí)行-〉 返回結(jié)果
這個(gè)過程稱為 Round trip time(簡(jiǎn)稱 RTT, 往返時(shí)間),mget mset 有效節(jié)約了 RTT,但大部分命令(如 hgetall,并沒有 mhgetall)不支持批量操作,需要消耗 N 次 RTT ,這個(gè)時(shí)候需要 pipeline 來(lái)解決這個(gè)問題。
解決方案
Redis pipeline 將多個(gè)命令連接在一起來(lái)減少網(wǎng)絡(luò)響應(yīng)往返次數(shù)。
圖 5-1
2. 慢指令
根據(jù)上文的慢指令監(jiān)控到慢查詢的指令??梢酝ㄟ^以下兩種方式解決。
- 在 Cluster 集群中,將聚合運(yùn)算等 O(N) 時(shí)間復(fù)雜度操作放到 slave 上運(yùn)行或者在客戶端完成。
- 使用更高效的命令代替。比如使用增量迭代的方式,避免一次查詢大量數(shù)據(jù),具體請(qǐng)查看 SCAN、SSCAN、HSCAN、ZSCAN命令。
除此之外,生產(chǎn)中禁用 KEYS 命令,因?yàn)樗鼤?huì)遍歷所有的鍵值對(duì),所以操作延時(shí)高,只適用于調(diào)試。
3. 開啟透明大頁(yè)(Transparent HugePages)
常規(guī)的內(nèi)存頁(yè)是按照 4 KB 來(lái)分配,Linux 內(nèi)核從 2.6.38 開始支持內(nèi)存大頁(yè)機(jī)制,該機(jī)制支持 2MB 大小的內(nèi)存頁(yè)分配。
Redis 使用 fork 生成 RDB 快照的過程中,Redis 采用寫時(shí)復(fù)制技術(shù)使得主線程依然可以接收客戶端的寫請(qǐng)求。
也就是當(dāng)數(shù)據(jù)被修改的時(shí)候,Redis 會(huì)復(fù)制一份這個(gè)數(shù)據(jù),再進(jìn)行修改。
采用了內(nèi)存大頁(yè),生成 RDB 期間即使客戶端修改的數(shù)據(jù)只有 50B 的數(shù)據(jù),Redis 可能需要復(fù)制 2MB 的大頁(yè)。當(dāng)寫的指令比較多的時(shí)候就會(huì)導(dǎo)致大量的拷貝,導(dǎo)致性能變慢。
使用以下指令禁用 Linux 內(nèi)存大頁(yè)即可解決。
echo never > /sys/kernel/mm/transparent_hugepage/enabled4. swap 交換區(qū)
謝霸哥:“什么是 swap 交換區(qū)?”
當(dāng)物理內(nèi)存不夠用的時(shí)候,操作系統(tǒng)會(huì)將部分內(nèi)存上的數(shù)據(jù)交換到 swap 空間上,防止程序因?yàn)閮?nèi)存不夠用而導(dǎo)致 oom 或者更致命的情況出現(xiàn)。
當(dāng)應(yīng)用進(jìn)程向操作系統(tǒng)請(qǐng)求內(nèi)存發(fā)現(xiàn)不足時(shí),操作系統(tǒng)會(huì)把內(nèi)存中暫時(shí)不用的數(shù)據(jù)交換放在 SWAP 分區(qū)中,這個(gè)過程稱為 SWAP OUT。
當(dāng)該進(jìn)程又需要這些數(shù)據(jù)且操作系統(tǒng)發(fā)現(xiàn)還有空閑物理內(nèi)存時(shí),就會(huì)把 SWAP 分區(qū)中的數(shù)據(jù)交換回物理內(nèi)存中,這個(gè)過程稱為 SWAP IN。
內(nèi)存 swap 是操作系統(tǒng)里將內(nèi)存數(shù)據(jù)在內(nèi)存和磁盤間來(lái)回?fù)Q入和換出的機(jī)制,涉及到磁盤的讀寫。
謝霸哥:“觸發(fā) swap 的情況有哪些呢?”
對(duì)于 Redis 而言,有兩種常見的情況。
- Redis 使用了比可用內(nèi)存更多的內(nèi)存。
- 與 Redis 在同一機(jī)器運(yùn)行的其他進(jìn)程在執(zhí)行大量的文件讀寫 I/O 操作(包括生成大文件的 RDB 文件和 AOF 后臺(tái)線程),文件讀寫占用內(nèi)存,導(dǎo)致 Redis 獲得的內(nèi)存減少,觸發(fā)了 swap。
謝霸哥:“我要如何排查因?yàn)?swap 導(dǎo)致的性能變慢呢?”
Linux 提供了很好的工具來(lái)排查這個(gè)問題,當(dāng)你懷疑由于交換導(dǎo)致的延遲時(shí),只需按照以下步驟排查。
獲取 Redis pid
我省略部分指令響應(yīng)的信息,重點(diǎn)關(guān)注 process_id。
127.0.0.1:6379> INFO Server
# Server
redis_version:7.0.14
process_id:2847
process_supervised:no
run_id:8923cc83412b223823a1dcf00251eb025acab271
tcp_port:6379查找內(nèi)存布局
進(jìn)入 Redis 所在的服務(wù)器的 /proc 文件系統(tǒng)目錄。
cd /proc/2847在這里有一個(gè) smaps 的文件,該文件描述了 Redis 進(jìn)程的內(nèi)存布局,用 grep 查找所有文件中的 Swap 字段。
$ cat smaps | egrep '^(Swap|Size)'
Size: 316 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 8 kB
Swap: 0 kB
Size: 40 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 720896 kB
Swap: 12 kB每行 Size 表示 Redis 實(shí)例所用的一塊內(nèi)存大小,和 Size 下方的 Swap 對(duì)應(yīng)這塊 Size 大小的內(nèi)存區(qū)域有多少數(shù)據(jù)已經(jīng)被換出到磁盤上了,如果 Size == Swap 則說明數(shù)據(jù)被完全換出了。
可以看到有一個(gè) 720896 kB 的內(nèi)存大小有 12 kb 被換出到了磁盤上(僅交換了 12 kB),這就沒什么問題。
Redis 本身會(huì)使用很多大小不一的內(nèi)存塊,所以,你可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 720896KB。不同內(nèi)存塊被換出到磁盤上的大小也不一樣。
敲重點(diǎn)了
如果 Swap 一切都是 0 kb,或者零星的 4k ,那么一切正常。
當(dāng)出現(xiàn)百 MB,甚至 GB 級(jí)別的 swap 大小時(shí),就表明,此時(shí),Redis 實(shí)例的內(nèi)存壓力很大,很有可能會(huì)變慢。
解決方案
- 增加機(jī)器內(nèi)存。
- 將 Redis 放在單獨(dú)的機(jī)器上運(yùn)行,避免在同一機(jī)器上運(yùn)行需要大量?jī)?nèi)存的進(jìn)程,從而滿足 Redis 的內(nèi)存需求。
- 增加 Cluster 集群的數(shù)量分擔(dān)數(shù)據(jù)量,減少每個(gè)實(shí)例所需的內(nèi)存。
5. AOF 和磁盤 I/O 導(dǎo)致的延遲
在不死之身高可用章節(jié)我們知道 Redis 為了保證數(shù)據(jù)可靠性,你可以使用 AOF 和 RDB 內(nèi)存快照實(shí)現(xiàn)宕機(jī)快速恢復(fù)和持久化。
**可以使用 appendfsync **配置將 AOF 配置為以三種不同的方式在磁盤上執(zhí)行 write 或者 fsync (可以在運(yùn)行時(shí)使用 CONFIG SET命令修改此設(shè)置,比如:redis-cli CONFIG SET appendfsync no)。
- no:Redis 不執(zhí)行 fsync,唯一的延遲來(lái)自于 write 調(diào)用,write 只需要把日志記錄寫到內(nèi)核緩沖區(qū)就可以返回。
- everysec:Redis 每秒執(zhí)行一次 fsync,使用后臺(tái)子線程異步完成 fsync 操作。最多丟失 1s 的數(shù)據(jù)。
- always:每次寫入操作都會(huì)執(zhí)行 fsync,然后用 OK 代碼回復(fù)客戶端(實(shí)際上 Redis 會(huì)嘗試將同時(shí)執(zhí)行的許多命令聚集到單個(gè) fsync 中),沒有數(shù)據(jù)丟失。在這種模式下,性能通常非常低,強(qiáng)烈建議使用 SSD 和可以在短時(shí)間內(nèi)執(zhí)行 fsync 的文件系統(tǒng)實(shí)現(xiàn)。
我們通常只是將 Redis 用于緩存,數(shù)據(jù)未命中從數(shù)據(jù)獲取,并不需要很高的數(shù)據(jù)可靠性,建議設(shè)置成 no 或者 everysec。
除此之外,避免 AOF 文件過大 Redis 會(huì)進(jìn)行 AOF 重寫縮小的 AOF 文件大小。
你可以把配置項(xiàng) no-appendfsync-on-rewrite設(shè)置為 yes,表示在 AOF 重寫時(shí)不進(jìn)行 fsync 操作。
也就是說,Redis 實(shí)例把寫命令寫到內(nèi)存后,不調(diào)用后臺(tái)線程進(jìn)行 fsync 操作,就直接向客戶端返回了。
6. fork 生成 RDB 導(dǎo)致的延遲
Redis 必須 fork 后臺(tái)進(jìn)程才能生成 RDB 內(nèi)存快照文件,fork 操作(在主線程中運(yùn)行)本身會(huì)導(dǎo)致延遲。
Redis 使用操作系統(tǒng)的多進(jìn)程寫時(shí)復(fù)制技術(shù) COW(Copy On Write) 來(lái)實(shí)現(xiàn)快照持久化,減少內(nèi)存占用。
圖 5-2
但 fork 會(huì)涉及到復(fù)制大量鏈接對(duì)象,一個(gè) 24 GB 的大型 Redis 實(shí)例執(zhí)行 bgsave生成 RDB 內(nèi)存快照文件 需要復(fù)制 24 GB / 4 kB * 8 = 48 MB 的頁(yè)表。
此外,slave 在加載 RDB 期間無(wú)法提供讀寫服務(wù),所以主庫(kù)的數(shù)據(jù)量大小控制在 2~4G 左右,讓從庫(kù)快速的加載完成。
7. 鍵值對(duì)數(shù)據(jù)集中過期淘汰
Redis 有兩種方式淘汰過期數(shù)據(jù)。
- 惰性刪除:當(dāng)接收請(qǐng)求的時(shí)候檢測(cè) key 已經(jīng)過期,才執(zhí)行刪除。
- 定時(shí)刪除:按照每 100 毫秒的頻率刪除一些過期的 key。
定時(shí)刪除的算法如下。
- 隨機(jī)采樣 CTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(默認(rèn)設(shè)置為 20)`個(gè)數(shù)的 key,刪除所有過期的 key。
- 執(zhí)行之后,如果發(fā)現(xiàn)還有超過 25% 的 key 已過期未被刪除,則繼續(xù)執(zhí)行步驟一。
每秒執(zhí)行 10 次,一次刪除 200 個(gè) key 沒啥性能影響。如果觸發(fā)了第二條,就會(huì)導(dǎo)致 Redis 一致在刪除過期數(shù)據(jù)取釋放內(nèi)存。
謝霸哥:“碼哥,觸發(fā)條件是什么呀?”
大量的 key 設(shè)置了相同的時(shí)間參數(shù),同一秒內(nèi)大量 key 過期,需要重復(fù)刪除多次才能降低到 25% 以下。
簡(jiǎn)而言之:大量同時(shí)到期的 key 可能會(huì)導(dǎo)致性能波動(dòng)。
解決方案
如果一批 key 的確是同時(shí)過期,可以在 EXPIREAT 和 EXPIRE 的過期時(shí)間參數(shù)上,加上一個(gè)一定大小范圍內(nèi)的隨機(jī)數(shù),這樣,既保證了 key 在一個(gè)鄰近時(shí)間范圍內(nèi)被刪除,又避免了同時(shí)過期造成的壓力。
8. bigkey
謝霸哥:“什么是 Bigkey?key 很大么?”
“大”確實(shí)是關(guān)鍵字,但是這里的“大”指的是 Redis 中那些存有較大量元素的集合或列表、大對(duì)象的字符串占用較大內(nèi)存空間的鍵值對(duì)數(shù)據(jù)稱為 Bigkey。用幾個(gè)實(shí)際例子來(lái)說。
- 一個(gè) String 類型的 Key,它的 value 為 5MB(數(shù)據(jù)過大)。
- 一個(gè) List 類型的 Key,它的列表數(shù)量為 10000 個(gè)(列表數(shù)量過多)。
- 一個(gè) Zset 類型的 Key,它的成員數(shù)量為 10000 個(gè)(成員數(shù)量過多)。
- 一個(gè) Hash 格式的 Key,它的成員數(shù)量雖然只有 1000 個(gè)但這些成員的 value 總大小為 10MB(成員體積過大)。
Bigkey 的存在可能會(huì)引發(fā)以下問題。
- 內(nèi)存壓力增大: 大鍵會(huì)占用大量的內(nèi)存,可能導(dǎo)致 Redis 實(shí)例的內(nèi)存使用率過高,Redis 內(nèi)存不斷變大引發(fā) OOM,或者達(dá)到 maxmemory 設(shè)置值引發(fā)寫阻塞或重要 Key 被淘汰。
- 持久化延遲: 在進(jìn)行持久化操作(如 RDB 快照、AOF 日志)時(shí),處理 bigkey 可能導(dǎo)致持久化操作的延遲。
- 網(wǎng)絡(luò)傳輸壓力: 在主從復(fù)制中,如果有 bigkey 的存在,可能導(dǎo)致網(wǎng)絡(luò)傳輸?shù)膲毫υ龃蟆?/li>
- bigkey 的讀請(qǐng)求占用過大帶寬,自身變慢的同時(shí)影響到該服務(wù)器上的其它服務(wù)。
謝霸哥:“如何解決 Bigkey 問題呢?”
- 定期檢測(cè): 使用工具如 redis-cli 的 --bigkeys 參數(shù)進(jìn)行定期掃描和檢測(cè)。
- 優(yōu)化數(shù)據(jù)結(jié)構(gòu): 根據(jù)實(shí)際業(yè)務(wù)需求,優(yōu)化使用的數(shù)據(jù)結(jié)構(gòu),例如使用 HyperLogLog 替代 Set。
- 清理不必要的數(shù)據(jù): Redis 自 4.0 起提供了 UNLINK 命令,該命令能夠以非阻塞的方式緩慢逐步的清理傳入的 Key,通過 UNLINK,你可以安全的刪除大 Key 甚至特大 Key。
- 對(duì)大 key 拆分:如將一個(gè)含有數(shù)萬(wàn)成員的 HASH Key 拆分為多個(gè) HASH Key,并確保每個(gè) Key 的成員數(shù)量在合理范圍,在 Redis Cluster 集群中,大 Key 的拆分對(duì) node 間的內(nèi)存平衡能夠起到顯著作用。
































