前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis存储键值方式详解

Redis存储键值方式详解

作者头像
星哥玩云
发布2022-08-13 17:52:38
1.1K0
发布2022-08-13 17:52:38
举报
文章被收录于专栏:开源部署开源部署

redis是一个存储键值对的内存数据库,其存储键值的方式和Java中的HashMap相似。表征redis数据库的结构体是redisDb (在server.h文件中),redis服务器默认有16个数据库,编号从0到15。

typedef struct redisDb {     dict *dict;                /* 键空间 */     dict *expires;              /* 过期键空间 */     dict *blocking_keys;        /* 客户端在等待的键 (BLPOP) */     dict *ready_keys;          /* 接收到 push 的阻塞键 */     dict *watched_keys;        /* WATCHED keys for MULTI/EXEC CAS */     struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */     int id;                    /* Database ID */     long long avg_ttl;          /* Average TTL, just for stats */ } redisDb;

dict 中存储的是 key -> value,而expires存储的 key -> 过期时间

dict是dict.h文件中定义的结构体:

typedef struct dict {     dictType *type;     void *privdata;     dictht ht[2];     long rehashidx; /* rehashing not in progress if rehashidx == -1 */     unsigned long iterators; /* number of iterators currently running */ } dict;

typedef struct dictht {     dictEntry **table;     unsigned long size; //table的大小     unsigned long sizemask;     unsigned long used; //table中键值对的数量 } dictht;

typedef struct dictEntry {     void *key;     union {         void *val;         uint64_t u64;         int64_t s64;         double d;     } v;     struct dictEntry *next; } dictEntry;

dict可以类比为java中的HashMap,dictEntry对应java.util.HashMap.Entry<K, V>,稍微不同的是,dict对entry的table做了简单的封装(即dictht),而且dict中有两个table用于rehash。

分析dict的dictReplace(dict.c文件中),类似于HashMap的put:

/* Add or Overwrite:  * Add an element, discarding the old value if the key already exists.  * Return 1 if the key was added from scratch, 0 if there was already an  * element with such key and dictReplace() just performed a value update  * operation. */ int dictReplace(dict *d, void *key, void *val) {     dictEntry *entry, *existing, auxentry;

    /* Try to add the element. If the key     * does not exists dictAdd will suceed. */     entry = dictAddRaw(d,key,&existing);     if (entry) {         dictSetVal(d, entry, val);         return 1;     }

    /* Set the new value and free the old one. Note that it is important     * to do that in this order, as the value may just be exactly the same     * as the previous one. In this context, think to reference counting,     * you want to increment (set), and then decrement (free), and not the     * reverse. */     auxentry = *existing;     dictSetVal(d, existing, val);     dictFreeVal(d, &auxentry);     return 0; }

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {     int index;     dictEntry *entry;     dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* 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. */     ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];     entry = zmalloc(sizeof(*entry));     entry->next = ht->table[index];     ht->table[index] = entry;     ht->used++;

    /* Set the hash entry fields. */     dictSetKey(d, entry, key);     return entry; }

主要逻辑在dictAddRaw中,也是先取得table中index,然后使用头插法插入到table的链表中。

如果dict处于rehash状态(即rehashidx !=  -1),则在插入的时候,先调用_dictRehashStep,对于rehash中的dict,使用的table是ht[1]。

static void _dictRehashStep(dict *d) {     if (d->iterators == 0) dictRehash(d,1); }

int dictRehash(dict *d, int n) {     int empty_visits = n*10; /* Max number of empty buckets to visit. */     if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {         dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more         * elements because ht[0].used != 0 */         assert(d->ht[0].size > (unsigned long)d->rehashidx);         while(d->ht[0].table[d->rehashidx] == NULL) {             d->rehashidx++;             if (--empty_visits == 0) return 1;         }         de = d->ht[0].table[d->rehashidx];         /* Move all the keys in this bucket from the old to the new hash HT */         while(de) {             unsigned int h;

            nextde = de->next;             /* Get the index in the new hash table */             h = dictHashKey(d, de->key) & d->ht[1].sizemask;             de->next = d->ht[1].table[h];             d->ht[1].table[h] = de;             d->ht[0].used--;             d->ht[1].used++;             de = nextde;         }         d->ht[0].table[d->rehashidx] = NULL;         d->rehashidx++;     }

    /* Check if we already rehashed the whole table... */     if (d->ht[0].used == 0) {         zfree(d->ht[0].table);         d->ht[0] = d->ht[1];         _dictReset(&d->ht[1]);         d->rehashidx = -1;         return 0;     }

    /* More to rehash... */     return 1; }

从代码中可以看出:rehashidx标记了ht[0]中正在rehash的链表的index。

那么,在什么情况下,redis会对dict进行rehash呢?

调用栈: _dictKeyIndex -> _dictExpandIfNeeded -> dictExpand。在计算键的index时,会判断是否需要扩展dict,如果需要扩展,则把dict的rehashidx置为0。

static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing) {     unsigned int idx, table;     dictEntry *he;     if (existing) *existing = NULL;

    /* Expand the hash table if needed */     if (_dictExpandIfNeeded(d) == DICT_ERR)         return -1;     for (table = 0; table <= 1; table++) {         idx = hash & d->ht[table].sizemask;         /* Search if this slot does not already contain the given key */         he = d->ht[table].table[idx];         while(he) {             if (key==he->key || dictCompareKeys(d, key, he->key)) {                 if (existing) *existing = he;                 return -1;             }             he = he->next;         }         if (!dictIsRehashing(d)) break;     }     return idx; }

/* Expand the hash table if needed */ static 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 (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* 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[0].used >= d->ht[0].size &&         (dict_can_resize ||         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))     {         return dictExpand(d, d->ht[0].used*2);     }     return DICT_OK; }

/* Expand or create the hash table */ int dictExpand(dict *d, unsigned long size) {     dictht n; /* the new hash table */     unsigned long realsize = _dictNextPower(size);

    /* the size is invalid if it is smaller than the number of     * elements already inside the hash table */     if (dictIsRehashing(d) || d->ht[0].used > size)         return DICT_ERR;

    /* Rehashing to the same table size is not useful. */     if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */     n.size = realsize;     n.sizemask = realsize-1;     n.table = zcalloc(realsize*sizeof(dictEntry*));     n.used = 0;

    /* 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[0].table == NULL) {         d->ht[0] = n;         return DICT_OK;     }

    /* Prepare a second hash table for incremental rehashing */     d->ht[1] = n;     d->rehashidx = 0;     return DICT_OK; }

从数据结构的角度来看,redis的dict和java的HashMap很像,区别在于rehash:HashMap在resize时是一次性拷贝的,然后使用新的数组,而dict维持了2个dictht,平常使用ht[0],一旦开始rehash则使用ht[0]和ht[1],rehash被分摊到每次的dictAdd和dictFind等操作中。

dictEntry *dictFind(dict *d, const void *key) {     dictEntry *he;     unsigned int h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */     if (dictIsRehashing(d)) _dictRehashStep(d);     h = dictHashKey(d, key);     for (table = 0; table <= 1; table++) { //会遍历d->ht[0]和d->ht[1]         idx = h & d->ht[table].sizemask;         he = d->ht[table].table[idx];         while(he) {             if (key==he->key || dictCompareKeys(d, key, he->key))                 return he; //找到即返回             he = he->next;         }         if (!dictIsRehashing(d)) return NULL;     }     return NULL; }

redis为什么要如此设计?

试想一下,如果和java的HashMap一样,redis也是一次性拷贝,那么当这个dict非常大时,拷贝就会比较耗时,而在这段时间内,redis就无法对外提供服务了。

 这种设计增加了复杂度,开始rehash后,dict的数据分散在ht[0]和ht[1]中,对于查询(dictFind)和删除(dictDelete)和设置(dictReplace),则会遍历ht[0]和ht[1]。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档