一、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