redis内部核心数据结构dict源码级分析
mhr18 2024-11-13 11:06 21 浏览 0 评论
redis中所有的kv数据都保存在一个字典dict(相当于Java里的HashMap)里。这个dict字典在很多设计上也和Java的HashMap接近, dict通过数组加链表的方式存储数据,数据超过一定阈值会进行扩容(rehash),扩容时会通过新旧两个表的方式渐进式rehash避免长时间停顿。 dict在redis中作为一种Map,除了存储用户缓存数据,还有非常多的使用场景比如存储所有的redis命令的映射信息。
下面是dict字典的定义(位于dict.h)。有两个dictEntry的table表(用于渐进式rehash,稍后讲解) ht_size_exp字段保存dict的数组长度的指数值,redis的dict也像Java的HashMap一样,数组长度是2的指数倍,每次扩容2倍,这样的好处包含通过hash计算数组中的index只需要做&运算
struct dict {
// 类型
dictType *type;
// 两个hashtable,默认用0,rehash的时候会从0向1逐步转移数据
dictEntry **ht_table[2];
// 存储hashtable中写入了多少元素
unsigned long ht_used[2];
// rehashidx-1的时候说明没有在rehash
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// pauserehash用作暂停rehash的标识,在dictScan的时候会暂停rehash防止漏掉元素或者重复遍历元素
// 把int16_t的小的字段防止结构体最后来尽量优化结构体的padding,类似Java对象的8字节对齐
/* Keep small vars at end for optimal (minimal) struct padding */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
// hashtable数组长度的2的指数值
signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};
数组链表中的链表元素定义为dictEntry,key指向entry的key,v中的val是value数据
/**
* dictEntry是dict字典中的链表元素
*/
typedef struct dictEntry {
// 字典key value的key
void *key;
union {
// value如果是复杂类型使用val指针指向真正的数据比如redisObject
void *val;
// value如果是简单数据类型uint64,int64,double 分别用下面三种字段存储
uint64_t u64;
int64_t s64;
double d;
} v; // union结构
// 形成链表的next引用
struct dictEntry *next; /* Next entry in the same hash bucket. */
void *metadata[]; /* An arbitrary number of bytes (starting at a
* pointer-aligned address) of size as returned
* by dictType's dictEntryMetadataBytes(). */
} dictEntry;
dictEntry->v->val指向的是一个redisObject对象,结构如下 type用来区分具体的对象类型(有OBJ_STRING,OBJ_LIST,OBJ_SET,OBJ_ZSET,OBJ_HASH等) encoding用来区分具体的编码,redis为了提升性能或节省内存会根据不同的情况选择合适的对象编码方式 lru用作内存淘汰,如果是LRU保存的是最近访问时间,如果是LFU保存8bit的访问次数和16bit的访问时间 refcount引用计数 ptr指向真正的value对象
typedef 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;
} robj;
dictType保存字典类型,不同的字段可以配置不同的hashFunction等函数。
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(dict *d, const void *key);
void *(*valDup)(dict *d, const void *obj);
int (*keyCompare)(dict *d, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
void (*valDestructor)(dict *d, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
/* Allow a dictEntry to carry extra caller-defined metadata. The
* extra memory is initialized to 0 when a dictEntry is allocated. */
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
dict添加元素的方法实现
/**
* 向dict中添加元素,如果对应的key已经存在,返回DICT_ERR。能够覆盖key对应的value的方法是dictReplace
*/
/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
// 先创建出对应的dictEntry
dictEntry *entry = dictAddRaw(d,key,NULL);
// 如果entry为NULL说明key已经存在返回DICT_ERR
if (!entry) return DICT_ERR;
// 调用dictSetVal把val设置到对应的entry中
dictSetVal(d, entry, val);
return DICT_OK;
}
/**
* 向dict中添加entry,如果对应的key已经存在了,返回NULL,如果不存在返回创建的entry
*/
/* Low level add or find:
* This function adds the entry but instead of setting a value returns the
* dictEntry structure to the user, that will make sure to fill the value
* field as they wish.
*
* This function is also directly exposed to the user API to be called
* mainly in order to store non-pointers inside the hash value, example:
*
* entry = dictAddRaw(dict,mykey,NULL);
* if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
*
* Return values:
*
* If key already exists NULL is returned, and "*existing" is populated
* with the existing entry if existing is not NULL.
*
* If key was added, the hash entry is returned to be manipulated by the caller.
*/
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
int htidx;
// 如果dict在rehash过程中,执行一步rehash step
if (dictIsRehashing(d)) _dictRehashStep(d);
// 获取对应key在数组中的index,如果key已经存在返回NULL
/* Get the index of the new element, or -1 if
* the element already exists. */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
// 如果在rehash过程中,写入到新table即index=1的table
htidx = dictIsRehashing(d) ? 1 : 0;
size_t metasize = dictMetadataSize(d);
// 分配内存
entry = zmalloc(sizeof(*entry) + metasize);
if (metasize > 0) {
memset(dictMetadata(entry), 0, metasize);
}
// 头插法,新创建的entry放到链表的表头,新创建的next指向原有的表头
entry->next = d->ht_table[htidx][index];
d->ht_table[htidx][index] = entry;
// hashtable used也就是元素数量加一
d->ht_used[htidx]++;
// 设置对应entry的key字段
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
dict查找元素
/**
* 在dict中查找对应key的entry
*/
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
// dict大小为0,说明dict中没有元素
if (dictSize(d) == 0) return NULL; /* dict is empty */
// 如果在rehash中,进行一次增量rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
// 计算key的hash值
h = dictHashKey(d, key);
// 先查询旧table,如果旧table没有并且在rehash中则继续查新table
for (table = 0; table <= 1; table++) {
// 计算数组index
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
// 获取链表头节点
he = d->ht_table[table][idx];
// 遍历
while(he) {
// 判断是否是同一个key,类似java里的相同引用或equals
if (key==he->key || dictCompareKeys(d, key, he->key))
// 查找到则返回
return he;
// 继续遍历
he = he->next;
}
// 如果没有在rehash,则不需要查询index=1的table
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
rehash
当dict中存入的元素比较多时,链表会变长,查找速度就会变慢,所以为了保证性能,dict需要增加数组长度来降低链表长度。
rehash触发时机: 默认情况下dict元素数量大于等于数组长度(特殊情况可以关闭扩容)
dictAdd方法会调用dictAddRaw,dictAddRaw中会调用_dictKeyIndex。 _dictKeyIndex方法会调用_dictExpandIfNeeded来进行判断是否要扩容或初始化,如果需要进行扩容或初始化。
/**
* 按需进行初始化或扩容
*/
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
// 如果已经在rehash中了,返回
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
// 如果table还没有初始化,则进行初始化
/* If the hash table is empty expand it to the initial size. */
if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 如果table里的元素数量除以table数组长度大于阈值(默认5),并且这个dict对应的类型允许扩容,则进行扩容。
/* 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. */
if (d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0]) &&
(dict_can_resize ||
d->ht_used[0]/ DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio) &&
dictTypeExpandAllowed(d))
{
return dictExpand(d, d->ht_used[0] + 1);
}
return DICT_OK;
}
/**
* 扩容或创建hash table
* 参数的size是新的hash table要能容纳的元素的数量,
*/
/* Expand or create the hash table,
* when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
* Returns DICT_OK if expand was performed, and DICT_ERR if skipped. */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
if (malloc_failed) *malloc_failed = 0;
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht_used[0] > size)
return DICT_ERR;
/* the new hash table */
dictEntry **new_ht_table;
unsigned long new_ht_used;
// 计算新的hash table的数组大小,是第一个大于等于size的2的指数倍的值,比如size=5,则new_ht_size_exp是8
signed char new_ht_size_exp = _dictNextExp(size);
// 检测越界的情况,发生在数组长度太大的情况
/* Detect overflows */
size_t newsize = 1ul<<new_ht_size_exp;
if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;
// 创建新的hash table并且初始化所有的指针为NULL
/* Allocate the new hash table and initialize all pointers to NULL */
if (malloc_failed) {
new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
*malloc_failed = new_ht_table == NULL;
if (*malloc_failed)
return DICT_ERR;
} else
new_ht_table = zcalloc(newsize*sizeof(dictEntry*));
new_ht_used = 0;
// 第一次初始化的情况和扩容的区别在于扩容需要设置rehashidx=0开启rehash,并且赋值的table index不同。
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht_table[0] == NULL) {
d->ht_size_exp[0] = new_ht_size_exp;
d->ht_used[0] = new_ht_used;
d->ht_table[0] = new_ht_table;
return DICT_OK;
}
// 保存新的table到index=1的位置,并且设置rehashidx=0开启rehash
/* Prepare a second hash table for incremental rehashing */
d->ht_size_exp[1] = new_ht_size_exp;
d->ht_used[1] = new_ht_used;
d->ht_table[1] = new_ht_table;
d->rehashidx = 0;
return DICT_OK;
}
rehash方法中包含了增量rehash的过程,在dict的增加查询等方法中都会判断如果当前是rehash中,进行一次增量rehash,同时也提供了dictRehashMilliseconds方法来实现一定时间的增量rehash。
/**
* 执行n此增量(渐进式)rehash。如果可能还有key要从旧table移动到新table则返回1,否则0
* n表示迁移n个bucket(数组元素),空的数组元素不算。为了避免太多空元素阻塞较长时间,限制最多访问n*10个空元素
*
*/
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
// 最多访问到10个空的bucket
int empty_visits = n*10; /* Max number of empty buckets to visit. */
// 如果rehashing已经结束,返回
if (!dictIsRehashing(d)) return 0;
// 执行n次或者直到旧table里已经没有元素
while(n-- && d->ht_used[0] != 0) {
// de是当前在移动的dictEntry,nextde是遍历链表用的next
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
// 只要bucket是空的,就增加rehashidx移动到下一个bucket,如果总的遍历过的空bucket达到empty_visits,返回
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
// 设置de为链表头
de = d->ht_table[0][d->rehashidx];
// 循环当前链表所有元素迁移到新table
/* Move all the keys in this bucket from the old to the new hash HT */
while(de) {
uint64_t h;
// 保存next
nextde = de->next;
// 计算当前entry的ke一种新table里的hash
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
// 头插法插入到新table
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
// 旧表元素数量减一
d->ht_used[0]--;
// 新表元素数量加1
d->ht_used[1]++;
// 替换de为nextde继续遍历
de = nextde;
}
// 清空旧table这个index的引用
d->ht_table[0][d->rehashidx] = NULL;
// 移动rehashidx到下一个位置
d->rehashidx++;
}
// 检查下旧table是不是已经迁移完成
/* Check if we already rehashed the whole table... */
if (d->ht_used[0] == 0) {
// 如果迁移完成,清理旧table内存
zfree(d->ht_table[0]);
// index=1的table换到index=0上
/* Copy the new ht onto the old one */
d->ht_table[0] = d->ht_table[1];
d->ht_used[0] = d->ht_used[1];
d->ht_size_exp[0] = d->ht_size_exp[1];
// 重置index=1的table的ht_table、ht_used、ht_size_exp
_dictReset(d, 1);
// rehashidx=-1表示rehash结束
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
Post Directory
dict添加元素的方法实现dict查找元素rehash
相关推荐
- MySQL数据库中,数据量越来越大,有什么具体的优化方案么?
-
个人的观点,这种大表的优化,不一定上来就要分库分表,因为表一旦被拆分,开发、运维的复杂度会直线上升,而大多数公司和开发人员是欠缺这种能力的。所以MySQL中几百万甚至小几千万的表,先考虑做单表的优化。...
- Redis的Bitmap(位图):签到打卡、用户在线状态,用它一目了然
-
你是不是每天打开APP,第一时间就是去“签到打卡”?或者在社交软件里,看到你的朋友头像旁边亮着“在线”的绿灯?这些看似简单的功能背后,都隐藏着一个有趣而高效的数据结构。如果让你来设计一个签到系统:用户...
- 想知道有多少人看了你的文章?Redis HyperLogLog几KB就搞定!
-
作为一名内容创作者,你每天最期待的,除了文章阅读量蹭蹭上涨,是不是还特别想知道,到底有多少个“独立用户”阅读了你的文章?这个数字,我们通常称为“UV”(UniqueVisitors),它比总阅读量更...
- Redis的“HyperLogLog”:统计网站日活用户,省内存又高效的神器
-
你可能从未听过这个拗口的名字——“HyperLogLog”,它听起来就像是某个高深莫测的数学公式。但请相信我,理解它的核心思想并不难,而且一旦你掌握了它,你会发现它在处理大数据统计问题时,简直就是“救...
- 阿里云国际站:为什么我的云服务器运行缓慢?
-
本文由【云老大】TG@yunlaoda360撰写一、网络性能瓶颈带宽不足现象:上传/下载速度慢,远程连接卡顿。排查:通过阿里云控制台查看网络流量峰值是否接近带宽上限34。解决:升级带宽(如从1M提...
- Java 近期新闻:Jakarta EE 11和Spring AI更新、WildFly 36.0 Beta、Infinispan
-
作者|MichaelRedlich译者|明知山策划|丁晓昀OpenJDKJEP503(移除32位x86移植版本)已从“ProposedtoTarget”状态进入到“T...
- 腾讯云国际站:怎样设置自动伸缩应对流量高峰?
-
云计算平台服务以阿里云为例:开通服务与创建伸缩组:登录阿里云控制台,找到弹性伸缩服务并开通。创建伸缩组时,选择地域与可用区,定义伸缩组内最小/最大实例数,绑定已有VPC虚拟交换机。实例模板需...
- 【案例分享】如何利用京东云建设高可用业务架构
-
本文以2022年一个实际项目为基础,来演示在京东云上构建高可用业务的整个过程。公有云及私有云客户可通过使用京东云的弹性IAAS、PAAS服务,创建高可用、高弹性、高可扩展、高安全的云上业务环境,提升业...
- Spring Security在前后端分离项目中的使用
-
1文章导读SpringSecurity是Spring家族中的一个安全管理框架,可以和SpringBoot项目很方便的集成。SpringSecurity框架的两大核心功能:认证和授权认证:...
- Redis与Java集成的最佳实践
-
Redis与Java集成的最佳实践在当今互联网飞速发展的时代,缓存技术的重要性毋庸置疑。Redis作为一款高性能的分布式缓存数据库,与Java语言的结合更是如虎添翼。今天,我们就来聊聊Redis与Ja...
- Redis在Java项目中的应用与数据持久化
-
Redis在Java项目中的应用与数据持久化Redis简介:为什么我们需要它?在Java项目中,Redis就像一位不知疲倦的快跑选手,总能在关键时刻挺身而出。作为一个内存数据库,它在处理高并发请求时表...
- Redis 集群最大节点个数是多少?
-
Redis集群最大节点个数取决于Redis的哈希槽数量,因为每个节点可以负责多个哈希槽。在Redis3.0之前,Redis集群最多支持16384个哈希槽,因此最大节点数为16384个。但是在Redi...
- Java开发岗面试宝典:分布式相关问答详解
-
今天千锋广州Java小编就给大家分享一些就业面试宝典之分布式相关问题,一起来看看吧!1.Redis和Memcache的区别?1、存储方式Memecache把数据全部存在内存之中,断电后会挂掉,数据不...
- 当Redis内存不足时,除了加内存,还有哪些曲线救国的办法?
-
作为“速度之王”的Redis,其高性能的秘密武器之一就是将数据存储在内存中。然而,内存资源是有限且昂贵的。当你的Redis实例开始告警“内存不足”,或者写入请求被阻塞时,最直接的解决方案似乎就是“加内...
- 商品详情页那么多信息,Redis的“哈希”如何优雅存储?
-
你每天网购时,无论是打开淘宝、京东还是拼多多,看到的商品详情页都琳琅满目:商品名称、价格、库存、图片、描述、评价数量、销量。这些信息加起来,多的惊人。那么问题来了:这些海量的商品信息,程序是去哪里取出...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- MySQL数据库中,数据量越来越大,有什么具体的优化方案么?
- Redis的Bitmap(位图):签到打卡、用户在线状态,用它一目了然
- 想知道有多少人看了你的文章?Redis HyperLogLog几KB就搞定!
- Redis的“HyperLogLog”:统计网站日活用户,省内存又高效的神器
- 阿里云国际站:为什么我的云服务器运行缓慢?
- Java 近期新闻:Jakarta EE 11和Spring AI更新、WildFly 36.0 Beta、Infinispan
- 腾讯云国际站:怎样设置自动伸缩应对流量高峰?
- 【案例分享】如何利用京东云建设高可用业务架构
- Spring Security在前后端分离项目中的使用
- Redis与Java集成的最佳实践
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)