一、Redis數據模型
1.1、查看Redis數據定義:
typedef struct redisDb {kvstore *keys; /* The keyspace for this DB 指向鍵值存儲的指針,用于快速訪問和修改數據庫中的鍵值對*/kvstore *expires; /* Timeout of keys with a timeout set 指向存儲鍵過期時間*/ebuckets hexpires; /* Hash expiration DS. Single TTL per hash (of next min field to expire) 用于存儲哈希鍵的過期時間*/dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) 存儲正在等待數據的阻塞鍵*/dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for* data, and should be unblocked if key is deleted (XREADEDGROUP).* This is a subset of blocking_keys 存儲當鍵被刪除時應解除阻塞的鍵 */dict *ready_keys; /* Blocked keys that received a PUSH 存儲已收到推送數據的阻塞鍵 */dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS 存儲被WATCH命令監控的鍵 */int id; /* Database ID 數據庫唯一標識符 */long long avg_ttl; /* Average TTL, just for stats 鍵的平均生存時間 */unsigned long expires_cursor; /* Cursor of the active expire cycle. 活動過期循環的游標 */list *defrag_later; /* List of key names to attempt to defrag one by one, gradually.指向一個列表的指針,該列表存儲了稍后嘗試進行碎片整理的鍵名;用于優化內存使用,減少內存碎片。*/
} redisDb;struct dict {dictType *type; /*使 dict 成為一個泛型數據結構,能夠根據不同的數據類型和操作需求進行定制*/dictEntry **ht_table[2]; /*用于哈希表擴容、縮容,如果不擴容就使用ht_table[0],擴容就使用ht_table[1] */unsigned long ht_used[2]; /*記錄兩個哈希表已使用情況*/long rehashidx; /* rehashing not in progress if rehashidx == -1 用于記錄 rehash 操作的進度。當 rehashidx 等于 -1 時,表示沒有 rehash 操作正在進行;否則,rehashidx 表示當前 rehash 操作需要處理的桶的索引。控制哈希表的漸進式 rehash 過程,避免一次性 rehash 帶來的性能開銷。*//* Keep small vars at end for optimal (minimal) struct padding */unsigned pauserehash : 15; /* If >0 rehashing is paused 用于控制 rehash 操作的暫停和恢復。當 pauserehash 大于 0 時,表示 rehash 操作被暫停。 */unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above 用于指示是否使用存儲的鍵 API */signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) 用于記錄兩個哈希表的大小指數。哈希表的大小是 2 的 ht_size_exp 次方,快速計算哈希表的大小,便于進行擴容和縮容操作。*/int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) 用于控制是否允許自動調整哈希表的大小。當 pauseAutoResize 大于 0 時,表示不允許自動調整哈希表的大小。*/void *metadata[]; /*用于存儲與字典相關的額外元數據,為字典提供額外的擴展能力,以滿足不同的使用需求。*/
};
1.2、Redis擴容過程
/* Returning DICT_OK indicates a successful expand or the dictionary is undergoing rehashing,* and there is nothing else we need to do about this dictionary currently. While DICT_ERR indicates* that expand has not been triggered (may be try shrinking?)* 用于檢查并可能擴展字典哈希表*/
int dictExpandIfNeeded(dict *d) {/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) return DICT_OK;/* If the hash table is empty expand it to the initial size. */if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) {dictExpand(d, DICT_HT_INITIAL_SIZE);return DICT_OK;}/* If we reached the 1:1 ratio, and we are allowed to resize the hash* table (global setting) or we should avoid it but the ratio between* elements/buckets is over the "safe" threshold, we resize doubling* the number of buckets. * 檢查是否需要擴容,如果負載因子達到或超過 1,或者超過了一個安全閾值(由 dict_force_resize_ratio 控制),并且字典類型允許擴容,則調用 dictExpand 函數進行擴容*/if ((dict_can_resize == DICT_RESIZE_ENABLE && //d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||(dict_can_resize != DICT_RESIZE_FORBID &&d->ht_used[0] >= dict_force_resize_ratio * DICTHT_SIZE(d->ht_size_exp[0]))){if (dictTypeResizeAllowed(d, d->ht_used[0] + 1))dictExpand(d, d->ht_used[0] + 1);return DICT_OK;}return DICT_ERR;
}
漸進式rehash
當 hashtable 中的元素過多的時候,不能一次性 rehash 到
ht[1] ;這樣會長期占用 redis,其他命令得不到響應;所以需要使用漸進式 rehash.
- 先將 ht[0] 中的元素重新經過 hash 函數生成 64 位整數
- 再對ht[1] 長度進行取余,從而映射到ht[1]
漸進式規則: -
- 采用分治的思想,將rehash分到之后的每步增刪改查的操作中
-
- 在定時器中,最大執行一毫秒 rehash ;每次步長 100 個數組槽位;
沖突,負載因子
- 負載因子 = used / size; used是數組存儲元素的個數,size是數組的長度
- 負載因子越小,沖突越小;負載因子越大,沖突越大
- redis的負載因子是1
擴容模擬
- 如果負載因子 > 1,則會發生擴容,擴容的規則是翻倍
- 如果正在fork(在rdb,aof復寫以及rdb-aof混用情況下)時,會阻止擴容;但是此時若負載因子 > 5,索引效率大大降低,則馬上擴容;這里涉及到寫時復制原理(通過延遲復制資源直到需要修改它們時,才真正完成復制操作)
1.3、Redis縮容過程
/* Returning DICT_OK indicates a successful shrinking or the dictionary is undergoing rehashing,* and there is nothing else we need to do about this dictionary currently. While DICT_ERR indicates* that shrinking has not been triggered (may be try expanding?)* 用于檢查并可能縮小字典哈希表 */
int dictShrinkIfNeeded(dict *d) {/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) return DICT_OK;/* If the size of hash table is DICT_HT_INITIAL_SIZE, don't shrink it. */if (DICTHT_SIZE(d->ht_size_exp[0]) <= DICT_HT_INITIAL_SIZE) return DICT_OK;/* If we reached below 1:8 elements/buckets ratio, and we are allowed to resize* the hash table (global setting) or we should avoid it but the ratio is below 1:32,* we'll trigger a resize of the hash table. *根據哈希表的負載因子(已使用桶的數量與哈希表大小的比值)來決定是否需要縮容。如果負載因子低于某個閾值(默認為 1/8,但可以通過 *dict_force_resize_ratio 進行調整),并且字典類型允許縮容,則調用 dictShrink 函數進行縮容 */if ((dict_can_resize == DICT_RESIZE_ENABLE &&d->ht_used[0] * HASHTABLE_MIN_FILL <= DICTHT_SIZE(d->ht_size_exp[0])) ||(dict_can_resize != DICT_RESIZE_FORBID &&d->ht_used[0] * HASHTABLE_MIN_FILL * dict_force_resize_ratio <= DICTHT_SIZE(d->ht_size_exp[0]))){if (dictTypeResizeAllowed(d, d->ht_used[0]))dictShrink(d, d->ht_used[0]);return DICT_OK;}return DICT_ERR;
}
縮容模擬
- 如果負載因子 < 1/8,則會發生縮容;
- 縮容的規則是恰好包含used的2^n
- 恰好的理解:加入此時數組存儲元素個數為9,恰好包含該元素的就是2^4,即16;
拓展
SCAN cursor [MATCH pattern] [COUNT count]
- 工作原理:SCAN 命令通過維護一個內部游標來迭代鍵空間。每次調用 SCAN 命令時,它都會返回當前游標位置的一批鍵以及更新后的游標值。客戶端應該使用返回的游標值作為下一次迭代的輸入,直到游標值變為 0,表示迭代完成。
- 優點:
- 非阻塞:與 KEYS 命令不同,SCAN 命令不會阻塞服務器,因為它是以增量方式迭代鍵空間的。
- 靈活性:SCAN 命令支持模式匹配和每次迭代返回鍵的最大數量,提供了更大的靈活性。
- 資源友好:由于 SCAN 命令不會一次性加載所有鍵到內存中,因此它對服務器資源的消耗更小。
二、高效的數據結構
2.1、Redis是基于K-V存儲的,K-V是如何實現的?
Redis 使用字典(dict)來存儲鍵值對。字典節點結構體 dictEntry 定義了鍵值對的存儲方式:
/* 在redis\src\dict.c中定義存儲k-v的字典節點結構體 */
/* -------------------------- types ----------------------------------------- */
struct dictEntry {void *key; // 一般指向string對象union {void *val; // 指向redisObject對象uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; /* Next entry in the same hash bucket. */
};typedef struct {void *key;dictEntry *next;
} dictEntryNoValue;
每個鍵值對都會有一個dictEntry節點,但是這部分不是用戶直接操作的,而是通過Redis對象來實現的。
/* -------------------------------------------------------------------------------- */
/* Redis對象結構體,在redis\src\server.h中 */
struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or* LFU data (least significant 8 bits frequency* and most significant 16 bits access time). */int refcount;void *ptr; // 指向底層實現數據結構,比如哈希表、雙向鏈表等
};
實質如下圖所示:
用戶并不關心底層具體的數據結構實現,只管上層的提供的API能否實現用戶想要的功能,如zset,hash等, 所以統一redisObject封裝了底層的數據結構,對外提供統一的接口。
- 這些鍵值對是如何保存進Redis并進行讀取操作的?
0vice·GitHub