標籤: Redis Redis數據結構 Redis內存管理策略 Redis數據類型 Redis類型映射
做者:王清培(Plen wang) 滬江Java資深架構師redis
dict 字典是基於 hash算法 來實現,是 Hash 數據類型的底層存儲數據結構。咱們來看下 redis 3.0.0 版本的 dict.h 頭文件定義。算法
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; int iterators; } dict;
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
說到 hash table 有兩個東西是咱們常常會碰到的,首先就是 hash 碰撞 問題,redis dict 是採用鏈地址法來解決,dictEntry->next 就是指向下個衝突 key 的節點。數據庫
還有一個常常碰到的就是 rehash 的問題,提到 rehash 咱們仍是有點擔憂性能的。那麼redis 實現是很是巧妙的,採用 惰性漸進式 rehash 算法 。數組
在 dict struct 裏有一個 ht[2] 組數,還有一個 rehashidx 索引。redis 進行 rehash 的大體算法是這樣的,首先會開闢一個新的 dictht 空間,放在 ht[2] 索引上,此時將 rehashidx 設置爲0,表示開始進入 rehash 階段,這個階段可能會持續很長時間,rehashidx 表示 dictEntry 個數。緩存
每次當有對某個 ht[1] 索引中的 key 進行訪問時,獲取、刪除、更新,redis 都會將當前 dictEntry 索引中的全部 key rehash 到 ht[2] 字典中。一旦 rehashidx=-1 表示 rehash 結束。服務器
skip list 是 zset 的底層數據結構,有着高性能的查找排序能力。數據結構
咱們都知道通常用來實現帶有排序的查找都是用 Tree 來實現,不論是各類變體的 B Tree 仍是 B+ Tree,本質都是用來作順序查找。架構
skip list 實現起來簡單,性能也與 B Tree 相接近。併發
typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist;
zskiplistNode->zskiplistLevel->span 這個值記錄了當前節點距離下個節點的跨度。每個節點會有最大不超過 zskiplist->level 節點個數,分別用來表示不一樣跨度與節點的距離。app
每一個節點會有多個 forward 向前指針,只有一個 backward 指針。每一個節點會有對象 __*obj__ 和 score 分值,每一個分值都會按照順序排列。
int set 整數集合是 set 數據類型的底層實現數據結構,它的特色和使用場景很明顯,只要咱們使用的集合都是整數且在必定的範圍以內都會使用整數集合編碼。
SADD set:userid 100 200 300 (integer) 3
OBJECT encoding set:userid "intset"
int set 使用一塊連續的內存來存儲集合數據,它是數組結構不是鏈表結構。
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
intset->encoding 用來肯定 contents[] 是什麼類型的整數編碼,如下三種值之一。
#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
redis 會根據咱們設置的值類型動態 sizeof 出一個對應的空間大小。若是咱們集合原來是 int16 ,而後往集合裏添加了 int32 整數將觸發升級,一旦升級成功不會觸發降級操做。
zip list 壓縮表是 list、zset、hash 數據類型的底層數據結構之一。它是爲了節省內存經過壓縮數據存儲在一塊連續的內存空間中。
typedef struct zlentry { unsigned int prevrawlensize, prevrawlen; unsigned int lensize, len; unsigned int headersize; unsigned char encoding; unsigned char *p; } zlentry;
它最大的優勢就是壓縮空間,空間利用率很高。缺點就是一旦出現更新可能就是連鎖更新,由於數據在內容空間中都是連續的,最極端狀況下就是可能出現順序連鎖擴張。
壓縮列表會由多個 zlentry 節點組成,每個 zlentry 記錄上一個節點長度和大小,當前節點長度 lensize 和大小 len 包括編碼 encoding 。
這取決於業務場景,redis 提供了一組配置,專門用來針對不一樣的場景進行閾值控制。
hash-max-ziplist-entries 512 hash-max-ziplist-value 64
list-max-ziplist-entries 512 list-max-ziplist-value 64
zset-max-ziplist-entries 128 zset-max-ziplist-value 64
上述配置分別用來配置 ziplist 做爲 hash 、list、zset 數據類型的底層壓縮閾值控制。
redis 內部每一種數據類型都是對象化的,也就是咱們所說的5種數據類型其實內部都會對應到 redisObject 對象,而後在由 redisObject 來包裝具體的存儲數據結構和編碼。
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:REDIS_LRU_BITS; int refcount; void *ptr; } robj;
這是一個很 OO 的設計,redisObject->type 是 5 種數據類型之一,redisObject->encoding 是這個數據類型所使用的數據結構和編碼。
咱們看下 redis 提供的 5 種數據類型與每一種數據類型對應的存儲數據結構和編碼。
/* Object types */ #define REDIS_STRING 0 #define REDIS_LIST 1 #define REDIS_SET 2 #define REDIS_ZSET 3 #define REDIS_HASH 4
#define REDIS_ENCODING_RAW 0 #define REDIS_ENCODING_INT 1 #define REDIS_ENCODING_HT 2 #define REDIS_ENCODING_ZIPMAP 3 #define REDIS_ENCODING_LINKEDLIST 4 #define REDIS_ENCODING_ZIPLIST 5 #define REDIS_ENCODING_INTSET 6 #define REDIS_ENCODING_SKIPLIST 7 #define REDIS_ENCODING_EMBSTR 8
REDIS_ENCODING_ZIPMAP 3 這個編碼能夠忽略了,在特定的狀況下有性能問題,在 redis 2.6 版本以後已經廢棄,爲了兼容性保留。
上圖是 redis 5 種數據類型與底層數據結構和編碼的對應關係,可是這種對應關係在每個版本中都會有可能發生變化,這也是 redisObject 的靈活性所在,有着 OO 的這種多態性。
redisObject->refcount 表示當前對象的引用計數,在 redis 內部爲了節省內存採用了共享對象的方法,當某個對象被引用的時候這個 refcount 會加 1,釋放的時候會減 1。
redisObject->lru 表示當前對象的 空轉時長,也就是 idle time ,這個時間會是 redis lru 算法用來釋放對象的時間依據。能夠經過 OBJECT idletime 命令查看某個 key 的空轉時長 lru 時間。
redis 在服務端分別爲不一樣的 db index 維護一個 dict 這個 dict 稱爲 key space 鍵空間 。每個 redis client 只能屬於一個 db index ,在 redis 服務端會維護每個連接的 redisClient 。
typedef struct redisClient { uint64_t id; int fd; redisDb *db; } redisClient;
在服務端每個 redis 客戶端都會有一個指向 redisDb 的指針。
typedef struct redisDb { dict *dict; dict *expires; dict *blocking_keys; dict *ready_keys; dict *watched_keys; struct evictionPoolEntry *eviction_pool; int id; long long avg_ttl; } redisDb;
key space 鍵空間就是這裏的 redisDb->dict 。redisDb->expires 是維護全部鍵空間的每個 key 的過時時間。
對於一個 key 咱們能夠設置它多少秒、毫秒以後過時,也能夠設置它在某個具體的時間點過時,後者是一個時間戳。
EXPIRE 命令能夠設置某個 key 多少秒以後過時
PEXPIRE 命令能夠設置某個 key 多少毫秒以後過時
EXPIREAT 命令能夠設置某個 key 在多少秒時間戳以後過時
PEXPIREAT 命令能夠設置某個 key 在多少毫秒時間戳以後過時
PERSIST 命令能夠移除鍵的過時時間
其實上述命令最終都會被轉換成對 PEXPIREAT 命令。在 redisDb->expires 指向的 key 字典中維護着一個到期的毫秒時間戳。
TTL、PTTL 能夠經過這兩個命令查看某個 key 的過時秒、毫秒數。
redis 內部有一個 事件循環,這個事件循環會檢查鍵的過時時間是否小於當前時間,若是小於則會刪除這個鍵。
在使用 redis 的時候咱們最關心的就是鍵是如何被刪除的,如何高效的準時的刪除某個鍵。其實 redis 提供了兩個方案來完成這件事情。
redis 採用 惰性刪除 、 按期刪除 雙重刪除策略。
當咱們訪問某個 key 的時候 redis 會檢查它是否過時,這是惰性刪除。
robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; expireIfNeeded(db,key); val = lookupKey(db,key); if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val; }
int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); mstime_t now; if (when < 0) return 0; /* No expire for this key */ if (server.loading) return 0; now = server.lua_caller ? server.lua_time_start : mstime(); if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id); return dbDelete(db,key); }
redis 也會經過 事件循環 週期性的執行 key 的過時刪除動做,這是按期刪除。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* Handle background operations on Redis databases. */ databasesCron(); }
void databasesCron(void) { /* Expire keys by random sampling. Not required for slaves * as master will synthesize DELs for us. */ if (server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); }
惰性刪除 是每次只要有讀取、寫入都會觸發惰性刪除代碼。週期刪除 是由 redis EventLoop 來觸發的。redis 內部不少維護性工做都是基於 EventLoop 。
既然鍵會隨時存在過時問題,那麼涉及到持久化 redis 是如何幫咱們處理的。
當 redis 使用 RDB 方式持久化時,每次持久化的時候就會檢查這些即將被持久化的 key 是否已通過期,若是過時將直接忽略,持久化那些沒有過時的鍵。當 redis 做爲 master 主服務器 啓動的時候,在載入 rdb 持久化鍵時也會檢查這些鍵是否過時,將忽略過時的鍵,只載入沒過時的鍵。
當 redis 使用 AOF 方式持久化時,每次遇到過時的 key redis 會追加一條 DEL 命令 到 AOF 文件,也就是說只要咱們順序載入執行 AOF 命令文件就會刪除過時的鍵。
若是 redis 做爲從服務器啓動的化,它一旦與 master 主服務器 創建連接就會清空全部數據進行完整同步,固然新版本的 redis 支持 SYCN2 的半同步。若是是已經創建了 master/slave 主從同步以後,主服務器會發送 DEL 命令給全部從服務器執行刪除操做。
在使用 redis 的時候咱們會設置 maxmemory 選項,64 位的默認是 0 不限制。線上的服務器必需要設置的,要否則頗有可能致使 redis 宿主服務器直接內存耗盡最後連接都上不去。
因此基本要設置兩個配置:
maxmemory 最大內存閾值
maxmemory-policy 到達閾值的執行策略
能夠經過 CONFIG GET maxmemory/maxmemory-policy 分別查看這兩個配置值,也能夠經過 CONFIG SET 去分別配置。
maxmemory-policy 有一組配置,能夠用在不少場景下:
noeviction:客戶端嘗試執行會讓更多內存被使用的命令直接報錯
allkeys-lru: 在全部key裏執行lru算法
volatile-lru:在全部已通過期的key裏執行lru算法
allkeys-random:在全部key裏隨機回收
volatile-random:在已通過期的key裏隨機回收
volatile-ttl:回收已通過期的key,而且優先回收存活時間(TTL)較短的鍵
關於 cache 的命中率能夠經過 info 命令查看 鍵空間的命中率和未命中率。
# Stats keyspace_hits:33 keyspace_misses:5
maxmemory 在到達閾值的時候會採用必定的策略去釋放內存,這些策略咱們能夠根據本身的業務場景來選擇,默認是 noeviction 。
redis LRU 算法有一個取樣的優化機制,能夠經過必定的取樣因子來增強回收的 key 的準確度。CONFIG GET maxmemory-samples 查看取樣配置,具體能夠參考更加詳細的文章。
redis 自己提供持久化功能,有兩種持久化機制,一種是數據持久化 RDB ,一種是命令持久化 AOF,這兩種持久化方式各有優缺點,也能夠組合使用,一旦組合使用 redis 在載入數據的時候會優先載入 aof 文件,只有當 AOF 持久化關閉的時候纔會載入 rdb 文件。
RDB 是 redis 數據庫,redis 會根據一個配置來觸發持久化。
#save <seconds> <changes> save 900 1 save 300 10 save 60 10000
CONFIG GET save 1) "save" 2) "3600 1 300 100 60 10000"
表示在多少秒之類的變化次數,一旦達到這個觸發條件 redis 將觸發持久化動做。redis 在執行持久化的時候有兩種模式 BGSAVE、SAVE 。BGSAVE 是後臺保存,redis 會 fork 出一個子進程來處理持久化,不會 block 用戶的執行請求。而 SAVE 則會 block 用戶執行請求。
struct redisServer { long long dirty;/* Changes to DB from the last save */ time_t lastsave; /* Unix time of last successful save */ long long dirty_before_bgsave; pid_t rdb_child_pid;/* PID of RDB saving child */ struct saveparam *saveparams; /* Save points array for RDB */ }
struct saveparam { time_t seconds; int changes; };
redisServer 包含的信息不少,其中就包含了有關於 RDB 持久化的信息。redisServer->dirty 至上次 save 到目前爲止的 change 數。redisServer->lastsave 上次 save 時間。
saveparam struct 保存了咱們經過 save 命令設置的參數,time_t 是個 long 時間戳。
typedef __darwin_time_t time_t;
typedef long __darwin_time_t; /* time() */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > REDIS_BGSAVE_RETRY_DELAY || server.lastbgsave_status == REDIS_OK)) { redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...", sp->changes, (int)sp->seconds); rdbSaveBackground(server.rdb_filename); break; } } }
redis 事件循環會週期性的執行 serverCron 方法,這段代碼會循環遍歷 server.saveparams 參數鏈表。
若是 server.dirty 大於等於 咱們參數裏配置的變化而且 server.unixtime-server.lastsave 大於參數裏配置的時間而且 server.unixtime-server.lastbgsave_try 減去 bgsave 重試延遲時間或者當前 server.lastbgsave_status==REDIS_OK 則執行 rdbSaveBackground 方法。
AOF 持久化是採用對文件進行追加對方式進行,每次追加都是 redis 處理的 命令。有點相似 command sourcing 命令溯源 的模式。
只要咱們能夠將全部的命令按照執行順序在重放一遍就能夠還原最終的 redis 內存狀態。
AOF 持久化最大的優點是能夠縮短數據丟失的間隔,能夠作到秒級的丟失率。RDB 會丟失上一個保存週期到目前的全部數據,只要沒有觸發 save 命令設置的 save seconds changes 閾值數據就會一直不被持久化。
struct redisServer { /* AOF buffer, written before entering the event loop */ sds aof_buf; }
struct sdshdr { unsigned int len; unsigned int free; char buf[]; };
aof_buf 是命令緩存區,採用 sds 結構緩存,每次當有命令被執行當時候都會寫一次到 aof_buf 中。有幾個配置用來控制 AOF 持久化的機制。
appendonly no appendfilename "appendonly.aof"
appendonly 用來控制是否開啓 AOF 持久化,appendfilename 用來設置 aof 文件名。
appendfsync always appendfsync everysec appendfsync no
appendfsync 用來控制命令刷盤機制。如今操做系統都有文件 cache/buffer 的概念,全部的寫入和讀取都會走 cache/buffer,並不會每次都同步刷盤,由於這樣性能必定會受影響。因此 redis 也提供了這個選項讓咱們來本身根據業務場景控制。
always :每次將 aof_buf 命令寫入 aof 文件而且執行實時刷盤。
everysec :每次將 aof_buf 命令寫入 aof 文件,可是每隔一秒執行一次刷盤。
no :每次將 aof_buf 命令寫入 aof 文件不執行刷盤,由操做系統來自行控制。
AOF 也是採用後臺子進程的方式進行,與主進程共享數據空間也就是 aof_buf,可是隻要開始了 AOF 子進程以後 redis 事件循環文件事件處理器 會將以後的命令寫入另一個 aof_buf ,這樣就能夠作到平滑的切換。
AOF 會不斷的追加命令進 aof 文件,隨着時間和併發量的加大 aof 文件會極速膨脹,因此有必要對這個文件大小進行優化。redis 基於 rewrite 重寫對文件進行壓縮。
no-appendfsync-on-rewrite no/yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
no-appendfsync-on-rewrite 控制是否在 bgrewriteaof 的時候還須要進行命令追加,若是追加可能會出現磁盤 IO 跑高現象。
上面說過,當 AOF 進程在執行的時候原來的事件循環還會正常的追加命令進 aof 文件,同時還會追加命令進另一個 aof_buf ,用來作新 aof 文件的重寫。這是兩條並行的動做,若是咱們設置成 yes 就不追加原來的 aof_buf 由於新的 aof 文件已經包含了以後進來的命令。
auto-aof-rewrite-percentage、auto-aof-rewrite-min-size 64mb 這兩個配置前者是文件增加百分比來進行 rewrite ,後者是按照文件大小增加進行 rewrite 。